Skip to main content

egui_charts/config/drawing_tool/
behavior.rs

1//! Behavioral queries for [`DrawingToolType`].
2//!
3//! Provides methods that describe *how* each tool type behaves: whether it is
4//! a cursor, what interaction mode it uses, and how many points it requires.
5
6use super::{DrawingInteractionMode, DrawingToolType};
7
8impl DrawingToolType {
9    /// Returns `true` if this tool is a cursor/utility tool rather than a
10    /// drawing tool.
11    ///
12    /// Cursor tools (`CrossCursor`, `DotCursor`, `ArrowCursor`, `Eraser`)
13    /// do not create `Drawing` objects. They modify
14    /// the cursor appearance or provide utility functions (e.g., erasing).
15    #[inline]
16    pub fn is_cursor(&self) -> bool {
17        matches!(
18            self,
19            Self::CrossCursor | Self::DotCursor | Self::ArrowCursor | Self::Eraser
20        )
21    }
22
23    /// Whether the renderer paints a filled area that takes the drawing's fill colour.
24    pub fn supports_fill(&self) -> bool {
25        !matches!(
26            self,
27            Self::CrossCursor
28                | Self::DotCursor
29                | Self::ArrowCursor
30                | Self::Eraser
31                | Self::TrendLine
32                | Self::TrendAngle
33                | Self::InfoLine
34                | Self::ABCDPattern
35                | Self::ThreeDrivesPattern
36                | Self::ElliottImpulse
37                | Self::ElliottCorrection
38                | Self::ElliottTriangle
39                | Self::ElliottDoubleCombo
40                | Self::ElliottTripleCombo
41                | Self::GannFan
42                | Self::GannBox
43                | Self::GannSquare
44                | Self::GannFixed
45                | Self::Ray
46                | Self::ExtendedLine
47                | Self::HorizontalLine
48                | Self::HorizontalRay
49                | Self::VerticalLine
50                | Self::CrossLine
51                | Self::FibonacciRetracement
52                | Self::FibonacciExtension
53                | Self::FibonacciTimeZones
54                | Self::TrendBasedFibTime
55                | Self::FibonacciCircles
56                | Self::FibonacciSpiral
57                | Self::FibonacciSpeedResistanceArcs
58                | Self::FibonacciWedge
59                // Forks fill their bands from their level settings.
60                | Self::Pitchfork
61                | Self::SchiffPitchfork
62                | Self::ModifiedSchiffPitchfork
63                | Self::InsidePitchfork
64                | Self::Pitchfan
65                | Self::GhostFeed
66                | Self::BarsPattern
67                | Self::LongPos
68                | Self::ShortPos
69                | Self::AnchoredVWAP
70                | Self::FixedRangeVolumeProfile
71                | Self::AnchoredVolumeProfile
72                | Self::Brush
73                | Self::Highlighter
74                | Self::ArrowMarker
75                | Self::Arrow
76                | Self::Path
77                | Self::CyclicLines
78                | Self::SineLine
79                // Text boxes are coloured with their background, in the Text tab.
80                | Self::TextLabel
81                | Self::AnchoredText
82                | Self::AnchoredNote
83                | Self::FontIcon
84                | Self::ArrowMarkUp
85                | Self::ArrowMarkDown
86                | Self::ArrowMarkLeft
87                | Self::ArrowMarkRight
88                | Self::Sticker
89        )
90    }
91
92    /// Whether the drawing's left and right extension flags change its geometry.
93    pub fn supports_extend(&self) -> bool {
94        matches!(
95            self,
96            Self::TrendLine
97                | Self::Ray
98                | Self::ExtendedLine
99                | Self::Rect
100                | Self::InfoLine
101                | Self::TrendAngle
102                | Self::Arrow
103                | Self::ParallelChannel
104                | Self::FlatTopBottom
105                | Self::DisjointChannel
106                | Self::PriceRange
107                | Self::DateAndPriceRange
108                | Self::Pitchfork
109                | Self::SchiffPitchfork
110                | Self::ModifiedSchiffPitchfork
111                | Self::InsidePitchfork
112                | Self::RegressionTrend
113        )
114    }
115
116    /// Whether the drawing is a text annotation: its text is its content and its colour is the
117    /// text's.
118    pub(crate) fn is_text_annotation(&self) -> bool {
119        matches!(
120            self,
121            Self::TextLabel
122                | Self::AnchoredText
123                | Self::Note
124                | Self::AnchoredNote
125                | Self::Callout
126                | Self::FlagNote
127                | Self::Table
128                | Self::Tweet
129                | Self::Idea
130        )
131    }
132
133    /// Whether the renderer displays the drawing's editable text.
134    pub fn supports_text(&self) -> bool {
135        self.is_text_annotation()
136            || matches!(
137                self,
138                Self::PriceNote
139                    | Self::Comment
140                    | Self::Signpost
141                    | Self::TrendLine
142                    | Self::Ray
143                    | Self::InfoLine
144                    | Self::ExtendedLine
145                    | Self::HorizontalLine
146                    | Self::HorizontalRay
147                    | Self::VerticalLine
148                    | Self::CrossLine
149                    | Self::ParallelChannel
150                    | Self::RegressionTrend
151                    | Self::FlatTopBottom
152                    | Self::DisjointChannel
153                    | Self::Rect
154                    | Self::Circle
155                    | Self::Ellipse
156                    | Self::Arrow
157                    | Self::ArrowMarker
158                    | Self::ArrowMarkUp
159                    | Self::ArrowMarkDown
160                    | Self::ArrowMarkLeft
161                    | Self::ArrowMarkRight
162            )
163    }
164
165    /// Whether the renderer draws the drawing's line start and line end styles.
166    pub fn supports_line_ends(&self) -> bool {
167        matches!(
168            self,
169            Self::TrendLine
170                | Self::Ray
171                | Self::ExtendedLine
172                | Self::InfoLine
173                | Self::TrendAngle
174                | Self::HorizontalLine
175                | Self::HorizontalRay
176                | Self::VerticalLine
177                | Self::Arrow
178                | Self::Path
179                | Self::Polyline
180        )
181    }
182
183    /// Whether the renderer draws a middle line when the drawing's middle line flag is on.
184    pub fn supports_middle_line(&self) -> bool {
185        matches!(self, Self::Rect | Self::ParallelChannel)
186    }
187
188    /// Whether a double click edits the drawing's text in place instead of opening its settings.
189    pub fn edits_text_inline(&self) -> bool {
190        self.is_text_annotation()
191            || matches!(
192                self,
193                Self::Image | Self::PriceNote | Self::Comment | Self::Signpost
194            )
195    }
196
197    /// Returns the [`DrawingInteractionMode`] that governs how mouse input is
198    /// captured when creating a drawing of this type.
199    ///
200    /// See [`DrawingInteractionMode`] for details on each mode.
201    pub fn interaction_mode(&self) -> DrawingInteractionMode {
202        match self {
203            // Cursors - not drawing tools
204            Self::CrossCursor | Self::DotCursor | Self::ArrowCursor | Self::Eraser => {
205                DrawingInteractionMode::NotDrawing
206            }
207
208            // Single-click tools
209            Self::LongPos
210            | Self::ShortPos
211            | Self::HorizontalLine
212            | Self::VerticalLine
213            | Self::CrossLine
214            | Self::HorizontalRay
215            | Self::TextLabel
216            | Self::AnchoredText
217            | Self::AnchoredNote
218            | Self::PriceLabel
219            | Self::FlagNote
220            | Self::ArrowMarkUp
221            | Self::ArrowMarkDown
222            | Self::ArrowMarkLeft
223            | Self::ArrowMarkRight
224            | Self::Sticker
225            | Self::AnchoredVWAP
226            | Self::AnchoredVolumeProfile
227            | Self::Table
228            | Self::Comment
229            | Self::FontIcon
230            | Self::Image
231            | Self::Tweet
232            | Self::Idea => DrawingInteractionMode::SingleClick,
233
234            // Drag-to-draw tools also accept two successive clicks.
235            Self::Rect
236            | Self::Circle
237            | Self::Ellipse
238            | Self::Measure
239            | Self::PriceRange
240            | Self::DateRange
241            | Self::DateAndPriceRange
242            | Self::GannBox
243            | Self::GannSquare
244            | Self::GannFixed
245            | Self::Forecast
246            | Self::FixedRangeVolumeProfile
247            | Self::FibonacciRetracement
248            | Self::FibonacciSpeedResistanceArcs
249            | Self::FibonacciTimeZones
250            | Self::FibonacciCircles
251            | Self::FibonacciSpeedFan
252            | Self::FibonacciSpiral
253            | Self::GannFan
254            | Self::RegressionTrend
255            | Self::CyclicLines
256            | Self::TimeCycles
257            | Self::SineLine
258            | Self::Callout
259            | Self::Arrow => DrawingInteractionMode::DragToDraw,
260
261            // Continuous drawing tools
262            Self::Brush | Self::Highlighter => DrawingInteractionMode::ContinuousDraw,
263
264            // Multi-point tools
265            Self::RotatedRect
266            | Self::Arc
267            | Self::Sector
268            | Self::FibonacciExtension
269            | Self::FibonacciWedge
270            | Self::TrendBasedFibTime
271            | Self::Pitchfan
272            | Self::Curve
273            | Self::DoubleCurve
274            | Self::ProjectionTool
275            | Self::Polyline
276            | Self::Path
277            | Self::Triangle
278            | Self::ParallelChannel
279            | Self::FlatTopBottom
280            | Self::DisjointChannel
281            | Self::FibonacciChannel
282            | Self::Pitchfork
283            | Self::SchiffPitchfork
284            | Self::ModifiedSchiffPitchfork
285            | Self::InsidePitchfork
286            | Self::XABCDPattern
287            | Self::CypherPattern
288            | Self::HeadAndShoulders
289            | Self::ABCDPattern
290            | Self::TrianglePattern
291            | Self::ThreeDrivesPattern
292            | Self::ElliottImpulse
293            | Self::ElliottCorrection
294            | Self::ElliottTriangle
295            | Self::ElliottDoubleCombo
296            | Self::ElliottTripleCombo
297            | Self::GhostFeed => DrawingInteractionMode::MultiPoint,
298
299            // Click-click tools
300            Self::BarsPattern
301            | Self::ArrowMarker
302            | Self::Note
303            | Self::Signpost
304            | Self::PriceNote
305            | Self::TrendLine
306            | Self::Ray
307            | Self::InfoLine
308            | Self::ExtendedLine
309            | Self::TrendAngle => DrawingInteractionMode::ClickClick,
310        }
311    }
312
313    /// Anchors a drawing of this tool needs to be drawn and selected: what its renderer reads, not
314    /// what the toolbar collects, so a drawing finished early or saved by an earlier release
315    /// still counts. Tools without a meaningful partial construction require all their
316    /// geometry anchors; the other multi-point tools can draw partial paths.
317    #[cfg(feature = "drawings")]
318    pub(crate) fn anchors_needed(&self) -> usize {
319        match self {
320            Self::ParallelChannel
321            | Self::RotatedRect
322            | Self::FibonacciWedge
323            | Self::Pitchfork
324            | Self::SchiffPitchfork
325            | Self::ModifiedSchiffPitchfork
326            | Self::InsidePitchfork
327            | Self::Pitchfan
328            | Self::Sector => 3,
329            Self::DoubleCurve => 4,
330            Self::Triangle
331            | Self::FlatTopBottom
332            | Self::DisjointChannel
333            | Self::Polyline
334            | Self::Path
335            | Self::GhostFeed
336            | Self::XABCDPattern
337            | Self::CypherPattern
338            | Self::HeadAndShoulders
339            | Self::ABCDPattern
340            | Self::TrianglePattern
341            | Self::ThreeDrivesPattern
342            | Self::ElliottImpulse
343            | Self::ElliottCorrection
344            | Self::ElliottDoubleCombo
345            | Self::ElliottTripleCombo => 1,
346            Self::ElliottTriangle => 6,
347            // Saved before the label had its own anchor.
348            Self::Note | Self::Signpost => 1,
349            _ if self.interaction_mode() == DrawingInteractionMode::SingleClick => 1,
350            _ => 2,
351        }
352    }
353
354    /// Returns the number of points placed to complete a drawing of this type, or `None` for
355    /// tools with no fixed count (`Polyline`, `Path`, `Brush`, cursors). Some tools store more
356    /// anchors than they are placed with: a long or short position adds its target and stop,
357    /// three anchors in all, and a curve and a double curve add their controls between their
358    /// ends, three and four. A bars pattern adds its copy anchor, three anchors in all.
359    ///
360    /// | Mode | Typical count |
361    /// |---|---|
362    /// | `SingleClick` | 1 |
363    /// | `ClickClick` / `DragToDraw` | 2 |
364    /// | `MultiPoint` | 2 to 7 depending on tool |
365    /// | `ContinuousDraw` | `None` (unlimited) |
366    /// | `NotDrawing` | `None` |
367    pub fn required_points(&self) -> Option<usize> {
368        match self.interaction_mode() {
369            DrawingInteractionMode::SingleClick => Some(1),
370            DrawingInteractionMode::ClickClick | DrawingInteractionMode::DragToDraw => Some(2),
371            DrawingInteractionMode::MultiPoint => {
372                // Multi-point tools have varying requirements
373                match self {
374                    Self::Curve | Self::DoubleCurve => Some(2),
375                    Self::FibonacciExtension
376                    | Self::FibonacciWedge
377                    | Self::TrendBasedFibTime
378                    | Self::Pitchfan
379                    | Self::Arc
380                    | Self::Sector
381                    | Self::RotatedRect
382                    | Self::ProjectionTool
383                    | Self::FlatTopBottom
384                    | Self::DisjointChannel
385                    | Self::FibonacciChannel
386                    | Self::Triangle
387                    | Self::ParallelChannel
388                    | Self::Pitchfork
389                    | Self::SchiffPitchfork
390                    | Self::ModifiedSchiffPitchfork
391                    | Self::InsidePitchfork => Some(3),
392                    Self::ABCDPattern
393                    | Self::ElliottCorrection
394                    | Self::ElliottDoubleCombo
395                    | Self::TrianglePattern => Some(4),
396                    Self::XABCDPattern | Self::CypherPattern => Some(5),
397                    Self::ElliottImpulse | Self::ElliottTriangle | Self::ElliottTripleCombo => {
398                        Some(6)
399                    }
400                    Self::HeadAndShoulders | Self::ThreeDrivesPattern => Some(7),
401                    // Polyline, Path and Ghost Feed have no fixed limit
402                    _ => None,
403                }
404            }
405            DrawingInteractionMode::ContinuousDraw => None,
406            DrawingInteractionMode::NotDrawing => None,
407        }
408    }
409}
410
411#[cfg(all(test, feature = "drawings"))]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn placement_collects_every_construction_anchor() {
417        use DrawingToolType::*;
418        for (tool, mode, count) in [
419            (FibonacciExtension, DrawingInteractionMode::MultiPoint, 3),
420            (TrendBasedFibTime, DrawingInteractionMode::MultiPoint, 3),
421            (FibonacciWedge, DrawingInteractionMode::MultiPoint, 3),
422            (Pitchfan, DrawingInteractionMode::MultiPoint, 3),
423            (Arc, DrawingInteractionMode::MultiPoint, 3),
424            (Sector, DrawingInteractionMode::MultiPoint, 3),
425            (ArrowMarkLeft, DrawingInteractionMode::SingleClick, 1),
426            (ArrowMarkRight, DrawingInteractionMode::SingleClick, 1),
427            (Sticker, DrawingInteractionMode::SingleClick, 1),
428            (RotatedRect, DrawingInteractionMode::MultiPoint, 3),
429            (ProjectionTool, DrawingInteractionMode::MultiPoint, 3),
430            (FlatTopBottom, DrawingInteractionMode::MultiPoint, 3),
431            (DisjointChannel, DrawingInteractionMode::MultiPoint, 3),
432            (FibonacciChannel, DrawingInteractionMode::MultiPoint, 3),
433            (Curve, DrawingInteractionMode::MultiPoint, 2),
434            (DoubleCurve, DrawingInteractionMode::MultiPoint, 2),
435            (TrianglePattern, DrawingInteractionMode::MultiPoint, 4),
436            (ThreeDrivesPattern, DrawingInteractionMode::MultiPoint, 7),
437            (ElliottDoubleCombo, DrawingInteractionMode::MultiPoint, 4),
438            (ElliottTriangle, DrawingInteractionMode::MultiPoint, 6),
439            (ElliottTripleCombo, DrawingInteractionMode::MultiPoint, 6),
440            (Note, DrawingInteractionMode::ClickClick, 2),
441            (Signpost, DrawingInteractionMode::ClickClick, 2),
442            (PriceNote, DrawingInteractionMode::ClickClick, 2),
443            (ArrowMarker, DrawingInteractionMode::ClickClick, 2),
444            (LongPos, DrawingInteractionMode::SingleClick, 1),
445            (ShortPos, DrawingInteractionMode::SingleClick, 1),
446            (GhostFeed, DrawingInteractionMode::MultiPoint, 5),
447        ] {
448            assert_eq!(tool.interaction_mode(), mode, "{tool:?}");
449            let required = if tool == GhostFeed { None } else { Some(count) };
450            assert_eq!(tool.required_points(), required, "{tool:?}");
451            let mut manager = crate::drawings::DrawingManager::new();
452            manager.start_drawing_with_coords(
453                tool,
454                egui::pos2(10.0, 500.0),
455                |x| x,
456                |y| 1000.0 - f64::from(y),
457            );
458            for i in 1..count {
459                assert!(manager.drawings.is_empty(), "{tool:?} completed too soon");
460                manager.add_point_with_coords(
461                    egui::pos2(10.0 + i as f32 * 100.0, 400.0),
462                    |x| x,
463                    |y| 1000.0 - f64::from(y),
464                );
465            }
466            if required.is_none() {
467                assert!(manager.drawings.is_empty(), "{tool:?} completed too soon");
468                manager.complete_curr_drawing();
469            }
470            assert_eq!(manager.drawings.len(), 1, "{tool:?}");
471            // A curve placed by its ends stores the bend it is drawn with between them: the
472            // curve's control 0.3 of the chord to the side of its middle, the double curve's at
473            // the chord's thirds, half the chord to either side.
474            let stored: &[(f32, f64)] = match tool {
475                Curve => &[(10.0, 500.0), (90.0, 520.0), (110.0, 600.0)],
476                DoubleCurve => &[
477                    (10.0, 500.0),
478                    (280.0 / 3.0, 1450.0 / 3.0),
479                    (80.0 / 3.0, 1850.0 / 3.0),
480                    (110.0, 600.0),
481                ],
482                _ => &[],
483            };
484            if !stored.is_empty() {
485                let points = &manager.drawings[0].chart_points;
486                assert_eq!(points.len(), stored.len(), "{tool:?}");
487                for (point, &(bar, price)) in points.iter().zip(stored) {
488                    assert!(
489                        (point.bar_idx - bar).abs() < 1e-3 && (point.price - price).abs() < 1e-3,
490                        "{tool:?}: {point:?}"
491                    );
492                }
493            }
494
495            if required.is_none() {
496                assert_eq!(manager.drawings[0].chart_points.len(), count, "{tool:?}");
497            }
498            // Position and channel handles can sit away from their stored anchors;
499            // `HandleService`'s `get_handles_by_tool` covers them.
500            if matches!(
501                tool,
502                LongPos | ShortPos | ParallelChannel | FlatTopBottom | DisjointChannel
503            ) {
504                continue;
505            }
506            let drawing = &manager.drawings[0];
507            let handles = crate::drawings::services::DrawingInteraction::new().get_handles(drawing);
508            for point in &drawing.points {
509                assert!(
510                    handles.iter().any(|(_, p)| p == point),
511                    "{tool:?}: no handle for {point:?}"
512                );
513            }
514        }
515    }
516}