1use super::PaneId;
4use crate::Chart;
5use crate::drawings::{ChartPoint, Drawing};
6use chrono::{DateTime, Utc};
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum DrawingError {
12 UnknownDrawing(usize),
14 Locked(usize),
16 NonFinitePrice {
18 point: usize,
20 },
21 Invalid(&'static str),
24}
25
26impl std::fmt::Display for DrawingError {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 Self::UnknownDrawing(id) => write!(f, "Unknown drawing {id}"),
30 Self::Locked(id) => write!(f, "Drawing {id} is locked"),
31 Self::NonFinitePrice { point } => {
32 write!(f, "Drawing price of point {point} must be finite")
33 }
34 Self::Invalid(reason) => f.write_str(reason),
35 }
36 }
37}
38impl std::error::Error for DrawingError {}
39
40impl Chart {
41 pub fn create_drawing(
57 &mut self,
58 mut drawing: Drawing,
59 points: &[(DateTime<Utc>, f64)],
60 ) -> Result<usize, DrawingError> {
61 drawing.chart_points = self.map_drawing_points(points)?;
62 self.drawings_mut()
63 .add_drawing(drawing)
64 .map_err(DrawingError::Invalid)
65 }
66
67 pub fn drawing_points(&self, id: usize) -> Option<Vec<(DateTime<Utc>, f64)>> {
70 self.drawings()
71 .drawings
72 .iter()
73 .find(|d| d.id == id)?
74 .chart_points
75 .iter()
76 .map(|p| Some((self.drawing_point_time(p)?, p.price)))
77 .collect()
78 }
79
80 pub fn set_drawing_points(
83 &mut self,
84 id: usize,
85 points: &[(DateTime<Utc>, f64)],
86 ) -> Result<(), DrawingError> {
87 let mut drawing = self
88 .drawings()
89 .drawings
90 .iter()
91 .find(|d| d.id == id)
92 .ok_or(DrawingError::UnknownDrawing(id))?
93 .clone();
94 if drawing.locked {
95 return Err(DrawingError::Locked(id));
96 }
97 drawing.chart_points = self.map_drawing_points(points)?;
98 self.drawings()
99 .validate_drawing(&drawing)
100 .map_err(DrawingError::Invalid)?;
101 self.drawings_mut().edit_drawing(id, |target| {
102 target.chart_points = drawing.chart_points;
103 });
104 Ok(())
105 }
106
107 pub fn set_drawing_image(
113 &mut self,
114 id: usize,
115 uri: impl Into<String>,
116 size: egui::Vec2,
117 ) -> Result<(), &'static str> {
118 let drawing = self
119 .drawings()
120 .drawings
121 .iter()
122 .find(|d| d.id == id)
123 .ok_or("Unknown drawing")?;
124 if drawing.locked {
125 return Err("Drawing is locked");
126 }
127 if drawing.tool_type != crate::drawings::DrawingToolType::Image {
128 return Err("Drawing is not an image");
129 }
130 let uri = uri.into();
131 if uri.trim().is_empty() {
132 return Err("Image address is empty");
133 }
134 if !(size.x > 0.0 && size.y > 0.0 && size.is_finite()) {
135 return Err("Image size must be positive and finite");
136 }
137 let sizing = &crate::tokens::DESIGN_TOKENS.sizing.media;
138 let limit = egui::vec2(
139 2.0 * sizing.image_default_width,
140 2.0 * sizing.image_default_height,
141 );
142 let scale = 1.0_f32.min(limit.x / size.x).min(limit.y / size.y);
143 let frame = (size * scale).max(egui::Vec2::splat(1.0)).min(limit);
144 self.user_action(|chart| {
145 let manager = chart.drawings_mut();
146 if manager
147 .text_editor
148 .as_ref()
149 .is_some_and(|editor| editor.id == id)
150 {
151 manager.text_editor = None;
152 }
153 manager.edit_drawing(id, |drawing| {
154 drawing.text = Some(uri);
155 drawing.image.get_or_insert_with(Default::default).size = Some(frame.into());
156 });
157 });
158 Ok(())
159 }
160
161 pub fn drawing_pane(&self, id: usize) -> Option<PaneId> {
163 let drawing = self.drawings().drawings.iter().find(|d| d.id == id)?;
164 Some(
165 self.pane_layout
166 .panes
167 .iter()
168 .find(|pane| {
169 drawing.pane == Some(pane.id.0)
170 || pane.members.iter().any(|id| drawing.pane == Some(id.0))
171 })
172 .map_or(PaneId::MAIN, |pane| pane.id),
173 )
174 }
175
176 pub fn move_drawing_to_pane(&mut self, id: usize, pane: PaneId) -> bool {
180 let owner = if pane == PaneId::MAIN {
181 None
182 } else if let Some(pane) = self
183 .pane_layout
184 .panes
185 .iter()
186 .find(|item| item.id == pane)
187 .filter(|_| self.panes().contains(&pane))
188 {
189 Some(pane.members.first().map_or(pane.id.0, |id| id.0))
191 } else {
192 return false;
193 };
194 let drawings = self.drawings_mut();
195 if !drawings.drawings.iter().any(|d| d.id == id && !d.locked) {
196 return false;
197 }
198 drawings.edit_drawing(id, |d| d.pane = owner);
199 true
200 }
201
202 #[cfg(feature = "ui")]
206 pub(crate) fn draw_horizontal_line_at(&mut self, pos: egui::Pos2, price: f64) -> Option<usize> {
207 if !price.is_finite() {
208 return None;
209 }
210 let tool = crate::drawings::DrawingToolType::HorizontalLine;
211 let mut drawing = self.drawings.new_drawing(0, tool);
212 drawing.pane = self
214 .pane_layout
215 .panes
216 .iter()
217 .find(|pane| pane.id != PaneId::MAIN && pane.rect.contains(pos))
218 .map(|pane| pane.members.first().map_or(pane.id.0, |id| id.0));
219 drawing.z_order = self.drawings.top_z_order();
220 let bar = self.get_chart_mapping().x_to_idx_f64(pos.x).round() as f32;
221 drawing.chart_points = vec![ChartPoint::new(bar, price)];
222 let id = self.drawings.add_drawing(drawing).ok()?;
223 self.drawings.select(id);
224 Some(id)
225 }
226
227 #[cfg(feature = "ui")]
229 pub(crate) fn reveal_drawing(&mut self, id: usize) {
230 let Some(drawing) = self.drawings.drawings.iter().find(|d| d.id == id) else {
231 return;
232 };
233 if !drawing.selectable || drawing.screen_anchor.is_some() || self.state.shown().is_empty() {
234 return;
235 }
236 let pane = self.pane_layout.panes.iter().position(|pane| {
237 drawing.pane == Some(pane.id.0)
238 || pane.members.iter().any(|id| drawing.pane == Some(id.0))
239 });
240 let price = match pane {
241 Some(at) => &self.pane_layout.panes[at].scale,
242 None => self.state.price_scale(),
243 };
244 let transform = price.transform();
245 let bounds = drawing
246 .chart_points
247 .iter()
248 .filter_map(|point| {
249 let price = transform.to_scale(point.price);
250 (point.bar_idx.is_finite() && price.is_finite()).then_some((point.bar_idx, price))
251 })
252 .fold(None, |bounds, (bar, price)| {
253 Some(match bounds {
254 None => (bar, bar, price, price),
255 Some((left, right, low, high)) => (
256 bar.min(left),
257 bar.max(right),
258 price.min(low),
259 price.max(high),
260 ),
261 })
262 });
263 let Some((left, right, low, high)) = bounds else {
264 return;
265 };
266 let range = self.state.time_scale().visible_logical_range();
267 let padding = ((range.right - range.left) * 0.05).clamp(0.0, 2.0);
268 let shift = if right < range.left {
269 right - range.left - padding
270 } else if left > range.right {
271 left - range.right + padding
272 } else {
273 0.0
274 };
275 if shift != 0.0 {
276 self.cancel_view_motion();
277 self.tracking_mode_active = false;
278 let scale = self.state.time_scale_mut();
279 scale.set_right_offset(scale.right_offset_precise() + f64::from(shift));
280 }
281 let scale = match pane {
282 Some(at) => &mut self.pane_layout.panes[at].scale,
283 None => self.state.price_scale_mut(),
284 };
285 if scale.is_manual() {
286 let range = scale.range();
287 let padding = (range.max - range.min) * 0.05;
288 let shift = if high < range.min {
289 high - range.min - padding
290 } else if low > range.max {
291 low - range.max + padding
292 } else {
293 0.0
294 };
295 if shift != 0.0 && (range.min + shift).is_finite() && (range.max + shift).is_finite() {
296 scale.set_range(range.min + shift, range.max + shift);
297 }
298 }
299 }
300
301 fn map_drawing_points(
302 &self,
303 points: &[(DateTime<Utc>, f64)],
304 ) -> Result<Vec<ChartPoint>, DrawingError> {
305 points
306 .iter()
307 .enumerate()
308 .map(|(point, &(time, price))| {
309 if !price.is_finite() {
310 return Err(DrawingError::NonFinitePrice { point });
311 }
312 Ok(self.drawing_point_at(time, price).unwrap_or_else(|| {
313 let mut point = ChartPoint::new(0.0, price);
314 point.time_anchor = Some((time, 0.0));
315 point
316 }))
317 })
318 .collect()
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::drawings::DrawingToolType;
326 use crate::drawings::manager::DrawingSyncAction;
327 use crate::model::{Bar, BarData};
328 use crate::studies::{CustomIndicator, IndicatorValue};
329
330 #[test]
333 fn drawings_move_between_panes_with_their_anchors() {
334 let bars = (0..20)
335 .map(|i| {
336 let time = DateTime::from_timestamp(i * 60, 0).unwrap();
337 Bar::new(time, 10., 12., 8., 10. + i as f64 % 3., 1.)
338 })
339 .collect();
340 let mut chart = Chart::new(BarData::from_bars(bars));
341 let study = chart.add_indicator(Box::new(
342 CustomIndicator::new("Pane", |bars| {
343 vec![IndicatorValue::Single(50.0); bars.len()]
344 })
345 .with_overlay(false),
346 ));
347 let pane = chart.series_pane(study).unwrap();
348 let mut line = Drawing::new(0, DrawingToolType::TrendLine);
349 line.color = [1, 2, 3, 255];
350 line.chart_points = vec![ChartPoint::new(2.0, 30.0), ChartPoint::new(9.0, 70.0)];
351 let id = chart.drawings_mut().add_drawing(line).unwrap();
352 let before = chart.drawings().drawings[0].clone();
353 assert_eq!(chart.drawing_pane(id), Some(PaneId::MAIN));
354
355 assert!(chart.user_action(|chart| chart.move_drawing_to_pane(id, pane)));
356 let moved = &chart.drawings().drawings[0];
357 assert_eq!(chart.drawing_pane(id), Some(pane));
358 assert_eq!(moved.pane, Some(study.0));
359 assert_eq!(
360 (&moved.chart_points, moved.color),
361 (&before.chart_points, before.color)
362 );
363 assert!(chart.undo());
364 assert_eq!(chart.drawing_pane(id), Some(PaneId::MAIN));
365 assert!(chart.redo());
366 assert!(chart.move_drawing_to_pane(id, PaneId::MAIN));
367 assert_eq!(chart.drawings().drawings[0].pane, None);
368
369 assert!(!chart.move_drawing_to_pane(id, PaneId(999)));
370 assert!(!chart.move_drawing_to_pane(id + 1, pane));
371 chart.drawings_mut().edit_drawing(id, |d| d.locked = true);
372 assert!(!chart.move_drawing_to_pane(id, pane));
373 assert_eq!(chart.drawing_pane(id), Some(PaneId::MAIN));
374 }
375
376 #[test]
377 fn timestamp_creation_validates_before_mutation_and_round_trips_edits() {
378 let time = |seconds| DateTime::from_timestamp(seconds, 0).unwrap();
379 let bars = [0, 60, 180]
380 .map(|seconds| Bar::new(time(seconds), 10., 12., 8., 10., 1.))
381 .to_vec();
382 let mut chart = Chart::new(BarData::from_bars(bars));
383 let points = [(time(-30), 9.), (time(120), 11.)];
384 let id = chart
385 .user_action(|chart| {
386 chart.create_drawing(Drawing::new(99, DrawingToolType::TrendLine), &points)
387 })
388 .unwrap();
389 let drawing = &chart.drawings().drawings[0];
390 assert_eq!(
392 drawing
393 .chart_points
394 .iter()
395 .map(|p| p.bar_idx)
396 .collect::<Vec<_>>(),
397 [-0.5, 1.5]
398 );
399 assert_eq!(chart.drawing_points(id).unwrap(), points);
400 assert!(chart.undo());
401 assert!(chart.drawings().drawings.is_empty());
402 assert!(chart.redo());
403 chart.drawings().pending_sync.lock().unwrap().clear();
404 for invalid in [
405 vec![],
406 vec![points[0]],
407 vec![points[0]; 3],
408 vec![(time(0), f64::NAN); 2],
409 ] {
410 assert!(
411 chart
412 .create_drawing(Drawing::new(0, DrawingToolType::TrendLine), &invalid)
413 .is_err()
414 );
415 assert!(chart.set_drawing_points(id, &invalid).is_err());
416 assert_eq!(chart.drawing_points(id).unwrap(), points);
417 }
418 assert!(chart.drawings().pending_sync.lock().unwrap().is_empty());
419 let edited = [(time(30), 10.), (time(360), 12.)];
420 chart.set_drawing_points(id, &edited).unwrap();
421 let json = chart.save_drawings().unwrap().to_json().unwrap();
422 let mut empty = Chart::new(crate::model::BarData::default());
423 empty.load_drawings(&json).unwrap();
424 let restored_id = empty.drawings().drawings[0].id;
425 assert_eq!(empty.drawing_points(restored_id).unwrap(), edited);
426 chart.drawings_mut().edit_drawing(id, |d| {
427 d.saveable = false;
428 d.listed = false;
429 });
430 assert!(chart.save_drawings().unwrap().drawings().is_empty());
431 assert!(chart.drawings().listed_drawings().next().is_none());
432 assert!(matches!(
433 chart.drawings().pending_sync.lock().unwrap().last(),
434 Some(DrawingSyncAction::Delete(_))
435 ));
436 assert!(chart.drawings_mut().undo());
437 assert_eq!(chart.save_drawings().unwrap().drawings().len(), 1);
438 assert!(matches!(
439 chart.drawings().pending_sync.lock().unwrap().last(),
440 Some(DrawingSyncAction::SaveDrawing(_))
441 ));
442 let mut volatile = Drawing::new(0, DrawingToolType::HorizontalLine);
443 volatile.saveable = false;
444 chart.drawings().pending_sync.lock().unwrap().clear();
445 let volatile = chart.create_drawing(volatile, &points[..1]).unwrap();
446 chart.drawings_mut().delete_drawing(volatile);
447 assert!(chart.drawings().pending_sync.lock().unwrap().is_empty());
448 }
449}