1use super::{DrawingManager, DrawingSyncAction};
4use crate::drawings::domain::{
5 ChartPoint, Drawing, DrawingInteractionMode, DrawingToolType, HandlePos,
6};
7use crate::drawings::services::HandleService;
8use crate::model::PostLink;
9use egui::Pos2;
10
11impl DrawingManager {
12 pub fn get_handles(&self, drawing: &Drawing) -> Vec<(HandlePos, Pos2)> {
17 HandleService::get_handles(&self.handle_drawing(drawing))
18 }
19
20 pub fn hit_test_handle(&self, drawing: &Drawing, point: Pos2) -> Option<HandlePos> {
24 if !drawing.selectable
25 || drawing.locked
26 || !drawing.is_visible_on_timeframe(&self.curr_timeframe)
27 || (!self.pane_clips.is_empty()
28 && !self
29 .pane_clips
30 .get(&drawing.pane)
31 .is_some_and(|rect| rect.contains(point)))
32 {
33 return None;
34 }
35 HandleService::hit_test_handle(
36 &self.handle_drawing(drawing),
37 point,
38 self.options.handle_config.size,
39 )
40 }
41
42 pub fn start_drag_handle(&mut self, drawing_id: usize, handle: HandlePos) {
45 if let Some(drawing) = self.drawings.iter().find(|d| {
46 d.id == drawing_id
47 && d.selectable
48 && !d.locked
49 && d.is_visible_on_timeframe(&self.curr_timeframe)
50 }) {
51 self.dragging_handle = Some((drawing_id, handle));
52 self.drag_old_state = Some(drawing.clone());
53 self.drag_origin = (handle == HandlePos::Middle)
54 .then(|| {
55 HandleService::get_handles(drawing)
56 .into_iter()
57 .find(|(h, _)| *h == handle)
58 .map(|(_, p)| p)
59 })
60 .flatten();
61 }
62 }
63
64 pub fn start_drag_body(&mut self, drawing_id: usize, origin: Pos2) {
68 self.start_drag_handle(drawing_id, HandlePos::Middle);
69 if self.dragging_handle == Some((drawing_id, HandlePos::Middle)) {
70 self.drag_origin = Some(origin);
71 self.drag_group = self
72 .drawings
73 .iter()
74 .filter(|d| {
75 d.id != drawing_id
76 && self.pane_members.contains(&d.pane)
77 && d.selectable
78 && self.selection.is_selected(d.id)
79 && !d.locked
80 && d.is_visible_on_timeframe(&self.curr_timeframe)
81 })
82 .cloned()
83 .collect();
84 }
85 }
86
87 pub(crate) fn copy_dragged_drawings(&mut self) {
88 let Some((_, handle)) = self.dragging_handle else {
89 return;
90 };
91 let Some(old) = self.drag_old_state.take() else {
92 return;
93 };
94 let primary_id = old.id;
95 let mut copies = std::mem::take(&mut self.drag_group);
96 copies.push(old);
97 copies.sort_by_key(|copy| {
98 (
99 copy.z_order,
100 self.drawings.iter().position(|d| d.id == copy.id),
101 )
102 });
103 if copies
105 .iter()
106 .any(|copy| self.validate_created(copy).is_err())
107 {
108 self.dragging_handle = None;
109 self.drag_origin = None;
110 return;
111 }
112 let primary = copies.iter().position(|d| d.id == primary_id).unwrap();
113 for copy in &mut copies {
114 copy.id = self.take_id();
115 copy.stored_id = None;
116 copy.created_at = None;
117 copy.z_order = self.top_z_order();
118 self.drawings.push(copy.clone());
119 }
120 let old = copies.remove(primary);
121 self.dragging_handle = Some((old.id, handle));
122 self.drag_old_state = Some(old.clone());
123 self.drag_group = copies;
124 self.select_drawings(
125 self.drag_group
126 .iter()
127 .map(|d| d.id)
128 .chain([old.id])
129 .collect::<Vec<_>>(),
130 );
131 self.drag_copy = true;
132 }
133
134 pub(crate) fn constrains_angle(tool: DrawingToolType) -> bool {
135 use DrawingToolType as T;
136 matches!(
137 tool,
138 T::TrendLine
139 | T::Ray
140 | T::ExtendedLine
141 | T::InfoLine
142 | T::TrendAngle
143 | T::Arrow
144 | T::ParallelChannel
145 | T::DisjointChannel
146 | T::FlatTopBottom
147 | T::RegressionTrend
148 | T::FibonacciChannel
149 )
150 }
151
152 fn constrain_angle(&self, drawing: &Drawing, index: usize, point: Pos2) -> Pos2 {
154 if !self.interaction_modifiers.shift
155 || drawing.custom_tool.is_some()
156 || !Self::constrains_angle(drawing.tool_type)
157 {
158 return point;
159 }
160 let anchor_index = if index == 0 { 1 } else { index - 1 };
161 let Some(&anchor) = drawing.points.get(anchor_index) else {
162 return point;
163 };
164 let delta = point - anchor;
165 let angle =
166 (delta.angle() / std::f32::consts::FRAC_PI_4).round() * std::f32::consts::FRAC_PI_4;
167 let direction = egui::vec2(angle.cos().round(), angle.sin().round());
168 let mut offset = direction * (delta.dot(direction) / direction.length_sq());
169 if self.pane_rect.is_positive() {
171 let mut first = 0.0_f32;
172 let mut last = f32::INFINITY;
173 for (start, step, min, max) in [
174 (
175 anchor.x,
176 direction.x,
177 self.pane_rect.left(),
178 self.pane_rect.right(),
179 ),
180 (
181 anchor.y,
182 direction.y,
183 self.pane_rect.top(),
184 self.pane_rect.bottom(),
185 ),
186 ] {
187 if step == 0.0 {
188 if start < min || start > max {
189 last = f32::NEG_INFINITY;
190 }
191 } else {
192 let a = (min - start) / step;
193 let b = (max - start) / step;
194 first = first.max(a.min(b));
195 last = last.min(a.max(b));
196 }
197 }
198 if first <= last {
199 let distance = delta.dot(direction) / direction.length_sq();
200 offset = direction * distance.clamp(first, last);
201 }
202 }
203 anchor + offset
204 }
205
206 fn constrain_preview(&self, point: Pos2) -> Pos2 {
207 self.curr_drawing.as_ref().map_or(point, |drawing| {
208 let index = drawing
209 .points
210 .len()
211 .saturating_sub(usize::from(self.preview_point));
212 self.constrain_angle(drawing, index, point)
213 })
214 }
215
216 pub fn update_drag_handle<F, G, H>(
221 &mut self,
222 new_pos: Pos2,
223 x_to_bar: F,
224 y_to_price: G,
225 translate_price: H,
226 ) where
227 F: Fn(f32) -> f32,
228 G: Fn(f32) -> f64,
229 H: Fn(f64, f32, f32) -> f64,
230 {
231 if let Some((id, handle)) = self.dragging_handle {
232 if let Some(drawing) = self.drawings.iter().find(|d| d.id == id)
234 && drawing.tool_type == DrawingToolType::RegressionTrend
235 && self
236 .get_handles(drawing)
237 .into_iter()
238 .any(|(h, point)| h == handle && x_to_bar(new_pos.x) == x_to_bar(point.x))
239 {
240 return;
241 }
242 let mut snapped = self.snap_point(new_pos, &self.drawings);
244 let mut constrained_price = None;
245 if self.interaction_modifiers.shift
246 && let Some(origin) = self.drag_origin
247 {
248 let delta = snapped - origin;
249 if delta.x.abs() >= delta.y.abs() {
250 snapped.y = origin.y;
251 } else {
252 snapped.x = origin.x;
253 }
254 } else if let Some(drawing) = self.drawings.iter().find(|d| d.id == id) {
255 let index = match handle {
256 HandlePos::Start => Some(0),
257 HandlePos::End => Some(1),
258 HandlePos::Point(index) => Some(index),
259 _ => None,
260 };
261 if let Some(index) = index {
262 snapped = self.constrain_angle(drawing, index, snapped);
263 let anchor_index = if index == 0 { 1 } else { index - 1 };
264 if self.interaction_modifiers.shift
265 && Self::constrains_angle(drawing.tool_type)
266 && drawing.custom_tool.is_none()
267 && let Some((anchor, chart_anchor)) = drawing
268 .points
269 .get(anchor_index)
270 .zip(drawing.chart_points.get(anchor_index))
271 {
272 constrained_price = Some((chart_anchor.price, anchor.y));
274 }
275 }
276 }
277 let whole_bars = self.interaction_modifiers.shift && constrained_price.is_none();
279 let x_to_bar = |x| {
280 let bar = x_to_bar(x);
281 if whole_bars { bar.round() } else { bar }
282 };
283 if let Some(origin) = self.drag_origin {
284 let bars = x_to_bar(snapped.x) - x_to_bar(origin.x);
285 for old in &self.drag_group {
286 if let Some(drawing) = self
287 .drawings
288 .iter_mut()
289 .find(|d| d.id == old.id && !d.locked)
290 {
291 let first = old.first_moved_anchor();
292 for (point, before) in drawing
293 .chart_points
294 .iter_mut()
295 .zip(&old.chart_points)
296 .skip(first)
297 {
298 *point = ChartPoint::new(
299 before.bar_idx + bars,
300 translate_price(before.price, origin.y, snapped.y),
301 );
302 }
303 for (point, before) in
304 drawing.points.iter_mut().zip(&old.points).skip(first)
305 {
306 *point = *before + (snapped - origin);
307 }
308 if let Some([x, y]) = old.screen_anchor {
309 drawing.screen_anchor = Some([
310 x + (snapped.x - origin.x) / self.pane_rect.width(),
311 y + (snapped.y - origin.y) / self.pane_rect.height(),
312 ]);
313 }
314 }
315 }
316 }
317 if let Some(drawing) = self.drawings.iter_mut().find(|d| d.id == id && !d.locked) {
318 if let (Some(origin), Some(old)) = (self.drag_origin, &self.drag_old_state) {
319 let bars = x_to_bar(snapped.x) - x_to_bar(origin.x);
320 let skip = usize::from(
323 self.drag_group.is_empty()
324 && !self.drag_copy
325 && old.points.len() > 1
326 && matches!(
327 drawing.tool_type,
328 DrawingToolType::Callout
329 | DrawingToolType::Note
330 | DrawingToolType::PriceNote
331 ),
332 );
333 let skip = skip.max(old.first_moved_anchor());
334 for (point, before) in drawing
335 .chart_points
336 .iter_mut()
337 .zip(&old.chart_points)
338 .skip(skip)
339 {
340 *point = ChartPoint::new(
341 before.bar_idx + bars,
342 translate_price(before.price, origin.y, snapped.y),
343 );
344 }
345 for (point, before) in drawing.points.iter_mut().zip(&old.points).skip(skip) {
346 *point = *before + (snapped - origin);
347 }
348 } else {
349 HandleService::update_handle(
350 drawing,
351 handle,
352 snapped,
353 x_to_bar,
354 |y| {
355 constrained_price.map_or_else(
356 || y_to_price(y),
357 |(price, from)| translate_price(price, from, y),
358 )
359 },
360 &translate_price,
361 );
362 }
363 if drawing.screen_anchor.is_some()
364 && let Some(pos) = drawing.points.first()
365 {
366 drawing.screen_anchor = Some([
367 (pos.x - self.pane_rect.left()) / self.pane_rect.width(),
368 (pos.y - self.pane_rect.top()) / self.pane_rect.height(),
369 ]);
370 }
371 }
372 }
373 }
374
375 pub(crate) fn anchor_pins_to_bars(
379 &mut self,
380 x_to_bar: impl Fn(f32) -> f32,
381 y_to_price: impl Fn(f32) -> f64,
382 ) {
383 let pane = self.pane_rect;
384 if !pane.is_finite() || pane.width() <= 0.0 || pane.height() <= 0.0 {
385 return;
386 }
387 let mut converted = Vec::new();
388 for drawing in self.drawings.iter_mut().chain(&mut self.curr_drawing) {
389 let Some([x, y]) = drawing.screen_anchor else {
390 continue;
391 };
392 if drawing.tool_type != DrawingToolType::AnchoredNote
393 || !self.pane_members.contains(&drawing.pane)
394 {
395 continue;
396 }
397 let pos = pane.min + egui::vec2(x * pane.width(), y * pane.height());
398 let anchor = ChartPoint::new(x_to_bar(pos.x), y_to_price(pos.y));
399 if let (Some(point), Some(chart_point), true) = (
400 drawing.points.first_mut(),
401 drawing.chart_points.first_mut(),
402 anchor.bar_idx.is_finite() && anchor.price.is_finite(),
403 ) {
404 *point = pos;
405 *chart_point = anchor;
406 drawing.screen_anchor = None;
407 converted.push(drawing.id);
408 }
409 }
410 for id in converted {
411 self.queue_update(id);
412 }
413 }
414
415 pub fn end_drag_handle(&mut self) {
419 if let Some(old_state) = self.drag_old_state.take() {
420 if std::mem::take(&mut self.drag_copy) {
421 let mut ids: Vec<_> = std::mem::take(&mut self.drag_group)
422 .iter()
423 .map(|d| d.id)
424 .collect();
425 ids.push(old_state.id);
426 ids.sort_by_key(|id| self.drawings.iter().position(|d| d.id == *id));
427 for &id in &ids {
428 self.queue(DrawingSyncAction::SaveDrawing(id));
429 }
430 self.history
431 .push_command(crate::drawings::services::DrawingCommand::Snapshot {
432 drawings: Vec::new(),
433 ids,
434 });
435 self.dragging_handle = None;
436 self.drag_origin = None;
437 return;
438 }
439 self.queue_edit(&old_state);
440 if self.drag_group.is_empty() {
441 self.history.push_modify(old_state.id, old_state);
442 } else {
443 let mut before = std::mem::take(&mut self.drag_group);
444 for drawing in &before {
445 self.queue_update(drawing.id);
446 }
447 before.push(old_state);
448 let snapshot = self
449 .drawings
450 .iter()
451 .enumerate()
452 .filter_map(|(index, drawing)| {
453 let old = before.iter().find(|old| old.id == drawing.id)?;
454 Some((index, old.clone()))
455 })
456 .collect();
457 self.history.push_snapshot(snapshot);
458 }
459 }
460 self.dragging_handle = None;
461 self.drag_origin = None;
462 }
463
464 pub fn set_active_tool(&mut self, tool: Option<DrawingToolType>) -> bool {
472 self.active_custom_tool = None;
473 let set = tool.filter(|tool| self.registry.availability(*tool).enabled);
474 self.active_tool = set;
475 self.cancel_curr_drawing();
476 if set.is_some() {
477 self.eraser_mode = false;
478 }
479 set == tool
480 }
481
482 pub fn active_custom_tool(&self) -> Option<&str> {
484 self.active_custom_tool.as_deref()
485 }
486
487 pub fn active_tool(&self) -> Option<DrawingToolType> {
492 self.active_tool
493 }
494
495 pub fn is_eraser_mode(&self) -> bool {
498 self.eraser_mode
499 }
500
501 pub fn curr_drawing(&self) -> Option<&Drawing> {
504 self.curr_drawing.as_ref()
505 }
506
507 pub fn curr_drawing_mut(&mut self) -> Option<&mut Drawing> {
510 self.curr_drawing.as_mut()
511 }
512
513 pub fn dragging_handle(&self) -> Option<(usize, HandlePos)> {
516 self.dragging_handle
517 }
518
519 pub fn set_custom_tool(&mut self, id: &str) -> Result<(), &'static str> {
522 if self.registry.get(id).is_none() {
523 return Err("Unknown drawing tool");
524 }
525 self.cancel_curr_drawing();
526 self.eraser_mode = false;
527 self.active_tool = Some(DrawingToolType::Polyline);
528 self.active_custom_tool = Some(id.into());
529 Ok(())
530 }
531
532 pub fn show_custom_settings(&mut self, ui: &mut egui::Ui, id: usize) -> bool {
535 let Some(drawing) = self.drawings.iter().find(|d| d.id == id && !d.locked) else {
536 return false;
537 };
538 let Some(tool) = drawing
539 .custom_tool
540 .as_deref()
541 .and_then(|id| self.registry.get(id))
542 else {
543 return false;
544 };
545 let mut settings = drawing.custom_settings.clone();
546 if !tool.implementation.settings(ui, &mut settings) || settings == drawing.custom_settings {
547 return false;
548 }
549 self.edit_drawing(id, |drawing| drawing.custom_settings = settings);
550 true
551 }
552
553 pub fn set_eraser_mode(&mut self, on: bool) {
558 self.eraser_mode = on;
559 if on {
560 self.active_custom_tool = None;
561 self.active_tool = None;
562 self.cancel_curr_drawing();
563 }
564 }
565
566 pub fn start_text_annotation(&mut self) {
571 self.set_active_tool(Some(DrawingToolType::Note));
572 }
573
574 pub fn start_icon_insertion(&mut self, icon_name: String) {
579 let tool = if icon_name.starts_with("sticker:") {
580 DrawingToolType::Sticker
581 } else {
582 DrawingToolType::FontIcon
583 };
584 if !self.registry.availability(tool).enabled {
585 return;
586 }
587 self.set_active_tool(Some(tool));
588
589 let id = self.take_id();
591 let mut drawing = self.new_drawing(id, tool);
592 drawing.text = Some(icon_name);
593 drawing.completed = false; self.curr_drawing = Some(drawing);
597 }
598
599 pub fn start_post_insertion(&mut self, tool: DrawingToolType, post: PostLink) {
604 if !matches!(tool, DrawingToolType::Tweet | DrawingToolType::Idea)
605 || !self.registry.availability(tool).enabled
606 {
607 return;
608 }
609 self.set_active_tool(Some(tool));
610 let id = self.take_id();
611 let mut drawing = self.new_drawing(id, tool);
612 drawing.post = Some(post);
613 drawing.completed = false;
614 self.curr_drawing = Some(drawing);
615 }
616
617 fn snap_placement(&self, tool: Option<DrawingToolType>, point: Pos2) -> Pos2 {
621 if tool
622 .is_some_and(|tool| tool.interaction_mode() == DrawingInteractionMode::ContinuousDraw)
623 {
624 point
625 } else {
626 self.snap_point(point, &self.drawings)
627 }
628 }
629
630 pub fn start_drawing_with_coords<F, G>(
640 &mut self,
641 tool_type: DrawingToolType,
642 point: Pos2,
643 x_to_bar: F,
644 y_to_price: G,
645 ) where
646 F: Fn(f32) -> f32,
647 G: Fn(f32) -> f64,
648 {
649 if tool_type.is_cursor()
650 || (self.active_custom_tool.is_none() && !self.registry.availability(tool_type).enabled)
651 {
652 return;
653 }
654 let point = self.snap_placement(Some(tool_type), point);
655 let mut drawing = match self.curr_drawing.take() {
657 Some(prepared) if prepared.tool_type == tool_type && prepared.points.is_empty() => {
658 prepared
659 }
660 _ => {
661 let id = self.take_id();
662 self.new_drawing(id, tool_type)
663 }
664 };
665 if let Some(id) = &self.active_custom_tool {
666 drawing.custom_tool = Some(id.clone());
667 }
668 drawing.pane = self.pane;
669 drawing.z_order = self.top_z_order();
670 self.preview_point = false;
671 drawing.add_point_with_chart_coords(point, &x_to_bar, &y_to_price);
672 if matches!(
673 tool_type,
674 DrawingToolType::LongPos | DrawingToolType::ShortPos
675 ) {
676 let side = if tool_type == DrawingToolType::LongPos {
677 -1.0
678 } else {
679 1.0
680 };
681 let target = point + egui::vec2(100.0, side * 50.0);
682 let target_price = y_to_price(target.y);
683 let entry_price = drawing.chart_points[0].price;
684 let stop = Pos2::new(target.x, point.y - side * 50.0);
685 drawing.points.extend([target, stop]);
686 drawing.chart_points.extend([
687 ChartPoint::new(x_to_bar(target.x), target_price),
688 ChartPoint::new(x_to_bar(target.x), 2.0 * entry_price - target_price),
689 ]);
690 }
691
692 if let Some(tool) = drawing
693 .custom_tool
694 .as_deref()
695 .and_then(|id| self.registry.get(id))
696 {
697 drawing.completed = drawing.chart_points.len() == tool.anchors;
698 }
699 let completed = drawing.completed;
700 self.curr_drawing = Some(drawing);
701 if completed {
702 self.finish_curr_drawing();
703 }
704 }
705
706 pub fn add_point_with_coords<F, G>(&mut self, point: Pos2, x_to_bar: F, y_to_price: G)
712 where
713 F: Fn(f32) -> f32,
714 G: Fn(f32) -> f64,
715 {
716 let point = self.constrain_preview(
717 self.snap_placement(self.curr_drawing.as_ref().map(|d| d.tool_type), point),
718 );
719 if let Some(ref mut drawing) = self.curr_drawing {
720 if std::mem::take(&mut self.preview_point) {
722 drawing.points.pop();
723 drawing.chart_points.pop();
724 }
725 drawing.add_point_with_chart_coords(point, &x_to_bar, &y_to_price);
726 drawing.align_second_anchor();
727 if drawing.completed {
728 drawing.store_default_curve_controls(&x_to_bar, &y_to_price);
729 }
730 if let Some(tool) = drawing
731 .custom_tool
732 .as_deref()
733 .and_then(|id| self.registry.get(id))
734 {
735 drawing.completed = drawing.chart_points.len() == tool.anchors;
736 }
737 if drawing.completed {
738 self.finish_curr_drawing();
739 }
740 }
741 }
742
743 pub fn update_last_point_with_coords<F, G>(&mut self, point: Pos2, x_to_bar: F, y_to_price: G)
747 where
748 F: Fn(f32) -> f32,
749 G: Fn(f32) -> f64,
750 {
751 let point = self.constrain_preview(
752 self.snap_placement(self.curr_drawing.as_ref().map(|d| d.tool_type), point),
753 );
754 if let Some(ref mut drawing) = self.curr_drawing
755 && !drawing.points.is_empty()
756 && !drawing.completed
757 {
758 if self.preview_point {
759 drawing.update_last_point_with_chart_coords(point, x_to_bar, y_to_price);
760 } else {
761 drawing.add_point_with_chart_coords(point, &x_to_bar, &y_to_price);
762 drawing.completed = false;
763 self.preview_point = true;
764 }
765 drawing.align_second_anchor();
766 }
767 }
768
769 pub fn complete_curr_drawing(&mut self) {
774 if let Some(drawing) = &mut self.curr_drawing
775 && std::mem::take(&mut self.preview_point)
776 {
777 drawing.points.pop();
778 drawing.chart_points.pop();
779 }
780 if let Some(ref mut drawing) = self.curr_drawing
781 && drawing.custom_tool.as_deref().map_or_else(
782 || drawing.points.len() >= drawing.tool_type.anchors_needed().max(2),
783 |id| {
784 self.registry
785 .get(id)
786 .is_some_and(|tool| drawing.points.len() == tool.anchors)
787 },
788 )
789 {
790 drawing.completed = true;
791 self.finish_curr_drawing();
792 }
793 }
794
795 pub(crate) fn close_curr_polyline(&mut self, point: Pos2) -> bool {
798 let reach = self.options.handle_config.size * 1.5;
799 let preview = usize::from(self.preview_point);
800 let Some(drawing) = self.curr_drawing.as_mut().filter(|d| {
801 d.tool_type == DrawingToolType::Polyline
802 && d.custom_tool.is_none()
803 && d.points.len().saturating_sub(preview) >= 3
804 && d.points[0].distance(point) <= reach
805 }) else {
806 return false;
807 };
808 drawing.closed = true;
809 self.complete_curr_drawing();
810 true
811 }
812
813 fn finish_curr_drawing(&mut self) {
818 if let Some(mut drawing) = self.curr_drawing.take() {
819 if drawing.tool_type == DrawingToolType::BarsPattern
822 && let (&[first, second], &[first_at, second_at]) =
823 (drawing.chart_points.as_slice(), drawing.points.as_slice())
824 {
825 let (bar, x) = if second.bar_idx < first.bar_idx {
826 (second.bar_idx, second_at.x)
827 } else {
828 (first.bar_idx, first_at.x)
829 };
830 drawing.chart_points.push(ChartPoint::new(bar, first.price));
831 drawing.points.push(Pos2::new(x, first_at.y));
832 }
833 drawing.update_pane_coords(self.pane_rect);
834 if self.validate_created(&drawing).is_err() {
835 return;
836 }
837 if drawing.tool_type == DrawingToolType::Measure {
839 self.measurement = Some(drawing);
840 self.active_tool = None;
841 return;
842 }
843 let icon = matches!(
844 drawing.tool_type,
845 DrawingToolType::FontIcon | DrawingToolType::Sticker
846 )
847 .then(|| drawing.text.clone())
848 .flatten();
849 let glyph_size = drawing.font_size;
850 self.history.push_add(drawing.clone());
851 let id = drawing.id;
852 self.drawings.push(drawing);
853 self.select(id);
854 self.queue(DrawingSyncAction::SaveDrawing(id));
855 if !self.stay_in_drawing_mode {
856 self.active_tool = None;
857 self.active_custom_tool = None;
858 } else if let Some(icon) = icon {
859 self.start_icon_insertion(icon);
860 if let Some(prepared) = &mut self.curr_drawing {
861 prepared.font_size = glyph_size;
862 }
863 }
864 }
865 }
866
867 pub fn cancel_curr_drawing(&mut self) {
870 self.curr_drawing = None;
871 self.preview_point = false;
872 self.measurement = None;
873 }
874
875 pub fn complete_drag_drawing(&mut self) {
879 self.preview_point = false;
881 if let Some(drawing) = &mut self.curr_drawing
884 && matches!(
885 drawing.tool_type,
886 DrawingToolType::LongPos | DrawingToolType::ShortPos
887 )
888 && let (&[entry, target], &[entry_at, target_at]) =
889 (drawing.chart_points.as_slice(), drawing.points.as_slice())
890 {
891 let stop = 2.0 * entry.price - target.price;
892 drawing
893 .chart_points
894 .push(ChartPoint::new(target.bar_idx, stop));
895 drawing
896 .points
897 .push(Pos2::new(target_at.x, 2.0 * entry_at.y - target_at.y));
898 }
899 self.complete_curr_drawing();
900 }
901
902 pub fn update_drag_handle_with_coords<F, G, H>(
904 &mut self,
905 new_pos: Pos2,
906 x_to_bar: F,
907 y_to_price: G,
908 translate_price: H,
909 ) where
910 F: Fn(f32) -> f32,
911 G: Fn(f32) -> f64,
912 H: Fn(f64, f32, f32) -> f64,
913 {
914 self.update_drag_handle(new_pos, x_to_bar, y_to_price, translate_price);
915 }
916
917 pub fn hit_test_handle_by_id(&self, point: Pos2, drawing_id: usize) -> Option<HandlePos> {
920 if let Some(drawing) = self.drawings.iter().find(|d| d.id == drawing_id) {
921 self.hit_test_handle(drawing, point)
922 } else {
923 None
924 }
925 }
926}
927
928#[cfg(test)]
929mod tests {
930 use super::*;
931
932 #[test]
935 fn a_bars_pattern_is_copied_on_two_clicks_and_moved_by_its_copy() {
936 let mut manager = DrawingManager::new();
937 let x_to_bar = |x: f32| x / 10.0;
938 let y_to_price = |y: f32| 1000.0 - f64::from(y);
939 manager.start_drawing_with_coords(
940 DrawingToolType::BarsPattern,
941 Pos2::new(300.0, 200.0),
942 x_to_bar,
943 y_to_price,
944 );
945 manager.add_point_with_coords(Pos2::new(100.0, 400.0), x_to_bar, y_to_price);
946 assert_eq!(manager.drawings.len(), 1);
947 let drawing = &manager.drawings[0];
948 assert_eq!(
949 drawing.chart_points,
950 [
951 ChartPoint::new(30.0, 800.0),
952 ChartPoint::new(10.0, 600.0),
953 ChartPoint::new(10.0, 800.0)
954 ]
955 );
956 assert_eq!(drawing.points[2], Pos2::new(100.0, 200.0));
957 let id = drawing.id;
958 manager.start_drag_body(id, Pos2::new(150.0, 250.0));
959 manager.update_drag_handle(
960 Pos2::new(250.0, 150.0),
961 x_to_bar,
962 y_to_price,
963 |price, from, to| price - f64::from(to - from),
964 );
965 manager.end_drag_handle();
966 assert_eq!(
967 manager.drawings[0].chart_points,
968 [
969 ChartPoint::new(30.0, 800.0),
970 ChartPoint::new(10.0, 600.0),
971 ChartPoint::new(20.0, 900.0)
972 ]
973 );
974 }
975
976 #[test]
977 fn copied_groups_keep_their_layers_locally_and_when_synced() {
978 fn top_tool(manager: &DrawingManager) -> DrawingToolType {
979 let id = manager.hit_test(Pos2::new(75.0, 100.0)).unwrap();
980 manager
981 .drawings
982 .iter()
983 .find(|d| d.id == id)
984 .unwrap()
985 .tool_type
986 }
987 for (background_layer, line_layer, expected) in [
988 (2, 5, DrawingToolType::TrendLine),
989 (5, 5, DrawingToolType::TrendLine),
990 (5, 2, DrawingToolType::Rect),
991 ] {
992 let mut manager = DrawingManager::new();
993 for (id, tool, layer, points) in [
994 (
995 7,
996 DrawingToolType::Rect,
997 background_layer,
998 [Pos2::new(50.0, 50.0), Pos2::new(150.0, 150.0)],
999 ),
1000 (
1001 8,
1002 DrawingToolType::TrendLine,
1003 line_layer,
1004 [Pos2::new(50.0, 100.0), Pos2::new(150.0, 100.0)],
1005 ),
1006 ] {
1007 let mut drawing = Drawing::new(id, tool);
1008 drawing.z_order = layer;
1009 drawing.completed = true;
1010 drawing.fill_color = Some([30, 60, 95, 255]);
1011 drawing.points = points.to_vec();
1012 drawing.chart_points = points
1013 .iter()
1014 .map(|p| ChartPoint::new(p.x / 10.0, 200.0 - f64::from(p.y)))
1015 .collect();
1016 manager.drawings.push(drawing);
1017 }
1018 manager.select_drawings([7, 8]);
1019 manager.start_drag_body(7, Pos2::new(75.0, 75.0));
1020 manager.copy_dragged_drawings();
1021 manager.end_drag_handle();
1022 manager.deselect();
1023 assert_eq!(manager.drawings.len(), 4);
1024 assert_eq!(top_tool(&manager), expected);
1025 let mut peer = DrawingManager::new();
1026 for action in manager.pending_sync.lock().unwrap().iter() {
1027 if let DrawingSyncAction::SaveDrawing(id) = action {
1028 let mut copy = manager
1029 .drawings
1030 .iter()
1031 .find(|d| d.id == *id)
1032 .unwrap()
1033 .clone();
1034 copy.stored_id = Some(id.to_string());
1035 peer.add_synced_drawing(copy).unwrap();
1036 }
1037 }
1038 peer.update_all_screen_coords(|bar| bar * 10.0, |price| (200.0 - price) as f32);
1039 assert_eq!(peer.drawings.len(), 2);
1040 assert_eq!(top_tool(&peer), expected);
1041 assert!(manager.undo());
1042 assert_eq!(manager.drawings.len(), 2);
1043 assert!(!manager.undo());
1044 assert!(manager.redo());
1045 assert_eq!(manager.drawings.len(), 4);
1046 manager.update_all_screen_coords(|bar| bar * 10.0, |price| (200.0 - price) as f32);
1047 assert_eq!(top_tool(&manager), expected);
1048 }
1049 }
1050
1051 #[test]
1052 fn copy_drag_ignores_non_pointer_edits() {
1053 let mut manager = DrawingManager::new();
1054 manager
1055 .drawings
1056 .push(Drawing::new(7, DrawingToolType::TrendLine));
1057 manager.drag_edit_drawing(7, |drawing| drawing.text = Some("Draft".into()));
1058 manager.copy_dragged_drawings();
1059 assert_eq!(manager.drawings.len(), 1);
1060 manager.end_drag_handle();
1061 assert!(manager.undo());
1062 assert_eq!(manager.drawings[0].text, None);
1063 assert!(!manager.undo());
1064 }
1065
1066 #[test]
1069 fn annotation_boxes_move_without_their_anchor() {
1070 let x_to_bar = |x: f32| x / 10.0;
1071 let y_to_price = |y: f32| 1000.0 - f64::from(y);
1072 let translate = |price: f64, from: f32, to: f32| price - f64::from(to - from);
1073 let start_points = [Pos2::new(100.0, 200.0), Pos2::new(200.0, 100.0)];
1074 let start_chart = [ChartPoint::new(10.0, 800.0), ChartPoint::new(20.0, 900.0)];
1075 let label = (
1076 vec![Pos2::new(100.0, 200.0), Pos2::new(250.0, 140.0)],
1077 vec![ChartPoint::new(10.0, 800.0), ChartPoint::new(25.0, 860.0)],
1078 );
1079 let whole = (
1080 vec![Pos2::new(150.0, 240.0), Pos2::new(250.0, 140.0)],
1081 vec![ChartPoint::new(15.0, 760.0), ChartPoint::new(25.0, 860.0)],
1082 );
1083 for (tool, anchors, group, copy, expected) in [
1085 (DrawingToolType::Callout, 2, false, false, &label),
1086 (DrawingToolType::Note, 2, false, false, &label),
1087 (DrawingToolType::PriceNote, 2, false, false, &label),
1088 (DrawingToolType::Note, 1, false, false, &whole),
1089 (DrawingToolType::TrendLine, 2, false, false, &whole),
1090 (DrawingToolType::Callout, 2, true, false, &whole),
1091 (DrawingToolType::Callout, 2, false, true, &whole),
1092 ] {
1093 let mut manager = DrawingManager::new();
1094 for id in [7, 8] {
1095 let mut drawing = Drawing::new(id, tool);
1096 drawing.points = start_points[..anchors].to_vec();
1097 drawing.chart_points = start_chart[..anchors].to_vec();
1098 manager.drawings.push(drawing);
1099 }
1100 if group {
1101 manager.select_drawings([7, 8]);
1102 }
1103 manager.start_drag_body(7, Pos2::new(205.0, 105.0));
1104 if copy {
1105 manager.copy_dragged_drawings();
1106 }
1107 manager.update_drag_handle(Pos2::new(255.0, 145.0), x_to_bar, y_to_price, translate);
1108 manager.end_drag_handle();
1109 let moved = manager
1110 .drawings
1111 .last()
1112 .filter(|_| copy)
1113 .unwrap_or(&manager.drawings[0]);
1114 assert_eq!(
1115 moved.points,
1116 expected.0[..anchors],
1117 "{tool:?} {group} {copy}"
1118 );
1119 assert_eq!(
1120 moved.chart_points,
1121 expected.1[..anchors],
1122 "{tool:?} {group} {copy}"
1123 );
1124 if !copy {
1125 assert!(manager.undo());
1126 assert_eq!(
1127 manager.drawings[0].chart_points,
1128 start_chart[..anchors],
1129 "{tool:?}"
1130 );
1131 }
1132 }
1133 }
1134
1135 #[test]
1138 fn second_anchors_follow_the_first_during_placement() {
1139 let same_price: fn(Pos2) -> Pos2 = |p| Pos2::new(p.x, 200.0);
1140 let same_bar: fn(Pos2) -> Pos2 = |p| Pos2::new(100.0, p.y);
1141 let chart = |p: Pos2| ChartPoint::new(p.x / 10.0, 1000.0 - f64::from(p.y));
1142 for (tool, aligned) in [
1143 (DrawingToolType::TimeCycles, same_price),
1144 (DrawingToolType::Signpost, same_bar),
1145 ] {
1146 for (preview, drag) in [(false, false), (true, false), (true, true)] {
1147 let mut manager = DrawingManager::new();
1148 let x_to_bar = |x: f32| x / 10.0;
1149 let y_to_price = |y: f32| 1000.0 - f64::from(y);
1150 let start = Pos2::new(100.0, 200.0);
1151 manager.start_drawing_with_coords(tool, start, x_to_bar, y_to_price);
1152 if preview {
1153 for point in [Pos2::new(150.0, 100.0), Pos2::new(300.0, 250.0)] {
1154 manager.update_last_point_with_coords(point, x_to_bar, y_to_price);
1155 let drawing = manager.curr_drawing.as_ref().unwrap();
1156 assert_eq!(drawing.points[1], aligned(point), "{tool:?}");
1157 assert_eq!(drawing.chart_points[1], chart(aligned(point)), "{tool:?}");
1158 }
1159 }
1160 let end = Pos2::new(300.0, 250.0);
1161 if drag {
1162 manager.complete_drag_drawing();
1163 } else {
1164 manager.add_point_with_coords(end, x_to_bar, y_to_price);
1165 }
1166 assert_eq!(manager.drawings.len(), 1);
1167 assert!(manager.is_selected(manager.drawings[0].id));
1168 assert_eq!(
1169 manager.drawings[0].points,
1170 vec![start, aligned(end)],
1171 "{tool:?}"
1172 );
1173 assert_eq!(
1174 manager.drawings[0].chart_points,
1175 vec![chart(start), chart(aligned(end))],
1176 "{tool:?}"
1177 );
1178 }
1179 }
1180 }
1181
1182 #[test]
1185 fn new_drawings_take_their_tool_style_then_the_saved_default() {
1186 let place = |manager: &mut DrawingManager, tool| {
1187 manager.start_drawing_with_coords(tool, Pos2::new(10.0, 10.0), |x| x, f64::from);
1188 manager.curr_drawing.take().unwrap()
1189 };
1190 let mut manager = DrawingManager::new();
1191 let line = place(&mut manager, DrawingToolType::TrendLine);
1192 let rect = place(&mut manager, DrawingToolType::Rect);
1193 assert_ne!(line.color, rect.color);
1194 assert_eq!(line.fill_color, None);
1195 assert_eq!(rect.fill_color.unwrap()[..3], rect.color[..3]);
1196
1197 manager.options.default_color = Some([1, 2, 3, 255]);
1198 let rect = place(&mut manager, DrawingToolType::Rect);
1199 assert_eq!(
1200 (rect.color, rect.fill_color),
1201 ([1, 2, 3, 255], Some([1, 2, 3, 51]))
1202 );
1203
1204 let mut saved = Drawing::new(0, DrawingToolType::Rect);
1205 (saved.color, saved.fill_color) = ([9, 9, 9, 255], None);
1206 let saved = crate::drawings::DrawingTemplate::from_drawing(&saved);
1207 manager.tool_defaults.insert(DrawingToolType::Rect, saved);
1208 let rect = place(&mut manager, DrawingToolType::Rect);
1209 assert_eq!((rect.color, rect.fill_color), ([9, 9, 9, 255], None));
1210
1211 let mut closed = Drawing::new(0, DrawingToolType::Polyline);
1213 closed.closed = true;
1214 manager.tool_defaults.insert(
1215 DrawingToolType::Polyline,
1216 crate::drawings::DrawingTemplate::from_drawing(&closed),
1217 );
1218 assert!(!place(&mut manager, DrawingToolType::Polyline).closed);
1219 let open = Drawing::new(0, DrawingToolType::Polyline);
1220 crate::drawings::DrawingTemplate::from_drawing(&open)
1221 .apply_to(&mut closed)
1222 .unwrap();
1223 assert!(closed.closed);
1224 }
1225}