Skip to main content

egui_charts/drawings/services/
handle.rs

1//! Handle manipulation service for drawing tools.
2//!
3//! Provides all handle-related operations for selected drawings:
4//!
5//! - **Discovery** -- [`HandleService::get_handles`] returns the handle
6//!   positions for a drawing based on its tool type.
7//! - **Hit testing** -- [`HandleService::hit_test_handle`] determines which
8//!   handle (if any) is under the cursor.
9//! - **Dragging** -- [`HandleService::update_handle`] moves a handle and
10//!   updates both screen and chart coordinates.
11//! - **Rendering** -- [`HandleService::render_handles`] draws round handles
12//!   with selection highlighting.
13
14use crate::drawings::domain::{Drawing, DrawingToolType, HandlePos};
15use crate::drawings::validation::MAX_IMAGE_SIDE;
16use crate::styles::typography::font_size;
17use crate::theme::ThemeExt;
18use egui::{Color32, Pos2};
19
20/// Configuration for selection handle appearance and hit testing.
21#[derive(Clone, Debug)]
22pub struct HandleConfig {
23    /// Handle diameter in pixels.
24    pub size: f32,
25    /// Ring color of a handle as RGBA bytes (`Color32::PLACEHOLDER.to_array()`: the theme's
26    /// accent).
27    pub color: [u8; 4],
28    /// Fill color of the handle being dragged as RGBA bytes
29    /// (`Color32::PLACEHOLDER.to_array()`: the theme's accent).
30    pub selected_color: [u8; 4],
31    /// Whether to render handles at all (can be disabled for screenshot mode, etc.).
32    pub show_handles: bool,
33}
34
35impl Default for HandleConfig {
36    fn default() -> Self {
37        Self {
38            size: 8.0,
39            color: Color32::PLACEHOLDER.to_array(),
40            selected_color: Color32::PLACEHOLDER.to_array(),
41            show_handles: true,
42        }
43    }
44}
45
46/// Stateless service for selection handle operations on drawings.
47///
48/// All methods are static (`&self`-free) since `HandleService` carries no state.
49/// It computes handle positions, performs hit testing, updates handle positions
50/// during drags, and renders the handles onto the chart.
51pub struct HandleService;
52
53impl HandleService {
54    /// Returns the selection handles for a drawing based on its tool type.
55    ///
56    /// Handle layouts vary by tool type:
57    /// - **Two-point tools** (TrendLine, Fibonacci, etc.): Start and End handles
58    /// - **Rectangle and Gann box**: Four corner handles and four edge handles
59    /// - **Single-point tools** (HorizontalLine, VerticalLine): One Start handle
60    /// - **Glyphs**: Four resize corners around the centre and a Middle move handle
61    /// - **Image**: One bottom-right corner that resizes with locked proportions
62    /// - **Brushes**: One Middle handle on the stroke, which moves it whole
63    /// - **Three-anchor channels**: Four indexed handles, one at each line end
64    /// - **Bars pattern**: Start and End at the ends of its copy
65    /// - **Multi-point tools**: Indexed handles at each anchor
66    pub fn get_handles(drawing: &Drawing) -> Vec<(HandlePos, Pos2)> {
67        let mut handles = Vec::new();
68
69        match drawing.tool_type {
70            DrawingToolType::TrendLine | DrawingToolType::Measure => {
71                if !drawing.points.is_empty() {
72                    handles.push((HandlePos::Start, drawing.points[0]));
73                }
74                if drawing.points.len() >= 2 {
75                    handles.push((HandlePos::End, drawing.points[1]));
76                }
77            }
78            DrawingToolType::Rect | DrawingToolType::GannBox => {
79                if drawing.points.len() >= 2 {
80                    let p1 = drawing.points[0];
81                    let p2 = drawing.points[1];
82                    handles.push((HandlePos::TopLeft, p1));
83                    handles.push((HandlePos::TopRight, Pos2::new(p2.x, p1.y)));
84                    handles.push((HandlePos::BottomLeft, Pos2::new(p1.x, p2.y)));
85                    handles.push((HandlePos::BottomRight, p2));
86                    let mid = p1 + (p2 - p1) / 2.0;
87                    handles.push((HandlePos::Top, Pos2::new(mid.x, p1.y)));
88                    handles.push((HandlePos::Bottom, Pos2::new(mid.x, p2.y)));
89                    handles.push((HandlePos::Left, Pos2::new(p1.x, mid.y)));
90                    handles.push((HandlePos::Right, Pos2::new(p2.x, mid.y)));
91                }
92            }
93            DrawingToolType::FontIcon | DrawingToolType::Sticker => {
94                if let Some(&center) = drawing.points.first() {
95                    let rect = egui::Rect::from_center_size(
96                        center,
97                        egui::Vec2::splat(font_size(drawing.font_size)),
98                    );
99                    handles.push((HandlePos::TopLeft, rect.left_top()));
100                    handles.push((HandlePos::TopRight, rect.right_top()));
101                    handles.push((HandlePos::BottomLeft, rect.left_bottom()));
102                    handles.push((HandlePos::BottomRight, rect.right_bottom()));
103                    handles.push((HandlePos::Middle, center));
104                }
105            }
106            DrawingToolType::HorizontalLine => {
107                if !drawing.points.is_empty() {
108                    handles.push((HandlePos::Start, drawing.points[0]));
109                }
110            }
111            DrawingToolType::VerticalLine => {
112                if !drawing.points.is_empty() {
113                    handles.push((HandlePos::Start, drawing.points[0]));
114                }
115            }
116            // Fibonacci and similar two-point tools
117            DrawingToolType::FibonacciRetracement
118            | DrawingToolType::GannFan
119            | DrawingToolType::GannSquare => {
120                if !drawing.points.is_empty() {
121                    handles.push((HandlePos::Start, drawing.points[0]));
122                }
123                if drawing.points.len() >= 2 {
124                    handles.push((HandlePos::End, drawing.points[1]));
125                }
126            }
127            // A position: the entry and the width on the entry line, the target and the stop
128            // on the box's left edge, and the box's centre, which the body drag moves.
129            DrawingToolType::LongPos | DrawingToolType::ShortPos => {
130                if let &[entry, target, ..] = drawing.points.as_slice() {
131                    let right = Drawing::position_right(entry, target);
132                    handles.push((HandlePos::Start, entry));
133                    handles.push((HandlePos::End, Pos2::new(right, entry.y)));
134                    handles.push((HandlePos::Point(1), Pos2::new(entry.x, target.y)));
135                    if let Some(stop) = drawing.points.get(2) {
136                        handles.push((HandlePos::Point(2), Pos2::new(entry.x, stop.y)));
137                    }
138                    handles.push((
139                        HandlePos::Middle,
140                        Pos2::new((entry.x + right) / 2.0, entry.y),
141                    ));
142                } else if let Some(&entry) = drawing.points.first() {
143                    handles.push((HandlePos::Start, entry));
144                }
145            }
146            DrawingToolType::ParallelChannel
147            | DrawingToolType::FlatTopBottom
148            | DrawingToolType::DisjointChannel => {
149                for (i, &point) in drawing.points.iter().take(2).enumerate() {
150                    handles.push((HandlePos::Point(i), point));
151                }
152                if let Some(ends) = drawing.channel_second_line() {
153                    for (i, point) in ends.into_iter().enumerate() {
154                        handles.push((HandlePos::Point(i + 2), point));
155                    }
156                }
157            }
158            DrawingToolType::RegressionTrend => {
159                for (i, &point) in drawing.points.iter().take(2).enumerate() {
160                    handles.push((HandlePos::Point(i), point));
161                }
162            }
163            // A freehand stroke moves as a whole from the sample halfway along it.
164            DrawingToolType::Brush | DrawingToolType::Highlighter => {
165                if let Some(&point) = drawing.points.get(drawing.points.len() / 2) {
166                    handles.push((HandlePos::Middle, point));
167                }
168            }
169            // The ends of a bars pattern's copy: its anchor, and the source's width to the right.
170            DrawingToolType::BarsPattern if drawing.points.len() >= 3 => {
171                let [a, b, c] = [drawing.points[0], drawing.points[1], drawing.points[2]];
172                handles.push((HandlePos::Start, c));
173                handles.push((HandlePos::End, Pos2::new(c.x + (b.x - a.x).abs(), c.y)));
174            }
175            DrawingToolType::Image => {
176                if let Some(frame) = drawing.image_frame() {
177                    handles.push((HandlePos::BottomRight, frame.max));
178                }
179            }
180            // Multi-point tools - show indexed handles
181            _ => {
182                for (i, &point) in drawing.points.iter().enumerate() {
183                    handles.push((HandlePos::Point(i), point));
184                }
185            }
186        }
187
188        handles
189    }
190
191    /// Hit-tests to find which handle (if any) is at a screen position.
192    ///
193    /// Returns `Some(HandlePos)` if `point` is within `handle_size * 1.5` pixels
194    /// of a handle center, `None` otherwise.
195    pub fn hit_test_handle(drawing: &Drawing, point: Pos2, handle_size: f32) -> Option<HandlePos> {
196        let hit_radius = handle_size * 1.5;
197        // The nearest handle wins, so a small glyph's centre still moves it instead of
198        // grabbing a corner that lies within reach.
199        Self::get_handles(drawing)
200            .into_iter()
201            .map(|(pos, handle_point)| (pos, point.distance(handle_point)))
202            .filter(|&(_, dist)| dist <= hit_radius)
203            .min_by(|a, b| a.1.total_cmp(&b.1))
204            .map(|(pos, _)| pos)
205    }
206
207    /// Moves a handle to a new screen position, updating both screen coordinates
208    /// and chart coordinates using the provided conversion closures.
209    ///
210    /// For the `Middle` handle, every anchor that moves with the drawing (all but a bars
211    /// pattern's source) is translated by the delta between the old and new centroid positions.
212    /// `translate_price(price, from_y, to_y)` shifts their prices in scale units;
213    /// other handles use `y_to_price`.
214    pub fn update_handle<F, G, H>(
215        drawing: &mut Drawing,
216        handle: HandlePos,
217        new_pos: Pos2,
218        x_to_bar: F,
219        y_to_price: G,
220        translate_price: H,
221    ) where
222        F: Fn(f32) -> f32,
223        G: Fn(f32) -> f64,
224        H: Fn(f64, f32, f32) -> f64,
225    {
226        if handle == HandlePos::Middle {
227            let first = drawing.first_moved_anchor();
228            let count = drawing.points.len().saturating_sub(first).max(1) as f32;
229            let centroid = drawing
230                .points
231                .iter()
232                .skip(first)
233                .fold(egui::Vec2::ZERO, |sum, p| sum + p.to_vec2())
234                / count;
235            let center = Self::get_handles(drawing)
236                .into_iter()
237                .find(|(h, _)| *h == handle)
238                .map_or(Pos2::ZERO + centroid, |(_, p)| p);
239            if !drawing.points.is_empty() {
240                let bars = x_to_bar(new_pos.x) - x_to_bar(center.x);
241                for point in drawing.chart_points.iter_mut().skip(first) {
242                    point.bar_idx += bars;
243                    point.price = translate_price(point.price, center.y, new_pos.y);
244                }
245                for point in drawing.points.iter_mut().skip(first) {
246                    *point += new_pos - center;
247                }
248            }
249            return;
250        }
251        match drawing.tool_type {
252            DrawingToolType::Image => {
253                let Some(frame) = drawing.image_frame() else {
254                    return;
255                };
256                let (origin, size) = (frame.min, frame.size());
257                let scale = ((new_pos.x - origin.x) / size.x).max((new_pos.y - origin.y) / size.y);
258                // Never below 16 points a side, unless the picture is already smaller.
259                let low = (16.0 / size.min_elem()).min(1.0);
260                let high = MAX_IMAGE_SIDE / size.max_elem();
261                let scale = if scale.is_finite() {
262                    scale.clamp(low, high)
263                } else {
264                    1.0
265                };
266                // Rounding the scale must not put a side above the stored limit.
267                drawing.image.get_or_insert_with(Default::default).size =
268                    Some((size * scale).min(egui::Vec2::splat(MAX_IMAGE_SIDE)).into());
269            }
270            // The copy's ends move in time: the start moves the left source anchor with the
271            // copy's anchor, the end the right source anchor, so the source gains or loses
272            // bars at that end and the copy's other end stays.
273            DrawingToolType::BarsPattern
274                if drawing.points.len() >= 3 && drawing.chart_points.len() >= 3 =>
275            {
276                let Some((_, current)) = Self::get_handles(drawing)
277                    .into_iter()
278                    .find(|(h, _)| *h == handle)
279                else {
280                    return;
281                };
282                let left = usize::from(drawing.points[1].x < drawing.points[0].x);
283                let moved: &[usize] = match handle {
284                    HandlePos::Start => &[left, 2],
285                    HandlePos::End => &[1 - left],
286                    _ => return,
287                };
288                let bars = x_to_bar(new_pos.x) - x_to_bar(current.x);
289                for &i in moved {
290                    drawing.points[i].x += new_pos.x - current.x;
291                    drawing.chart_points[i].bar_idx += bars;
292                }
293            }
294            DrawingToolType::TrendLine
295            | DrawingToolType::Measure
296            | DrawingToolType::FibonacciRetracement
297            | DrawingToolType::GannFan
298            | DrawingToolType::GannSquare => {
299                Self::update_two_point_handle(drawing, handle, new_pos, &x_to_bar, &y_to_price);
300            }
301            DrawingToolType::Rect | DrawingToolType::GannBox => {
302                Self::update_rect_handle(drawing, handle, new_pos, &x_to_bar, &y_to_price);
303            }
304            DrawingToolType::FontIcon | DrawingToolType::Sticker
305                if matches!(
306                    handle,
307                    HandlePos::TopLeft
308                        | HandlePos::TopRight
309                        | HandlePos::BottomLeft
310                        | HandlePos::BottomRight
311                ) =>
312            {
313                if let Some(&center) = drawing.points.first() {
314                    // The centre-to-corner distance is half the square's diagonal.
315                    drawing.font_size =
316                        font_size(center.distance(new_pos) * std::f32::consts::SQRT_2);
317                }
318            }
319            DrawingToolType::HorizontalLine => {
320                if !drawing.points.is_empty() && !drawing.chart_points.is_empty() {
321                    drawing.points[0].y = new_pos.y;
322                    drawing.chart_points[0].price = y_to_price(new_pos.y);
323                }
324            }
325            DrawingToolType::VerticalLine => {
326                if !drawing.points.is_empty() && !drawing.chart_points.is_empty() {
327                    drawing.points[0].x = new_pos.x;
328                    drawing.chart_points[0].bar_idx = x_to_bar(new_pos.x);
329                }
330            }
331            DrawingToolType::ParallelChannel
332            | DrawingToolType::FlatTopBottom
333            | DrawingToolType::DisjointChannel => {
334                let HandlePos::Point(index) = handle else {
335                    return;
336                };
337                if index < 2 {
338                    Self::update_multi_point_handle(
339                        drawing,
340                        handle,
341                        new_pos,
342                        &x_to_bar,
343                        &y_to_price,
344                    );
345                } else if index < 4 && drawing.chart_points.len() >= 3 {
346                    let Some(ends) = drawing.channel_second_line() else {
347                        return;
348                    };
349                    let delta = new_pos - ends[index - 2];
350                    match drawing.tool_type {
351                        DrawingToolType::DisjointChannel => {
352                            // Keep the opposite ends fixed and reflect this end's price move.
353                            let base = drawing.points[index - 2];
354                            drawing.points[index - 2] = Pos2::new(new_pos.x, base.y - delta.y);
355                            let anchor = &mut drawing.chart_points[index - 2];
356                            anchor.bar_idx = x_to_bar(new_pos.x);
357                            anchor.price = translate_price(anchor.price, base.y, base.y - delta.y);
358                            Self::update_multi_point_handle(
359                                drawing,
360                                HandlePos::Point(2),
361                                new_pos,
362                                &x_to_bar,
363                                &y_to_price,
364                            );
365                        }
366                        DrawingToolType::ParallelChannel
367                            if (drawing.points[1].x - drawing.points[0].x).abs() < 1e-3 =>
368                        {
369                            drawing.points[2].x += delta.x;
370                            drawing.chart_points[2].bar_idx = x_to_bar(drawing.points[2].x);
371                        }
372                        _ => {
373                            drawing.points[2].y += delta.y;
374                            drawing.chart_points[2].price =
375                                if drawing.tool_type == DrawingToolType::ParallelChannel {
376                                    translate_price(
377                                        drawing.chart_points[2].price,
378                                        ends[index - 2].y,
379                                        new_pos.y,
380                                    )
381                                } else {
382                                    y_to_price(new_pos.y)
383                                };
384                        }
385                    }
386                }
387            }
388            DrawingToolType::RegressionTrend => {
389                let index = match handle {
390                    HandlePos::Start | HandlePos::Point(0) => 0,
391                    HandlePos::End | HandlePos::Point(1) => 1,
392                    _ => return,
393                };
394                if let (Some(point), Some(anchor)) = (
395                    drawing.points.get_mut(index),
396                    drawing.chart_points.get_mut(index),
397                ) {
398                    point.x = new_pos.x;
399                    anchor.bar_idx = x_to_bar(new_pos.x);
400                }
401            }
402            DrawingToolType::TimeCycles | DrawingToolType::Signpost => {
403                match handle {
404                    HandlePos::Start | HandlePos::Point(0) => {
405                        if let (Some(point), Some(anchor)) =
406                            (drawing.points.first_mut(), drawing.chart_points.first_mut())
407                        {
408                            *point = new_pos;
409                            anchor.bar_idx = x_to_bar(new_pos.x);
410                            anchor.price = y_to_price(new_pos.y);
411                        }
412                    }
413                    HandlePos::End | HandlePos::Point(1)
414                        if drawing.points.len() > 1 && drawing.chart_points.len() > 1 =>
415                    {
416                        if drawing.tool_type == DrawingToolType::TimeCycles {
417                            drawing.points[1].x = new_pos.x;
418                            drawing.chart_points[1].bar_idx = x_to_bar(new_pos.x);
419                        } else {
420                            drawing.points[1].y = new_pos.y;
421                            drawing.chart_points[1].price = y_to_price(new_pos.y);
422                        }
423                    }
424                    _ => return,
425                }
426                drawing.align_second_anchor();
427            }
428            // The target and the stop move in price only; the width handle moves the right
429            // edge, where the target and the stop anchors sit, never left of the entry.
430            DrawingToolType::LongPos | DrawingToolType::ShortPos => match handle {
431                HandlePos::Point(level @ (1 | 2))
432                    if level < drawing.points.len() && level < drawing.chart_points.len() =>
433                {
434                    drawing.points[level].y = new_pos.y;
435                    drawing.chart_points[level].price = y_to_price(new_pos.y);
436                }
437                HandlePos::End if drawing.points.len() > 1 && drawing.chart_points.len() > 1 => {
438                    let x = new_pos.x.max(drawing.points[0].x);
439                    for level in 1..drawing.points.len().min(drawing.chart_points.len()) {
440                        drawing.points[level].x = x;
441                        drawing.chart_points[level].bar_idx = x_to_bar(x);
442                    }
443                }
444                _ => Self::update_multi_point_handle(
445                    drawing,
446                    handle,
447                    new_pos,
448                    &x_to_bar,
449                    &y_to_price,
450                ),
451            },
452            _ => {
453                Self::update_multi_point_handle(drawing, handle, new_pos, &x_to_bar, &y_to_price);
454            }
455        }
456    }
457
458    /// Update handle for two-point tools (trendline, fib, etc.)
459    fn update_two_point_handle<F, G>(
460        drawing: &mut Drawing,
461        handle: HandlePos,
462        new_pos: Pos2,
463        x_to_bar: &F,
464        y_to_price: &G,
465    ) where
466        F: Fn(f32) -> f32,
467        G: Fn(f32) -> f64,
468    {
469        match handle {
470            HandlePos::Start if !drawing.points.is_empty() => {
471                drawing.points[0] = new_pos;
472                if !drawing.chart_points.is_empty() {
473                    drawing.chart_points[0].bar_idx = x_to_bar(new_pos.x);
474                    drawing.chart_points[0].price = y_to_price(new_pos.y);
475                }
476            }
477            HandlePos::End if drawing.points.len() >= 2 => {
478                drawing.points[1] = new_pos;
479                if drawing.chart_points.len() >= 2 {
480                    drawing.chart_points[1].bar_idx = x_to_bar(new_pos.x);
481                    drawing.chart_points[1].price = y_to_price(new_pos.y);
482                }
483            }
484            _ => {}
485        }
486    }
487
488    /// Update handle for rectangle tool
489    fn update_rect_handle<F, G>(
490        drawing: &mut Drawing,
491        handle: HandlePos,
492        new_pos: Pos2,
493        x_to_bar: &F,
494        y_to_price: &G,
495    ) where
496        F: Fn(f32) -> f32,
497        G: Fn(f32) -> f64,
498    {
499        if drawing.points.len() < 2 || drawing.chart_points.len() < 2 {
500            return;
501        }
502
503        match handle {
504            HandlePos::TopLeft => {
505                drawing.points[0] = new_pos;
506                drawing.chart_points[0].bar_idx = x_to_bar(new_pos.x);
507                drawing.chart_points[0].price = y_to_price(new_pos.y);
508            }
509            HandlePos::TopRight => {
510                drawing.points[0].y = new_pos.y;
511                drawing.points[1].x = new_pos.x;
512                drawing.chart_points[0].price = y_to_price(new_pos.y);
513                drawing.chart_points[1].bar_idx = x_to_bar(new_pos.x);
514            }
515            HandlePos::BottomLeft => {
516                drawing.points[0].x = new_pos.x;
517                drawing.points[1].y = new_pos.y;
518                drawing.chart_points[0].bar_idx = x_to_bar(new_pos.x);
519                drawing.chart_points[1].price = y_to_price(new_pos.y);
520            }
521            HandlePos::BottomRight => {
522                drawing.points[1] = new_pos;
523                drawing.chart_points[1].bar_idx = x_to_bar(new_pos.x);
524                drawing.chart_points[1].price = y_to_price(new_pos.y);
525            }
526            HandlePos::Top | HandlePos::Bottom => {
527                let index = usize::from(handle == HandlePos::Bottom);
528                drawing.points[index].y = new_pos.y;
529                drawing.chart_points[index].price = y_to_price(new_pos.y);
530            }
531            HandlePos::Left | HandlePos::Right => {
532                let index = usize::from(handle == HandlePos::Right);
533                drawing.points[index].x = new_pos.x;
534                drawing.chart_points[index].bar_idx = x_to_bar(new_pos.x);
535            }
536            _ => {}
537        }
538    }
539
540    /// Update handle for multi-point tools
541    fn update_multi_point_handle<F, G>(
542        drawing: &mut Drawing,
543        handle: HandlePos,
544        new_pos: Pos2,
545        x_to_bar: &F,
546        y_to_price: &G,
547    ) where
548        F: Fn(f32) -> f32,
549        G: Fn(f32) -> f64,
550    {
551        match handle {
552            HandlePos::Point(idx)
553                if idx < drawing.points.len() && idx < drawing.chart_points.len() =>
554            {
555                drawing.points[idx] = new_pos;
556                drawing.chart_points[idx].bar_idx = x_to_bar(new_pos.x);
557                drawing.chart_points[idx].price = y_to_price(new_pos.y);
558            }
559            HandlePos::Start if !drawing.points.is_empty() && !drawing.chart_points.is_empty() => {
560                drawing.points[0] = new_pos;
561                drawing.chart_points[0].bar_idx = x_to_bar(new_pos.x);
562                drawing.chart_points[0].price = y_to_price(new_pos.y);
563            }
564            HandlePos::End if drawing.points.len() >= 2 && drawing.chart_points.len() >= 2 => {
565                let last = drawing.points.len() - 1;
566                drawing.points[last] = new_pos;
567                drawing.chart_points[last].bar_idx = x_to_bar(new_pos.x);
568                drawing.chart_points[last].price = y_to_price(new_pos.y);
569            }
570            _ => {}
571        }
572    }
573
574    /// Renders selection handles like the selection dots of a series: an accent ring around
575    /// the chart background.
576    ///
577    /// The actively dragged handle (if any) is filled with
578    /// [`HandleConfig::selected_color`]; the others are ringed with [`HandleConfig::color`].
579    pub fn render_handles(
580        painter: &egui::Painter,
581        drawing: &Drawing,
582        config: &HandleConfig,
583        dragging_handle: Option<HandlePos>,
584    ) {
585        if !config.show_handles {
586            return;
587        }
588
589        let theme = painter.ctx().active_theme();
590        let color = |[r, g, b, a]: [u8; 4]| {
591            if [r, g, b, a] == Color32::PLACEHOLDER.to_array() {
592                theme.ui.accent
593            } else {
594                Color32::from_rgba_unmultiplied(r, g, b, a)
595            }
596        };
597        let radius = config.size / 2.0;
598        let glyph = matches!(
599            drawing.tool_type,
600            DrawingToolType::FontIcon | DrawingToolType::Sticker
601        );
602        if glyph && let Some(&center) = drawing.points.first() {
603            painter.rect_stroke(
604                egui::Rect::from_center_size(
605                    center,
606                    egui::Vec2::splat(font_size(drawing.font_size)),
607                ),
608                0.0,
609                egui::Stroke::new(1.0, color(config.color)),
610                egui::StrokeKind::Outside,
611            );
612        }
613
614        let position = matches!(
615            drawing.tool_type,
616            DrawingToolType::LongPos | DrawingToolType::ShortPos
617        );
618        for (pos, point) in Self::get_handles(drawing) {
619            // A position moves by its body; its target and stop handles are squares.
620            if (position || glyph) && pos == HandlePos::Middle {
621                continue;
622            }
623            let ring = if dragging_handle == Some(pos) {
624                None
625            } else {
626                Some(theme.chart.bg)
627            };
628            let mut fill = color(if ring.is_some() {
629                config.color
630            } else {
631                config.selected_color
632            });
633            if position && ring.is_some() {
634                fill = fill.lerp_to_gamma(theme.chart.bg, 0.15);
635            }
636            if position && matches!(pos, HandlePos::Point(_)) {
637                let square = egui::Rect::from_center_size(point, egui::Vec2::splat(2.0 * radius));
638                let corner = radius / 2.0;
639                painter.rect(
640                    square,
641                    corner,
642                    ring.unwrap_or(fill),
643                    egui::Stroke::new(1.0, fill),
644                    egui::StrokeKind::Inside,
645                );
646            } else if position {
647                painter.circle(
648                    point,
649                    radius - 0.5,
650                    ring.unwrap_or(fill),
651                    egui::Stroke::new(1.0, fill),
652                );
653            } else {
654                painter.circle_filled(point, radius, fill);
655                if let Some(bg) = ring {
656                    painter.circle_filled(point, radius - RING_WIDTH, bg);
657                }
658            }
659        }
660    }
661}
662
663/// Width of a handle's ring, as on a series' selection dots.
664const RING_WIDTH: f32 = 2.0;
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use crate::drawings::domain::ChartPoint;
670    use egui::pos2;
671
672    /// Screen to chart mapping used by the tests: 10 px per bar, price 1000 at
673    /// the top falling one unit per pixel.
674    fn x_to_bar(x: f32) -> f32 {
675        x / 10.0
676    }
677
678    fn y_to_price(y: f32) -> f64 {
679        1000.0 - y as f64
680    }
681
682    fn create_test_drawing(tool_type: DrawingToolType, points: Vec<Pos2>) -> Drawing {
683        let mut drawing = Drawing::new(1, tool_type);
684        drawing.chart_points = points
685            .iter()
686            .map(|p| ChartPoint::new(x_to_bar(p.x), y_to_price(p.y)))
687            .collect();
688        drawing.points = points;
689        drawing
690    }
691
692    #[test]
693    fn get_handles_by_tool() {
694        use HandlePos::*;
695        let cases = [
696            (
697                DrawingToolType::BarsPattern,
698                vec![pos2(300.0, 200.0), pos2(100.0, 400.0), pos2(500.0, 100.0)],
699                vec![(Start, pos2(500.0, 100.0)), (End, pos2(700.0, 100.0))],
700                None,
701            ),
702            (
703                DrawingToolType::FontIcon,
704                vec![pos2(100.0, 200.0)],
705                vec![
706                    (TopLeft, pos2(80.0, 180.0)),
707                    (TopRight, pos2(120.0, 180.0)),
708                    (BottomLeft, pos2(80.0, 220.0)),
709                    (BottomRight, pos2(120.0, 220.0)),
710                    (Middle, pos2(100.0, 200.0)),
711                ],
712                None,
713            ),
714            (
715                DrawingToolType::Sticker,
716                vec![pos2(100.0, 200.0)],
717                vec![
718                    (TopLeft, pos2(45.0, 145.0)),
719                    (TopRight, pos2(155.0, 145.0)),
720                    (BottomLeft, pos2(45.0, 255.0)),
721                    (BottomRight, pos2(155.0, 255.0)),
722                    (Middle, pos2(100.0, 200.0)),
723                ],
724                None,
725            ),
726            (
727                DrawingToolType::ParallelChannel,
728                vec![pos2(100.0, 200.0), pos2(300.0, 400.0), pos2(200.0, 200.0)],
729                vec![
730                    (Point(0), pos2(100.0, 200.0)),
731                    (Point(1), pos2(300.0, 400.0)),
732                    (Point(2), pos2(100.0, 100.0)),
733                    (Point(3), pos2(300.0, 300.0)),
734                ],
735                None,
736            ),
737            (
738                DrawingToolType::FlatTopBottom,
739                vec![pos2(100.0, 200.0), pos2(300.0, 400.0), pos2(200.0, 200.0)],
740                vec![
741                    (Point(0), pos2(100.0, 200.0)),
742                    (Point(1), pos2(300.0, 400.0)),
743                    (Point(2), pos2(100.0, 200.0)),
744                    (Point(3), pos2(300.0, 200.0)),
745                ],
746                None,
747            ),
748            (
749                DrawingToolType::DisjointChannel,
750                vec![pos2(100.0, 200.0), pos2(300.0, 400.0), pos2(200.0, 200.0)],
751                vec![
752                    (Point(0), pos2(100.0, 200.0)),
753                    (Point(1), pos2(300.0, 400.0)),
754                    (Point(2), pos2(100.0, 300.0)),
755                    (Point(3), pos2(300.0, 100.0)),
756                ],
757                None,
758            ),
759            (
760                DrawingToolType::TrendLine,
761                vec![pos2(100.0, 200.0), pos2(300.0, 400.0)],
762                vec![(Start, pos2(100.0, 200.0)), (End, pos2(300.0, 400.0))],
763                None,
764            ),
765            (
766                DrawingToolType::Rect,
767                vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
768                vec![
769                    (TopLeft, pos2(100.0, 100.0)),
770                    (TopRight, pos2(200.0, 100.0)),
771                    (BottomLeft, pos2(100.0, 300.0)),
772                    (BottomRight, pos2(200.0, 300.0)),
773                    (Top, pos2(150.0, 100.0)),
774                    (Bottom, pos2(150.0, 300.0)),
775                    (Left, pos2(100.0, 200.0)),
776                    (Right, pos2(200.0, 200.0)),
777                ],
778                None,
779            ),
780            (
781                DrawingToolType::GannBox,
782                vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
783                vec![
784                    (TopLeft, pos2(100.0, 100.0)),
785                    (TopRight, pos2(200.0, 100.0)),
786                    (BottomLeft, pos2(100.0, 300.0)),
787                    (BottomRight, pos2(200.0, 300.0)),
788                    (Top, pos2(150.0, 100.0)),
789                    (Bottom, pos2(150.0, 300.0)),
790                    (Left, pos2(100.0, 200.0)),
791                    (Right, pos2(200.0, 200.0)),
792                ],
793                None,
794            ),
795            (
796                DrawingToolType::LongPos,
797                vec![pos2(100.0, 200.0), pos2(400.0, 100.0), pos2(400.0, 250.0)],
798                vec![
799                    (Start, pos2(100.0, 200.0)),
800                    (End, pos2(400.0, 200.0)),
801                    (Point(1), pos2(100.0, 100.0)),
802                    (Point(2), pos2(100.0, 250.0)),
803                    (Middle, pos2(250.0, 200.0)),
804                ],
805                None,
806            ),
807            // Narrower than the minimum width: the width handle sits at the box's drawn edge.
808            (
809                DrawingToolType::ShortPos,
810                vec![pos2(100.0, 200.0), pos2(150.0, 250.0), pos2(150.0, 120.0)],
811                vec![
812                    (Start, pos2(100.0, 200.0)),
813                    (End, pos2(300.0, 200.0)),
814                    (Point(1), pos2(100.0, 250.0)),
815                    (Point(2), pos2(100.0, 120.0)),
816                    (Middle, pos2(200.0, 200.0)),
817                ],
818                None,
819            ),
820            (
821                DrawingToolType::HorizontalLine,
822                vec![pos2(50.0, 120.0)],
823                vec![(Start, pos2(50.0, 120.0))],
824                None,
825            ),
826            (
827                DrawingToolType::FibonacciRetracement,
828                vec![pos2(0.0, 0.0), pos2(100.0, 50.0)],
829                vec![(Start, pos2(0.0, 0.0)), (End, pos2(100.0, 50.0))],
830                None,
831            ),
832            (
833                DrawingToolType::FibonacciChannel,
834                vec![pos2(0.0, 0.0), pos2(30.0, 0.0), pos2(0.0, 60.0)],
835                vec![
836                    (Point(0), pos2(0.0, 0.0)),
837                    (Point(1), pos2(30.0, 0.0)),
838                    (Point(2), pos2(0.0, 60.0)),
839                ],
840                None,
841            ),
842            (
843                DrawingToolType::Triangle,
844                vec![pos2(0.0, 0.0), pos2(30.0, 0.0), pos2(0.0, 60.0)],
845                vec![
846                    (Point(0), pos2(0.0, 0.0)),
847                    (Point(1), pos2(30.0, 0.0)),
848                    (Point(2), pos2(0.0, 60.0)),
849                ],
850                None,
851            ),
852            (
853                DrawingToolType::Brush,
854                vec![
855                    pos2(0.0, 0.0),
856                    pos2(30.0, 20.0),
857                    pos2(60.0, 40.0),
858                    pos2(90.0, 60.0),
859                    pos2(120.0, 80.0),
860                ],
861                vec![(Middle, pos2(60.0, 40.0))],
862                None,
863            ),
864            (
865                DrawingToolType::Highlighter,
866                vec![pos2(0.0, 0.0), pos2(30.0, 20.0)],
867                vec![(Middle, pos2(30.0, 20.0))],
868                None,
869            ),
870            (
871                DrawingToolType::Image,
872                vec![pos2(100.0, 200.0)],
873                vec![(BottomRight, pos2(220.0, 290.0))],
874                None,
875            ),
876            (
877                DrawingToolType::Image,
878                vec![pos2(100.0, 200.0)],
879                vec![(BottomRight, pos2(300.0, 300.0))],
880                Some([200.0, 100.0]),
881            ),
882        ];
883        for (tool, points, expected, image_size) in cases {
884            let mut drawing = create_test_drawing(tool, points);
885            if let Some(size) = image_size {
886                drawing.image = Some(crate::drawings::ImageConfig {
887                    size: Some(size),
888                    ..Default::default()
889                });
890            }
891            assert_eq!(HandleService::get_handles(&drawing), expected, "{tool:?}");
892        }
893        // Renderer control counts, including generated position stops.
894        let anchor_counts: &[(&[DrawingToolType], usize)] = &[
895            (
896                &[
897                    DrawingToolType::CrossCursor,
898                    DrawingToolType::DotCursor,
899                    DrawingToolType::ArrowCursor,
900                    DrawingToolType::Eraser,
901                ],
902                0,
903            ),
904            (
905                &[
906                    DrawingToolType::HorizontalLine,
907                    DrawingToolType::HorizontalRay,
908                    DrawingToolType::VerticalLine,
909                    DrawingToolType::CrossLine,
910                    DrawingToolType::TextLabel,
911                    DrawingToolType::AnchoredText,
912                    DrawingToolType::AnchoredNote,
913                    DrawingToolType::PriceLabel,
914                    DrawingToolType::FlagNote,
915                    DrawingToolType::ArrowMarkUp,
916                    DrawingToolType::ArrowMarkDown,
917                    DrawingToolType::AnchoredVWAP,
918                    DrawingToolType::AnchoredVolumeProfile,
919                    DrawingToolType::Table,
920                    DrawingToolType::Comment,
921                    DrawingToolType::FontIcon,
922                    DrawingToolType::Tweet,
923                    DrawingToolType::Idea,
924                ],
925                1,
926            ),
927            (
928                &[
929                    DrawingToolType::TrendLine,
930                    DrawingToolType::Ray,
931                    DrawingToolType::InfoLine,
932                    DrawingToolType::ExtendedLine,
933                    DrawingToolType::TrendAngle,
934                    DrawingToolType::RegressionTrend,
935                    DrawingToolType::FibonacciRetracement,
936                    DrawingToolType::FibonacciTimeZones,
937                    DrawingToolType::FibonacciSpeedFan,
938                    DrawingToolType::FibonacciCircles,
939                    DrawingToolType::FibonacciSpiral,
940                    DrawingToolType::FibonacciSpeedResistanceArcs,
941                    DrawingToolType::GannBox,
942                    DrawingToolType::GannSquare,
943                    DrawingToolType::GannFixed,
944                    DrawingToolType::GannFan,
945                    DrawingToolType::CyclicLines,
946                    DrawingToolType::TimeCycles,
947                    DrawingToolType::SineLine,
948                    DrawingToolType::Forecast,
949                    DrawingToolType::FixedRangeVolumeProfile,
950                    DrawingToolType::Measure,
951                    DrawingToolType::PriceRange,
952                    DrawingToolType::DateRange,
953                    DrawingToolType::DateAndPriceRange,
954                    DrawingToolType::ArrowMarker,
955                    DrawingToolType::Arrow,
956                    DrawingToolType::Rect,
957                    DrawingToolType::Circle,
958                    DrawingToolType::Ellipse,
959                    DrawingToolType::Note,
960                    DrawingToolType::Signpost,
961                    DrawingToolType::PriceNote,
962                    DrawingToolType::Callout,
963                ],
964                2,
965            ),
966            (
967                &[
968                    DrawingToolType::Pitchfork,
969                    DrawingToolType::SchiffPitchfork,
970                    DrawingToolType::ModifiedSchiffPitchfork,
971                    DrawingToolType::InsidePitchfork,
972                    DrawingToolType::FibonacciExtension,
973                    DrawingToolType::FibonacciChannel,
974                    DrawingToolType::FibonacciWedge,
975                    DrawingToolType::TrendBasedFibTime,
976                    DrawingToolType::Pitchfan,
977                    DrawingToolType::Triangle,
978                    DrawingToolType::RotatedRect,
979                    DrawingToolType::Arc,
980                    DrawingToolType::Curve,
981                    DrawingToolType::ProjectionTool,
982                    DrawingToolType::GhostFeed,
983                    DrawingToolType::ElliottDoubleCombo,
984                ],
985                3,
986            ),
987            (
988                &[
989                    DrawingToolType::ABCDPattern,
990                    DrawingToolType::ElliottCorrection,
991                    DrawingToolType::TrianglePattern,
992                    DrawingToolType::DoubleCurve,
993                ],
994                4,
995            ),
996            (
997                &[
998                    DrawingToolType::XABCDPattern,
999                    DrawingToolType::CypherPattern,
1000                    DrawingToolType::ElliottTripleCombo,
1001                    DrawingToolType::Polyline,
1002                    DrawingToolType::Path,
1003                ],
1004                5,
1005            ),
1006            (
1007                &[
1008                    DrawingToolType::ElliottImpulse,
1009                    DrawingToolType::ElliottTriangle,
1010                ],
1011                6,
1012            ),
1013            (
1014                &[
1015                    DrawingToolType::HeadAndShoulders,
1016                    DrawingToolType::ThreeDrivesPattern,
1017                ],
1018                7,
1019            ),
1020        ];
1021        for &(tools, count) in anchor_counts {
1022            for &tool in tools {
1023                let points: Vec<_> = (0..count)
1024                    .map(|i| pos2(i as f32 * 30.0, i as f32 * 20.0))
1025                    .collect();
1026                let drawing = create_test_drawing(tool, points.clone());
1027                let handles = HandleService::get_handles(&drawing);
1028                for point in points {
1029                    assert!(
1030                        handles.iter().any(|(_, p)| *p == point),
1031                        "{tool:?}: no handle at {point:?}"
1032                    );
1033                }
1034            }
1035        }
1036    }
1037
1038    /// A picture's corner follows the leading pointer axis without changing proportions or
1039    /// anchors, stays within loadable sizes and undoes the whole gesture in one step.
1040    #[test]
1041    fn image_corner_drag_keeps_proportions() {
1042        use crate::drawings::{DrawingManager, ImageConfig};
1043        for (size, pointer, expected) in [
1044            (Some([200.0, 100.0]), pos2(500.0, 150.0), [400.0, 200.0]),
1045            (Some([200.0, 100.0]), pos2(150.0, 400.0), [600.0, 300.0]),
1046            (Some([200.0, 100.0]), pos2(90.0, 90.0), [32.0, 16.0]),
1047            (Some([200.0, 100.0]), pos2(1e6, 0.0), [10_000.0, 5000.0]),
1048            (Some([71.0, 20.0]), pos2(1e6, 0.0), [10_000.0, 2816.9014]),
1049            (None, pos2(340.0, 190.0), [240.0, 180.0]),
1050            (Some([240.0, 1.0]), pos2(100.0, 100.0), [240.0, 1.0]),
1051        ] {
1052            let mut manager = DrawingManager::new();
1053            let mut drawing = create_test_drawing(DrawingToolType::Image, vec![pos2(100.0, 100.0)]);
1054            drawing.image = size.map(|size| ImageConfig {
1055                size: Some(size),
1056                opacity: 128,
1057            });
1058            let before = drawing.clone();
1059            manager.drawings.push(drawing);
1060            manager.start_drag_handle(1, HandlePos::BottomRight);
1061            manager.update_drag_handle(pointer, x_to_bar, y_to_price, |p, from, to| {
1062                p + f64::from(from - to)
1063            });
1064            let drawing = &manager.drawings[0];
1065            let actual = drawing.image.unwrap();
1066            for (actual, expected) in actual.size.unwrap().into_iter().zip(expected) {
1067                assert!((actual - expected).abs() < 1e-3, "{size:?} {pointer:?}");
1068            }
1069            assert_eq!(actual.opacity, before.image.map_or(255, |c| c.opacity));
1070            assert_eq!(drawing.points, before.points);
1071            assert_eq!(drawing.chart_points, before.chart_points);
1072            assert!(
1073                crate::drawings::persistence::drawing_to_stored(drawing, |bar| bar as i64 * 60_000)
1074                    .invalid_field()
1075                    .is_none()
1076            );
1077            manager.end_drag_handle();
1078            // The last case is unchanged, so it deliberately adds no history entry.
1079            if actual != before.image.unwrap_or_default() {
1080                assert!(manager.undo());
1081                assert_eq!(manager.drawings[0].image, before.image);
1082                assert!(!manager.undo());
1083            }
1084        }
1085    }
1086
1087    /// Corner drags keep the centre fixed, clamp to loadable sizes and undo as one gesture.
1088    #[test]
1089    fn glyph_corner_drags_resize_about_the_centre() {
1090        use crate::drawings::DrawingManager;
1091        for tool in [DrawingToolType::FontIcon, DrawingToolType::Sticker] {
1092            for handle in [
1093                HandlePos::TopLeft,
1094                HandlePos::TopRight,
1095                HandlePos::BottomLeft,
1096                HandlePos::BottomRight,
1097            ] {
1098                let mut manager = DrawingManager::new();
1099                let drawing = create_test_drawing(tool, vec![pos2(100.0, 200.0)]);
1100                let (anchors, size_before) = (drawing.chart_points.clone(), drawing.font_size);
1101                manager.drawings.push(drawing);
1102                manager.start_drag_handle(1, handle);
1103                // A corner is half a diagonal from the centre: side = radius * sqrt(2).
1104                // Offsets (30, 40) have radius 50, giving side 70.710678.
1105                for (point, size) in [
1106                    (pos2(70.0, 160.0), 70.710_68),
1107                    (pos2(100.0, 200.0), 1.0),
1108                    (pos2(1000.0, 2000.0), 256.0),
1109                    (pos2(148.0, 248.0), 96.0),
1110                ] {
1111                    manager.update_drag_handle(point, x_to_bar, y_to_price, |p, from, to| {
1112                        p + f64::from(from - to)
1113                    });
1114                    let drawing = &manager.drawings[0];
1115                    assert!(
1116                        (drawing.font_size - size).abs() < 1e-4,
1117                        "{tool:?} {handle:?}"
1118                    );
1119                    assert_eq!(drawing.points, [pos2(100.0, 200.0)]);
1120                    assert_eq!(drawing.chart_points, anchors);
1121                    assert!(
1122                        crate::drawings::persistence::drawing_to_stored(drawing, |bar| {
1123                            bar as i64 * 60_000
1124                        })
1125                        .invalid_field()
1126                        .is_none()
1127                    );
1128                }
1129                let ctx = egui::Context::default();
1130                let output = ctx.run_ui(egui::RawInput::default(), |_| {
1131                    HandleService::render_handles(
1132                        &egui::Painter::new(
1133                            ctx.clone(),
1134                            egui::LayerId::background(),
1135                            egui::Rect::EVERYTHING,
1136                        ),
1137                        &manager.drawings[0],
1138                        &HandleConfig::default(),
1139                        Some(handle),
1140                    );
1141                });
1142                assert!(
1143                    output.shapes.iter().any(|s| matches!(&s.shape,
1144                        egui::Shape::Rect(r) if r.rect.center() == pos2(100.0, 200.0)
1145                            && (r.rect.width() - 96.0).abs() < 1e-4
1146                            && (r.rect.height() - 96.0).abs() < 1e-4 && r.stroke.width > 0.0
1147                    )),
1148                    "selected glyph has a square frame"
1149                );
1150                output.drop_without_applying_deltas();
1151                manager.end_drag_handle();
1152                assert!(manager.undo());
1153                assert_eq!(manager.drawings[0].font_size, size_before);
1154                assert!(!manager.undo(), "one undo for the whole drag");
1155                assert!(manager.redo());
1156                assert!((manager.drawings[0].font_size - 96.0).abs() < 1e-4);
1157                HandleService::update_handle(
1158                    &mut manager.drawings[0],
1159                    HandlePos::Middle,
1160                    pos2(110.0, 220.0),
1161                    x_to_bar,
1162                    y_to_price,
1163                    |p, from, to| p + f64::from(from - to),
1164                );
1165                assert_eq!(manager.drawings[0].points, [pos2(110.0, 220.0)]);
1166                assert_eq!(
1167                    manager.drawings[0].chart_points,
1168                    [ChartPoint::new(11.0, 780.0)]
1169                );
1170            }
1171        }
1172    }
1173
1174    #[test]
1175    fn test_hit_test_handle() {
1176        let drawing = create_test_drawing(
1177            DrawingToolType::TrendLine,
1178            vec![Pos2::new(100.0, 200.0), Pos2::new(300.0, 400.0)],
1179        );
1180
1181        // Test hitting the start handle
1182        let hit = HandleService::hit_test_handle(&drawing, Pos2::new(102.0, 202.0), 8.0);
1183        assert_eq!(hit, Some(HandlePos::Start));
1184
1185        // Test missing all handles
1186        let miss = HandleService::hit_test_handle(&drawing, Pos2::new(500.0, 500.0), 8.0);
1187        assert_eq!(miss, None);
1188
1189        // A 16 px glyph has its corners within reach of its centre: the centre still moves it.
1190        let mut glyph = create_test_drawing(DrawingToolType::Sticker, vec![pos2(100.0, 200.0)]);
1191        glyph.font_size = 16.0;
1192        for (point, expected) in [
1193            (pos2(100.0, 200.0), HandlePos::Middle),
1194            (pos2(108.0, 208.0), HandlePos::BottomRight),
1195        ] {
1196            assert_eq!(
1197                HandleService::hit_test_handle(&glyph, point, 8.0),
1198                Some(expected)
1199            );
1200        }
1201    }
1202
1203    /// A handle drag moves the screen points and the persisted chart points
1204    /// together: one anchor for an endpoint, one coordinate of each anchor for
1205    /// a rectangle corner, and every anchor for the Middle handle of a two-point
1206    /// or multi-point tool.
1207    #[test]
1208    fn update_handle_moves_screen_and_chart_points() {
1209        // Edges ignore the pointer's other coordinate, including reversed and crossed sides.
1210        let cases = [
1211            (
1212                DrawingToolType::BarsPattern,
1213                vec![pos2(300.0, 200.0), pos2(100.0, 400.0), pos2(500.0, 100.0)],
1214                HandlePos::End,
1215                pos2(750.0, 50.0),
1216                vec![pos2(350.0, 200.0), pos2(100.0, 400.0), pos2(500.0, 100.0)],
1217                vec![
1218                    ChartPoint::new(35.0, 800.0),
1219                    ChartPoint::new(10.0, 600.0),
1220                    ChartPoint::new(50.0, 900.0),
1221                ],
1222            ),
1223            (
1224                DrawingToolType::BarsPattern,
1225                vec![pos2(300.0, 200.0), pos2(100.0, 400.0), pos2(500.0, 100.0)],
1226                HandlePos::Start,
1227                pos2(450.0, 150.0),
1228                vec![pos2(300.0, 200.0), pos2(50.0, 400.0), pos2(450.0, 100.0)],
1229                vec![
1230                    ChartPoint::new(30.0, 800.0),
1231                    ChartPoint::new(5.0, 600.0),
1232                    ChartPoint::new(45.0, 900.0),
1233                ],
1234            ),
1235            (
1236                DrawingToolType::BarsPattern,
1237                vec![pos2(300.0, 200.0), pos2(100.0, 400.0), pos2(500.0, 100.0)],
1238                HandlePos::Middle,
1239                pos2(520.0, 90.0),
1240                vec![pos2(300.0, 200.0), pos2(100.0, 400.0), pos2(520.0, 90.0)],
1241                vec![
1242                    ChartPoint::new(30.0, 800.0),
1243                    ChartPoint::new(10.0, 600.0),
1244                    ChartPoint::new(52.0, 910.0),
1245                ],
1246            ),
1247            // The label's handle changes only its height; the bar's handle carries the label along.
1248            (
1249                DrawingToolType::Signpost,
1250                vec![pos2(100.0, 300.0), pos2(100.0, 200.0)],
1251                HandlePos::Point(1),
1252                pos2(250.0, 80.0),
1253                vec![pos2(100.0, 300.0), pos2(100.0, 80.0)],
1254                vec![ChartPoint::new(10.0, 700.0), ChartPoint::new(10.0, 920.0)],
1255            ),
1256            (
1257                DrawingToolType::Signpost,
1258                vec![pos2(100.0, 300.0), pos2(100.0, 200.0)],
1259                HandlePos::Point(0),
1260                pos2(150.0, 320.0),
1261                vec![pos2(150.0, 320.0), pos2(150.0, 200.0)],
1262                vec![ChartPoint::new(15.0, 680.0), ChartPoint::new(15.0, 800.0)],
1263            ),
1264            (
1265                DrawingToolType::Rect,
1266                vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1267                HandlePos::Top,
1268                pos2(250.0, 80.0),
1269                vec![pos2(100.0, 80.0), pos2(200.0, 300.0)],
1270                vec![ChartPoint::new(10.0, 920.0), ChartPoint::new(20.0, 700.0)],
1271            ),
1272            (
1273                DrawingToolType::Rect,
1274                vec![pos2(200.0, 300.0), pos2(100.0, 100.0)],
1275                HandlePos::Top,
1276                pos2(250.0, 80.0),
1277                vec![pos2(200.0, 80.0), pos2(100.0, 100.0)],
1278                vec![ChartPoint::new(20.0, 920.0), ChartPoint::new(10.0, 900.0)],
1279            ),
1280            (
1281                DrawingToolType::Rect,
1282                vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1283                HandlePos::Bottom,
1284                pos2(250.0, 80.0),
1285                vec![pos2(100.0, 100.0), pos2(200.0, 80.0)],
1286                vec![ChartPoint::new(10.0, 900.0), ChartPoint::new(20.0, 920.0)],
1287            ),
1288            (
1289                DrawingToolType::Rect,
1290                vec![pos2(200.0, 300.0), pos2(100.0, 100.0)],
1291                HandlePos::Bottom,
1292                pos2(250.0, 80.0),
1293                vec![pos2(200.0, 300.0), pos2(100.0, 80.0)],
1294                vec![ChartPoint::new(20.0, 700.0), ChartPoint::new(10.0, 920.0)],
1295            ),
1296            (
1297                DrawingToolType::Rect,
1298                vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1299                HandlePos::Left,
1300                pos2(250.0, 80.0),
1301                vec![pos2(250.0, 100.0), pos2(200.0, 300.0)],
1302                vec![ChartPoint::new(25.0, 900.0), ChartPoint::new(20.0, 700.0)],
1303            ),
1304            (
1305                DrawingToolType::Rect,
1306                vec![pos2(200.0, 300.0), pos2(100.0, 100.0)],
1307                HandlePos::Left,
1308                pos2(250.0, 80.0),
1309                vec![pos2(250.0, 300.0), pos2(100.0, 100.0)],
1310                vec![ChartPoint::new(25.0, 700.0), ChartPoint::new(10.0, 900.0)],
1311            ),
1312            (
1313                DrawingToolType::Rect,
1314                vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1315                HandlePos::Right,
1316                pos2(250.0, 80.0),
1317                vec![pos2(100.0, 100.0), pos2(250.0, 300.0)],
1318                vec![ChartPoint::new(10.0, 900.0), ChartPoint::new(25.0, 700.0)],
1319            ),
1320            (
1321                DrawingToolType::Rect,
1322                vec![pos2(200.0, 300.0), pos2(100.0, 100.0)],
1323                HandlePos::Right,
1324                pos2(250.0, 80.0),
1325                vec![pos2(200.0, 300.0), pos2(250.0, 100.0)],
1326                vec![ChartPoint::new(20.0, 700.0), ChartPoint::new(25.0, 900.0)],
1327            ),
1328            // The width anchor can be off screen while both boundary handles are visible.
1329            (
1330                DrawingToolType::ParallelChannel,
1331                vec![pos2(100.0, 100.0), pos2(200.0, 200.0), pos2(600.0, 700.0)],
1332                HandlePos::Point(2),
1333                pos2(150.0, 225.0),
1334                vec![pos2(100.0, 100.0), pos2(200.0, 200.0), pos2(600.0, 725.0)],
1335                vec![
1336                    ChartPoint::new(10.0, 900.0),
1337                    ChartPoint::new(20.0, 800.0),
1338                    ChartPoint::new(60.0, 275.0),
1339                ],
1340            ),
1341            (
1342                DrawingToolType::ParallelChannel,
1343                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1344                HandlePos::Point(0),
1345                pos2(150.0, 250.0),
1346                vec![pos2(150.0, 250.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1347                vec![
1348                    ChartPoint::new(15.0, 750.0),
1349                    ChartPoint::new(30.0, 700.0),
1350                    ChartPoint::new(20.0, 900.0),
1351                ],
1352            ),
1353            (
1354                DrawingToolType::ParallelChannel,
1355                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1356                HandlePos::Point(1),
1357                pos2(350.0, 250.0),
1358                vec![pos2(100.0, 200.0), pos2(350.0, 250.0), pos2(200.0, 100.0)],
1359                vec![
1360                    ChartPoint::new(10.0, 800.0),
1361                    ChartPoint::new(35.0, 750.0),
1362                    ChartPoint::new(20.0, 900.0),
1363                ],
1364            ),
1365            (
1366                DrawingToolType::ParallelChannel,
1367                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1368                HandlePos::Point(2),
1369                pos2(150.0, 75.0),
1370                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 125.0)],
1371                vec![
1372                    ChartPoint::new(10.0, 800.0),
1373                    ChartPoint::new(30.0, 700.0),
1374                    ChartPoint::new(20.0, 875.0),
1375                ],
1376            ),
1377            (
1378                DrawingToolType::ParallelChannel,
1379                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1380                HandlePos::Point(3),
1381                pos2(350.0, 175.0),
1382                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 125.0)],
1383                vec![
1384                    ChartPoint::new(10.0, 800.0),
1385                    ChartPoint::new(30.0, 700.0),
1386                    ChartPoint::new(20.0, 875.0),
1387                ],
1388            ),
1389            (
1390                DrawingToolType::FlatTopBottom,
1391                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1392                HandlePos::Point(0),
1393                pos2(150.0, 250.0),
1394                vec![pos2(150.0, 250.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1395                vec![
1396                    ChartPoint::new(15.0, 750.0),
1397                    ChartPoint::new(30.0, 700.0),
1398                    ChartPoint::new(20.0, 900.0),
1399                ],
1400            ),
1401            (
1402                DrawingToolType::FlatTopBottom,
1403                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1404                HandlePos::Point(1),
1405                pos2(350.0, 250.0),
1406                vec![pos2(100.0, 200.0), pos2(350.0, 250.0), pos2(200.0, 100.0)],
1407                vec![
1408                    ChartPoint::new(10.0, 800.0),
1409                    ChartPoint::new(35.0, 750.0),
1410                    ChartPoint::new(20.0, 900.0),
1411                ],
1412            ),
1413            (
1414                DrawingToolType::FlatTopBottom,
1415                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1416                HandlePos::Point(2),
1417                pos2(150.0, 125.0),
1418                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 125.0)],
1419                vec![
1420                    ChartPoint::new(10.0, 800.0),
1421                    ChartPoint::new(30.0, 700.0),
1422                    ChartPoint::new(20.0, 875.0),
1423                ],
1424            ),
1425            (
1426                DrawingToolType::FlatTopBottom,
1427                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1428                HandlePos::Point(3),
1429                pos2(350.0, 125.0),
1430                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 125.0)],
1431                vec![
1432                    ChartPoint::new(10.0, 800.0),
1433                    ChartPoint::new(30.0, 700.0),
1434                    ChartPoint::new(20.0, 875.0),
1435                ],
1436            ),
1437            (
1438                DrawingToolType::DisjointChannel,
1439                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1440                HandlePos::Point(0),
1441                pos2(150.0, 250.0),
1442                vec![pos2(150.0, 250.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1443                vec![
1444                    ChartPoint::new(15.0, 750.0),
1445                    ChartPoint::new(30.0, 700.0),
1446                    ChartPoint::new(20.0, 900.0),
1447                ],
1448            ),
1449            (
1450                DrawingToolType::DisjointChannel,
1451                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1452                HandlePos::Point(1),
1453                pos2(350.0, 250.0),
1454                vec![pos2(100.0, 200.0), pos2(350.0, 250.0), pos2(200.0, 100.0)],
1455                vec![
1456                    ChartPoint::new(10.0, 800.0),
1457                    ChartPoint::new(35.0, 750.0),
1458                    ChartPoint::new(20.0, 900.0),
1459                ],
1460            ),
1461            (
1462                DrawingToolType::DisjointChannel,
1463                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1464                HandlePos::Point(2),
1465                pos2(150.0, 175.0),
1466                vec![pos2(150.0, 175.0), pos2(300.0, 300.0), pos2(150.0, 175.0)],
1467                vec![
1468                    ChartPoint::new(15.0, 825.0),
1469                    ChartPoint::new(30.0, 700.0),
1470                    ChartPoint::new(15.0, 825.0),
1471                ],
1472            ),
1473            (
1474                DrawingToolType::DisjointChannel,
1475                vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1476                HandlePos::Point(3),
1477                pos2(350.0, 75.0),
1478                vec![pos2(100.0, 200.0), pos2(350.0, 275.0), pos2(350.0, 75.0)],
1479                vec![
1480                    ChartPoint::new(10.0, 800.0),
1481                    ChartPoint::new(35.0, 725.0),
1482                    ChartPoint::new(35.0, 925.0),
1483                ],
1484            ),
1485            (
1486                DrawingToolType::TimeCycles,
1487                vec![pos2(100.0, 200.0), pos2(300.0, 200.0)],
1488                HandlePos::Point(0),
1489                pos2(150.0, 250.0),
1490                vec![pos2(150.0, 250.0), pos2(300.0, 250.0)],
1491                vec![ChartPoint::new(15.0, 750.0), ChartPoint::new(30.0, 750.0)],
1492            ),
1493            (
1494                DrawingToolType::TimeCycles,
1495                vec![pos2(100.0, 200.0), pos2(300.0, 200.0)],
1496                HandlePos::Point(1),
1497                pos2(50.0, 250.0),
1498                vec![pos2(100.0, 200.0), pos2(50.0, 200.0)],
1499                vec![ChartPoint::new(10.0, 800.0), ChartPoint::new(5.0, 800.0)],
1500            ),
1501            (
1502                DrawingToolType::TimeCycles,
1503                vec![pos2(100.0, 200.0), pos2(300.0, 200.0)],
1504                HandlePos::Middle,
1505                pos2(250.0, 280.0),
1506                vec![pos2(150.0, 280.0), pos2(350.0, 280.0)],
1507                vec![ChartPoint::new(15.0, 720.0), ChartPoint::new(35.0, 720.0)],
1508            ),
1509            (
1510                DrawingToolType::TrendLine,
1511                vec![pos2(100.0, 200.0), pos2(300.0, 400.0)],
1512                HandlePos::Start,
1513                pos2(150.0, 250.0),
1514                vec![pos2(150.0, 250.0), pos2(300.0, 400.0)],
1515                vec![ChartPoint::new(15.0, 750.0), ChartPoint::new(30.0, 600.0)],
1516            ),
1517            (
1518                DrawingToolType::TrendLine,
1519                vec![pos2(100.0, 200.0), pos2(300.0, 400.0)],
1520                HandlePos::Middle,
1521                pos2(250.0, 280.0),
1522                vec![pos2(150.0, 180.0), pos2(350.0, 380.0)],
1523                vec![ChartPoint::new(15.0, 820.0), ChartPoint::new(35.0, 620.0)],
1524            ),
1525            (
1526                DrawingToolType::Rect,
1527                vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1528                HandlePos::TopRight,
1529                pos2(250.0, 80.0),
1530                vec![pos2(100.0, 80.0), pos2(250.0, 300.0)],
1531                vec![ChartPoint::new(10.0, 920.0), ChartPoint::new(25.0, 700.0)],
1532            ),
1533            (
1534                DrawingToolType::GannBox,
1535                vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1536                HandlePos::TopRight,
1537                pos2(250.0, 80.0),
1538                vec![pos2(100.0, 80.0), pos2(250.0, 300.0)],
1539                vec![ChartPoint::new(10.0, 920.0), ChartPoint::new(25.0, 700.0)],
1540            ),
1541            (
1542                DrawingToolType::Triangle,
1543                vec![pos2(0.0, 0.0), pos2(30.0, 0.0), pos2(0.0, 60.0)],
1544                HandlePos::Middle,
1545                pos2(20.0, 30.0),
1546                vec![pos2(10.0, 10.0), pos2(40.0, 10.0), pos2(10.0, 70.0)],
1547                vec![
1548                    ChartPoint::new(1.0, 990.0),
1549                    ChartPoint::new(4.0, 990.0),
1550                    ChartPoint::new(1.0, 930.0),
1551                ],
1552            ),
1553        ];
1554        for (tool, points, handle, to, screen, chart) in cases {
1555            for logarithmic in [false, true] {
1556                let price = |y: f32| {
1557                    if logarithmic {
1558                        1000.0 * (-f64::from(y) / 100.0).exp()
1559                    } else {
1560                        y_to_price(y)
1561                    }
1562                };
1563                let mut drawing = create_test_drawing(tool, points.clone());
1564                for (anchor, point) in drawing.chart_points.iter_mut().zip(&drawing.points) {
1565                    anchor.price = price(point.y);
1566                }
1567                HandleService::update_handle(
1568                    &mut drawing,
1569                    handle,
1570                    to,
1571                    x_to_bar,
1572                    |y: f32| price(y.clamp(0.0, 350.0)),
1573                    |p, from, to| {
1574                        if logarithmic {
1575                            p * (-f64::from(to - from) / 100.0).exp()
1576                        } else {
1577                            p - f64::from(to - from)
1578                        }
1579                    },
1580                );
1581                assert_eq!(drawing.points, screen, "{tool:?} {handle:?}");
1582                let expected = if logarithmic {
1583                    screen
1584                        .iter()
1585                        .map(|p| ChartPoint::new(x_to_bar(p.x), price(p.y)))
1586                        .collect()
1587                } else {
1588                    chart.clone()
1589                };
1590                if logarithmic {
1591                    assert_eq!(drawing.chart_points.len(), expected.len());
1592                    for (got, want) in drawing.chart_points.iter().zip(&expected) {
1593                        assert_eq!(got.bar_idx, want.bar_idx, "{tool:?} {handle:?}");
1594                        assert!(
1595                            (got.price - want.price).abs() < 1e-12 * want.price.abs(),
1596                            "{tool:?} {handle:?}: {got:?}, expected {want:?}"
1597                        );
1598                    }
1599                } else {
1600                    assert_eq!(drawing.chart_points, expected, "{tool:?} {handle:?}");
1601                }
1602            }
1603        }
1604    }
1605}