1use super::rendering::ChartView;
4use super::{Drawing, DrawingToolType};
5use egui::{Painter, Pos2, Rect, Ui};
6use std::collections::{BTreeMap, HashMap};
7use std::sync::Arc;
8
9pub trait DrawingRenderer: Send + Sync {
13 fn render(&self, drawing: &Drawing, painter: &Painter, rect: Rect, view: &ChartView);
15 fn hit_test(&self, drawing: &Drawing, point: Pos2, tolerance: f32) -> bool;
18 fn settings(&self, _ui: &mut Ui, _settings: &mut serde_json::Value) -> bool {
21 false
22 }
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct ToolAvailability {
29 pub visible: bool,
31 pub enabled: bool,
33}
34
35impl Default for ToolAvailability {
36 fn default() -> Self {
37 Self {
38 visible: true,
39 enabled: true,
40 }
41 }
42}
43
44#[derive(Clone)]
46pub struct RegisteredDrawingTool {
47 pub name: String,
49 pub anchors: usize,
51 pub implementation: Arc<dyn DrawingRenderer>,
53}
54
55#[derive(Clone, Default)]
59pub struct DrawingToolRegistry {
60 custom: BTreeMap<String, RegisteredDrawingTool>,
61 builtins: HashMap<DrawingToolType, ToolAvailability>,
62}
63
64impl DrawingToolRegistry {
65 pub fn register(
67 &mut self,
68 id: impl Into<String>,
69 tool: RegisteredDrawingTool,
70 ) -> Result<(), &'static str> {
71 let id = id.into();
72 if id.trim().is_empty() || tool.anchors == 0 {
73 return Err("A drawing tool needs an id and at least one anchor");
74 }
75 if self.custom.contains_key(&id) {
76 return Err("Drawing tool id is already registered");
77 }
78 self.custom.insert(id, tool);
79 Ok(())
80 }
81
82 pub fn get(&self, id: &str) -> Option<&RegisteredDrawingTool> {
84 self.custom.get(id)
85 }
86
87 pub fn custom_tools(&self) -> impl Iterator<Item = (&str, &RegisteredDrawingTool)> {
89 self.custom.iter().map(|(id, tool)| (id.as_str(), tool))
90 }
91
92 pub fn set_availability(&mut self, tool: DrawingToolType, availability: ToolAvailability) {
96 self.builtins.insert(tool, availability);
97 }
98
99 pub fn availability(&self, tool: DrawingToolType) -> ToolAvailability {
101 self.builtins.get(&tool).copied().unwrap_or_default()
102 }
103
104 pub fn builtin_tools(&self) -> impl Iterator<Item = (DrawingToolType, ToolAvailability)> + '_ {
106 super::categories::get_all_tools()
107 .into_iter()
108 .filter_map(|tool| {
109 let availability = self.availability(tool);
110 availability.visible.then_some((tool, availability))
111 })
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::drawings::{ChartPoint, DrawingManager};
119
120 struct Midpoint;
121 impl DrawingRenderer for Midpoint {
122 fn render(&self, drawing: &Drawing, painter: &Painter, _rect: Rect, _view: &ChartView) {
123 if let [a, b] = drawing.points.as_slice() {
124 painter.circle_filled(a.lerp(*b, 0.5), 10.0, egui::Color32::RED);
125 }
126 }
127 fn hit_test(&self, drawing: &Drawing, point: Pos2, tolerance: f32) -> bool {
128 match drawing.points.as_slice() {
129 [a, b] => a.lerp(*b, 0.5).distance(point) <= 10.0 + tolerance,
130 _ => false,
131 }
132 }
133 fn settings(&self, _ui: &mut Ui, settings: &mut serde_json::Value) -> bool {
134 *settings = serde_json::json!({"radius": 10});
135 true
136 }
137 }
138
139 #[test]
140 fn registered_tools_use_host_geometry_and_retain_settings_through_history_and_storage() {
141 let mut manager = DrawingManager::new();
142 manager
143 .registry
144 .register(
145 "midpoint",
146 RegisteredDrawingTool {
147 name: "Midpoint".into(),
148 anchors: 2,
149 implementation: Arc::new(Midpoint),
150 },
151 )
152 .unwrap();
153 manager.set_custom_tool("midpoint").unwrap();
154 manager.start_drawing_with_coords(
155 DrawingToolType::Polyline,
156 egui::pos2(100., 100.),
157 |x| x,
158 f64::from,
159 );
160 manager.add_point_with_coords(egui::pos2(200., 100.), |x| x, f64::from);
161 let id = manager.drawings[0].id;
162 assert!(manager.curr_drawing.is_none());
163 assert_eq!(manager.drawings[0].custom_tool.as_deref(), Some("midpoint"));
164 assert_eq!(manager.hit_test(egui::pos2(150., 100.)), Some(id));
165 assert_eq!(manager.hit_test(egui::pos2(110., 100.)), None);
166 let ctx = egui::Context::default();
167 let output = ctx.run_ui(egui::RawInput::default(), |ui| {
168 manager.render_all(
169 ui.painter(),
170 ui.max_rect(),
171 &ChartView {
172 price_formatter: None,
173 time_formatter: None,
174 bar_seconds: 0,
175 instrument: None,
176 bars: &[],
177 revision: 0,
178 time_axis: true,
179 executions: None,
180 timeframe: None,
181 finer: &[],
182 bar_to_x: &|x| x,
183 price_to_y: &|y| y as f32,
184 y_to_price: &f64::from,
185 },
186 );
187 assert!(manager.show_custom_settings(ui, id));
188 });
189 assert!(output.shapes.iter().any(|s| matches!(&s.shape, egui::Shape::Circle(c) if c.center == egui::pos2(150.,100.) && c.radius == 10.)));
190 output.drop_without_applying_deltas();
191 assert_eq!(manager.hit_test(egui::pos2(110., 100.)), None);
192 assert!(manager.undo());
193 assert_eq!(manager.drawings[0].custom_settings, serde_json::Value::Null);
194 assert!(manager.redo());
195 let stored = crate::drawings::persistence::drawing_to_stored(&manager.drawings[0], |i| {
196 i as i64 * 1000
197 });
198 let restored =
199 crate::drawings::persistence::stored_to_drawing(&stored, |t| t as f32 / 1000., 7);
200 assert_eq!(restored.custom_tool.as_deref(), Some("midpoint"));
201 assert_eq!(restored.custom_settings, serde_json::json!({"radius": 10}));
202 let mut chart = crate::ChartBuilder::new()
203 .with_drawing_registry(manager.registry.clone())
204 .build();
205 chart.set_data(
206 crate::widget::SeriesId::MAIN,
207 crate::widget::test_frames::chart(&[90.0], &[110.0])
208 .bars(crate::widget::SeriesId::MAIN)
209 .to_vec(),
210 );
211 let mut exported = Drawing::custom("midpoint");
212 exported.chart_points = vec![ChartPoint::new(60.0, 100.0), ChartPoint::new(80.0, 100.0)];
213 chart.drawings_mut().add_drawing(exported).unwrap();
214 let mut image = chart.export_image(&ctx, &Default::default()).unwrap();
215 image.textures_delta.clear();
216 assert!(
217 image.primitives.iter().any(|primitive| matches!(
218 &primitive.primitive, egui::epaint::Primitive::Mesh(mesh)
219 if mesh.vertices.iter().any(|vertex| vertex.color == egui::Color32::RED)
220 )),
221 "custom drawing missing from export"
222 );
223 chart.set_symbol("A");
224 chart.drawings_mut().set_custom_tool("midpoint").unwrap();
225 chart.set_symbol("B");
226 chart.drawings_mut().set_active_tool(None);
227 chart.set_symbol("A");
228 assert_eq!(chart.drawings().active_custom_tool(), None);
229 let mut invalid = Drawing::custom("midpoint");
230 invalid.chart_points = vec![ChartPoint::new(1., 1.)];
231 assert!(manager.add_drawing(invalid).is_err());
232 manager.registry.set_availability(
233 DrawingToolType::TrendLine,
234 ToolAvailability {
235 visible: false,
236 enabled: false,
237 },
238 );
239 assert!(
240 !manager
241 .registry
242 .builtin_tools()
243 .any(|(tool, _)| tool == DrawingToolType::TrendLine)
244 );
245 assert!(!manager.set_active_tool(Some(DrawingToolType::TrendLine)));
246 assert_eq!(manager.active_tool, None);
247 manager.start_drawing_with_coords(DrawingToolType::TrendLine, Pos2::ZERO, |x| x, f64::from);
248 assert!(manager.curr_drawing.is_none());
249 let mut disabled = Drawing::new(0, DrawingToolType::TrendLine);
250 disabled.chart_points = vec![ChartPoint::new(1., 1.), ChartPoint::new(2., 2.)];
251 assert!(manager.add_drawing(disabled).is_err());
252 }
253}