Skip to main content

egui_charts/widget/
primitives.rs

1//! Host primitives attached to series, and custom series drawn by them.
2
3use super::{Chart, ChartElementId, PointerTarget, SeriesId};
4use crate::chart::coords::ChartMapping;
5use crate::chart::primitives::{Primitive, PrimitiveId, PrimitivePass, PrimitiveView};
6use crate::model::Bar;
7use crate::studies::Indicator;
8use egui::{CursorIcon, Painter, Pos2};
9use std::sync::{Arc, Mutex, MutexGuard};
10
11/// A primitive and the series it is attached to.
12#[derive(Clone)]
13pub(super) struct Attached {
14    pub(super) id: PrimitiveId,
15    pub(super) series: SeriesId,
16    /// Shared with image exports, which draw the same primitives.
17    primitive: Arc<Mutex<dyn Primitive>>,
18    /// Draws the study `series` in place of its plots.
19    pub(super) replaces: bool,
20    /// The series' bars last reported; `None` until `attached` ran.
21    pub(super) seen: Option<Seen>,
22}
23
24/// What identifies the bars of a series: a revision that changes with any bar but the last, the
25/// number of bars and the last bar, bitwise.
26pub(super) type Seen = (
27    u64,
28    usize,
29    Option<(chrono::DateTime<chrono::Utc>, [u64; 5])>,
30);
31
32fn seen(revision: u64, bars: &[Bar]) -> Seen {
33    let last = bars.last().map(|bar| {
34        (
35            bar.time,
36            [bar.open, bar.high, bar.low, bar.close, bar.volume].map(f64::to_bits),
37        )
38    });
39    (revision, bars.len(), last)
40}
41
42impl Attached {
43    pub(super) fn draw(
44        &self,
45        painter: &Painter,
46        view: &PrimitiveView<'_>,
47        pass: PrimitivePass,
48        factor: usize,
49    ) {
50        self.get().draw_merged(painter, view, pass, factor);
51    }
52
53    pub(super) fn get(&self) -> MutexGuard<'_, dyn Primitive + 'static> {
54        // A primitive that panicked in a hook keeps the state it had.
55        self.primitive
56            .lock()
57            .unwrap_or_else(|poisoned| poisoned.into_inner())
58    }
59}
60
61/// A primitive under the pointer and the cursor it asks for.
62#[derive(Clone, Copy)]
63pub(super) struct PrimitiveHit {
64    /// The custom series a renderer draws, or the primitive itself.
65    pub(super) target: PointerTarget,
66    pub(super) cursor: CursorIcon,
67}
68
69impl Chart {
70    /// Attaches `primitive` to `series`, the main series, an added bar series or a study, to
71    /// draw in the pane of the series on its price scale from the next [`show`](Self::show); see
72    /// [`Primitive`]. Several primitives draw in the order they were attached.
73    ///
74    /// Returns `None`, dropping `primitive`, when `series` is not present.
75    pub fn attach_primitive(
76        &mut self,
77        series: SeriesId,
78        primitive: impl Primitive + 'static,
79    ) -> Option<PrimitiveId> {
80        self.attach(series, Arc::new(Mutex::new(primitive)), false)
81    }
82
83    /// Detaches a primitive, calling its [`detached`](Primitive::detached) hook if it was
84    /// attached in a frame. Returns `false` for an unknown or already detached id.
85    pub fn detach_primitive(&mut self, id: PrimitiveId) -> bool {
86        let Some(at) = self.primitives.iter().position(|item| item.id == id) else {
87            return false;
88        };
89        let item = self.primitives.remove(at);
90        if item.seen.is_some() {
91            item.get().detached();
92        }
93        true
94    }
95
96    /// Adds a series type the host defines, and returns its id. `study` supplies its values,
97    /// which the legend, the crosshair, the last-value tag and the autoscale of its pane read as
98    /// for any study, in its own pane unless it is an overlay. `renderer` draws its points in
99    /// place of the study's plots and hit tests them: a hit selects the series and is reported
100    /// as [`ChartElementId::Indicator`]. The series is shown, hidden and removed like a study.
101    pub fn add_custom_series(
102        &mut self,
103        study: Box<dyn Indicator>,
104        renderer: impl Primitive + 'static,
105    ) -> SeriesId {
106        let id = self.add_indicator(study);
107        self.attach(id, Arc::new(Mutex::new(renderer)), true);
108        id
109    }
110
111    fn attach(
112        &mut self,
113        series: SeriesId,
114        primitive: Arc<Mutex<dyn Primitive>>,
115        replaces: bool,
116    ) -> Option<PrimitiveId> {
117        self.primitive_source(series)?;
118        let id = PrimitiveId(self.next_primitive);
119        self.next_primitive += 1;
120        self.primitives.push(Attached {
121            id,
122            series,
123            primitive,
124            replaces,
125            seen: None,
126        });
127        Some(id)
128    }
129
130    /// The bars reported to the primitives of `series` and what identifies them, `None` when the
131    /// series is not present. Studies follow the main bars they are computed from.
132    fn primitive_source(&self, series: SeriesId) -> Option<(Seen, &[Bar])> {
133        let bars = self.state.data().bars.as_slice();
134        let main = (seen(self.state.revision(), bars), bars);
135        if series == SeriesId::MAIN {
136            self.main_series_present.then_some(main)
137        } else if let Some(item) = self.price_series.iter().find(|item| item.id == series) {
138            Some((seen(item.revision, &item.bars), &item.bars))
139        } else {
140            self.study_ids.contains(&series).then_some(main)
141        }
142    }
143
144    /// Runs the lifecycle hooks before a frame is drawn: `detached` for the primitives of
145    /// removed series, `attached` for new ones and `data_changed` when their bars changed.
146    pub(super) fn sync_primitives(&mut self, ctx: &egui::Context) {
147        let mut primitives = std::mem::take(&mut self.primitives);
148        primitives.retain_mut(|item| {
149            let Some((current, bars)) = self.primitive_source(item.series) else {
150                if item.seen.is_some() {
151                    item.get().detached();
152                }
153                return false;
154            };
155            if item.seen != Some(current) {
156                let mut primitive = item.get();
157                if item.seen.is_none() {
158                    primitive.attached(ctx, item.series);
159                }
160                primitive.data_changed(bars);
161                drop(primitive);
162                item.seen = Some(current);
163            }
164            true
165        });
166        // The hooks have no access to the chart, so no primitive was attached meanwhile.
167        self.primitives = primitives;
168    }
169
170    /// Marks every primitive as up to date with this chart's bars, so that an image export
171    /// draws them without running their hooks again.
172    pub(super) fn settle_primitives(&mut self) {
173        for at in 0..self.primitives.len() {
174            let current = self
175                .primitive_source(self.primitives[at].series)
176                .map(|(current, _)| current);
177            self.primitives[at].seen = current;
178        }
179    }
180
181    pub(super) fn primitive_view(
182        &self,
183        series: SeriesId,
184        mapping: ChartMapping,
185    ) -> PrimitiveView<'_> {
186        let (start, end) = self.state.visible_range();
187        PrimitiveView {
188            series,
189            study: self
190                .study_ids
191                .iter()
192                .position(|id| *id == series)
193                .and_then(|idx| self.rendered_studies().indicators().get(idx))
194                .map(|study| study.as_ref()),
195            mapping,
196            bars: self.state.shown(),
197            visible: start..end,
198        }
199    }
200
201    /// The view of a primitive drawn in the main pane, from the main pane's mapping. `None`
202    /// when its series is hidden or a study in a pane of its own.
203    fn main_view(&self, series: SeriesId, coords: &ChartMapping) -> Option<PrimitiveView<'_>> {
204        if self.in_pane(series) {
205            return None;
206        }
207        let mapping = if series == SeriesId::MAIN {
208            self.main_series_visible.then_some(*coords)?
209        } else if let Some(item) = self.price_series.iter().find(|item| item.id == series) {
210            item.visible.then(|| ChartMapping {
211                price: self.series_map(item, coords.rect),
212                ..*coords
213            })?
214        } else {
215            let idx = self.study_ids.iter().position(|id| *id == series)?;
216            let study = self.rendered_studies().indicators().get(idx)?;
217            (study.is_visible()
218                && study.is_overlay()
219                && (!study.is_volume()
220                    || matches!(study.scale(), crate::studies::StudyScale::Shared(_))))
221            .then(|| crate::chart::rendering::study_mapping(study.as_ref(), coords))?
222        };
223        Some(self.primitive_view(series, mapping))
224    }
225
226    /// The primitives drawn in the main pane, with their views.
227    pub(super) fn main_primitives<'a>(
228        &'a self,
229        coords: &'a ChartMapping,
230    ) -> impl DoubleEndedIterator<Item = (&'a Attached, PrimitiveView<'a>)> {
231        self.primitives
232            .iter()
233            .filter_map(move |item| Some((item, self.main_view(item.series, coords)?)))
234    }
235
236    /// The primitives of every visible series, for the time axis the panes share. Those in a
237    /// study pane get the main pane's mapping.
238    pub(super) fn time_axis_primitives<'a>(
239        &'a self,
240        coords: &'a ChartMapping,
241    ) -> impl Iterator<Item = (&'a Attached, PrimitiveView<'a>)> {
242        self.primitives.iter().filter_map(move |item| {
243            let view = self.main_view(item.series, coords).or_else(|| {
244                self.is_series_visible(item.series)
245                    .then(|| self.primitive_view(item.series, *coords))
246            })?;
247            Some((item, view))
248        })
249    }
250
251    /// The primitives attached to `series`.
252    pub(super) fn primitives_of(
253        &self,
254        series: SeriesId,
255    ) -> impl DoubleEndedIterator<Item = &Attached> {
256        self.primitives
257            .iter()
258            .filter(move |item| item.series == series)
259    }
260
261    /// The prices the primitives of `series` add to the fit of its scale.
262    pub(super) fn primitive_prices(&self, series: SeriesId) -> impl Iterator<Item = f64> + '_ {
263        let (start, end) = self.state.visible_range();
264        self.primitives_of(series)
265            .filter_map(move |item| item.get().autoscale(self.state.shown(), start..end))
266            .flat_map(|(min, max)| [min, max])
267    }
268
269    /// The prices the primitives of the main series and of the overlay studies on the main
270    /// scale add to its fit.
271    pub(super) fn main_primitive_prices(&self) -> Vec<f64> {
272        let studies = self.rendered_studies().indicators();
273        let mut prices = Vec::new();
274        if self.main_series_visible {
275            prices.extend(self.primitive_prices(SeriesId::MAIN));
276        }
277        for (idx, id) in self.study_ids.iter().enumerate() {
278            if studies.get(idx).is_some_and(|study| {
279                study.is_visible()
280                    && study.is_overlay()
281                    && study.scale() == crate::studies::StudyScale::Main
282            }) && self.study_autoscale[idx]
283                && !self.config.scale_price_chart_only
284            {
285                prices.extend(self.primitive_prices(*id));
286            }
287        }
288        prices
289    }
290
291    /// Whether a renderer draws the study `series` in place of its plots.
292    pub(super) fn has_renderer(&self, series: SeriesId) -> bool {
293        self.primitives_of(series).any(|item| item.replaces)
294    }
295
296    /// Draws the main pane's primitives in `pass`, clipped to the plot.
297    pub(super) fn paint_primitives(
298        &self,
299        painter: &Painter,
300        coords: &ChartMapping,
301        pass: PrimitivePass,
302    ) {
303        let painter = painter.with_clip_rect(painter.clip_rect().intersect(coords.rect));
304        for (item, view) in self
305            .main_primitives(coords)
306            .filter(|(item, _)| pass != PrimitivePass::Series || !item.replaces)
307        {
308            item.draw(&painter, &view, pass, self.merge_groups.factor());
309        }
310    }
311}
312
313/// The latest attached primitive under `pos`.
314pub(super) fn primitive_at<'a>(
315    primitives: impl DoubleEndedIterator<Item = (&'a Attached, PrimitiveView<'a>)>,
316    pos: Pos2,
317) -> Option<PrimitiveHit> {
318    primitives.rev().find_map(|(item, view)| {
319        let cursor = item.get().hit_test(pos, &view)?;
320        Some(PrimitiveHit {
321            target: if item.replaces {
322                PointerTarget::Element(ChartElementId::Indicator(item.series))
323            } else {
324                PointerTarget::Primitive(item.id)
325            },
326            cursor,
327        })
328    })
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use crate::chart::primitives::{PriceAxisLabel, TimeAxisLabel};
335    use crate::model::Bar;
336    use crate::studies::{CustomIndicator, IndicatorValue};
337    use crate::widget::test_frames::{chart, frame, shapes};
338    use crate::widget::{ChartEvent, SeriesOptions};
339    use egui::{Color32, Event, Shape};
340
341    type Log = Arc<Mutex<Vec<String>>>;
342
343    struct Probe(&'static str, Log);
344
345    impl Primitive for Probe {
346        fn attached(&mut self, _: &egui::Context, _: SeriesId) {
347            self.1.lock().unwrap().push(format!("{} attached", self.0));
348        }
349        fn data_changed(&mut self, bars: &[Bar]) {
350            let line = format!("{} data {}", self.0, bars.len());
351            self.1.lock().unwrap().push(line);
352        }
353        fn detached(&mut self) {
354            self.1.lock().unwrap().push(format!("{} detached", self.0));
355        }
356    }
357
358    #[test]
359    fn hooks_follow_attachment_bar_changes_and_removal() {
360        let ctx = egui::Context::default();
361        let mut chart = chart(&[10.0], &[20.0]);
362        let bars = chart.bars(SeriesId::MAIN).to_vec();
363        let line = chart.add_series(SeriesOptions::default());
364        chart.set_data(line, bars[..50].to_vec());
365        let log = Log::default();
366        let main = chart.attach_primitive(SeriesId::MAIN, Probe("main", log.clone()));
367        chart.attach_primitive(line, Probe("line", log.clone()));
368        assert!(
369            chart
370                .attach_primitive(SeriesId(99), Probe("absent", log.clone()))
371                .is_none()
372        );
373        let take = || std::mem::take(&mut *log.lock().unwrap());
374        frame(&ctx, &mut chart, 0.0, vec![]);
375        frame(&ctx, &mut chart, 0.1, vec![]);
376        assert_eq!(
377            take(),
378            [
379                "main attached",
380                "main data 100",
381                "line attached",
382                "line data 50"
383            ]
384        );
385        chart.update(line, bars[50].clone());
386        frame(&ctx, &mut chart, 0.2, vec![]);
387        assert_eq!(take(), ["line data 51"]);
388        let mut corrected = bars[10].clone();
389        corrected.close = 15.0;
390        chart.update(line, corrected);
391        frame(&ctx, &mut chart, 0.25, vec![]);
392        assert_eq!(take(), ["line data 51"], "a correction before the last bar");
393        let mut last = bars[99].clone();
394        last.close = 15.0;
395        chart.update(SeriesId::MAIN, last);
396        frame(&ctx, &mut chart, 0.3, vec![]);
397        assert_eq!(take(), ["main data 100"]);
398        chart.remove_series(line);
399        frame(&ctx, &mut chart, 0.4, vec![]);
400        assert_eq!(take(), ["line detached"]);
401        assert!(chart.detach_primitive(main.unwrap()));
402        assert!(!chart.detach_primitive(main.unwrap()));
403        assert_eq!(take(), ["main detached"]);
404    }
405
406    const BELOW: Color32 = Color32::from_rgb(1, 2, 3);
407    const SERIES: Color32 = Color32::from_rgb(4, 5, 6);
408    const ABOVE: Color32 = Color32::from_rgb(7, 8, 9);
409
410    /// Fills a corner of the plot in each pass and asks for prices 0 to 500.
411    struct Layers;
412
413    impl Primitive for Layers {
414        fn draw(&self, painter: &Painter, view: &PrimitiveView<'_>, pass: PrimitivePass) {
415            let fill = match pass {
416                PrimitivePass::Below => BELOW,
417                PrimitivePass::Series => SERIES,
418                PrimitivePass::Above => ABOVE,
419            };
420            let corner = egui::Rect::from_min_size(view.mapping.rect.min, egui::vec2(9.0, 9.0));
421            painter.rect_filled(corner, 0.0, fill);
422        }
423        fn autoscale(&self, _: &[Bar], _: std::ops::Range<usize>) -> Option<(f64, f64)> {
424            Some((0.0, 500.0))
425        }
426        fn price_axis_labels(&self, _: &PrimitiveView<'_>) -> Vec<PriceAxisLabel> {
427            vec![PriceAxisLabel {
428                price: 250.0,
429                text: "Level".into(),
430                color: SERIES,
431            }]
432        }
433        fn time_axis_labels(&self, view: &PrimitiveView<'_>) -> Vec<TimeAxisLabel> {
434            vec![TimeAxisLabel {
435                time: view.bars[90].time,
436                text: "A session label much wider than any price on the axis".into(),
437                color: ABOVE,
438            }]
439        }
440    }
441
442    #[test]
443    fn passes_layer_around_series_and_studies_and_widen_the_scale() {
444        let ctx = egui::Context::default();
445        let mut chart = chart(&[10.0], &[20.0]);
446        let study = Color32::from_rgb(10, 11, 12);
447        let study_id = chart.add_indicator(Box::new(
448            CustomIndicator::new("Mid", |bars| {
449                bars.iter()
450                    .map(|bar| IndicatorValue::Single((bar.high + bar.low) / 2.0))
451                    .collect()
452            })
453            .with_color(study),
454        ));
455        let primitive = chart.attach_primitive(SeriesId::MAIN, Layers).unwrap();
456        frame(&ctx, &mut chart, 0.0, vec![]);
457        let shapes = shapes(&ctx, &mut chart, 0.1, vec![]);
458        let first = |found: &dyn Fn(&Shape) -> bool| shapes.iter().position(found).unwrap();
459        let fill =
460            |color| move |shape: &Shape| matches!(shape, Shape::Rect(rect) if rect.fill == color);
461        let candle = first(&fill(chart.config.bullish_color));
462        let line = first(&|shape| match shape {
463            Shape::Path(path) => path.stroke.color == egui::epaint::ColorMode::Solid(study),
464            Shape::LineSegment { stroke, .. } => stroke.color == study,
465            _ => false,
466        });
467        let (below, series, above) = (
468            first(&fill(BELOW)),
469            first(&fill(SERIES)),
470            first(&fill(ABOVE)),
471        );
472        assert!(below < candle && candle < line && line < series && series < above);
473        let range = chart.state().price_scale().range();
474        assert!(range.min <= 0.0 && range.max >= 500.0, "{range:?}");
475        let text = |label: &str| {
476            shapes
477                .iter()
478                .any(|shape| matches!(shape, Shape::Text(text) if text.galley.text() == label))
479        };
480        assert!(text("Level") && text("A session label much wider than any price on the axis"));
481        // A time label is on the time axis and does not size the price axis.
482        assert!(
483            chart.measured_axis_width < 120.0,
484            "{}",
485            chart.measured_axis_width
486        );
487        chart.detach_primitive(primitive);
488        for (owner, price_only, expected) in [
489            (SeriesId::MAIN, true, (-62.5, 562.5)),
490            (study_id, true, (8.75, 21.25)),
491            (study_id, false, (-62.5, 562.5)),
492        ] {
493            let primitive = chart.attach_primitive(owner, Layers).unwrap();
494            chart.config_mut().scale_price_chart_only = price_only;
495            let painted = super::super::test_frames::shapes(&ctx, &mut chart, 0.2, vec![]);
496            let range = chart.state().price_scale().range();
497            assert_eq!((range.min, range.max), expected);
498            assert!(
499                painted.iter().any(fill(ABOVE)),
500                "the primitive still paints"
501            );
502            chart.detach_primitive(primitive);
503        }
504    }
505
506    #[test]
507    fn bar_series_primitives_follow_pane_moves_and_layer_around_the_bars() {
508        use crate::model::ChartType;
509        use crate::widget::PaneId;
510
511        let ctx = egui::Context::default();
512        let mut chart = chart(&[10.0], &[20.0]);
513        let color = Color32::from_rgb(10, 11, 12);
514        let id = chart.add_series(SeriesOptions {
515            chart_type: ChartType::Candles,
516            up_color: color,
517            ..Default::default()
518        });
519        chart.set_data(id, chart.bars(SeriesId::MAIN).to_vec());
520        chart.attach_primitive(id, Layers);
521        let pane = chart.add_pane();
522        assert!(chart.set_preserve_empty_pane(pane, true));
523        for destination in [pane, PaneId::MAIN, pane] {
524            assert!(chart.move_series_to_pane(id, destination));
525            let painted = shapes(&ctx, &mut chart, 0.0, vec![]);
526            let map = chart.series_price_map(id).unwrap();
527            assert!(map.range().contains(map.transform().to_scale(500.0)));
528            if destination != PaneId::MAIN {
529                assert!(chart.last_price_map.range().max < 100.0);
530            }
531            let rects = |fill| {
532                painted
533                    .iter()
534                    .enumerate()
535                    .filter_map(move |(at, shape)| match shape {
536                        Shape::Rect(rect)
537                            if rect.fill == fill
538                                && (fill == color || rect.rect.size() == egui::vec2(9.0, 9.0)) =>
539                        {
540                            Some((at, rect.rect))
541                        }
542                        _ => None,
543                    })
544            };
545            let (below, rect) = rects(BELOW).next().unwrap();
546            assert_eq!(rects(BELOW).count(), 1, "drawn only in its own pane");
547            let candle = rects(color).next().unwrap().0;
548            let series = rects(SERIES).next().unwrap().0;
549            let above = rects(ABOVE).next().unwrap().0;
550            assert!(
551                below < candle && candle < series && series < above,
552                "{destination:?}: {below} < {candle} < {series} < {above}"
553            );
554            let pane_rect = chart
555                .pane_layout
556                .panes
557                .iter()
558                .find(|item| item.id == destination)
559                .unwrap()
560                .rect;
561            assert!(pane_rect.contains_rect(rect));
562        }
563        chart.set_series_visible(id, false);
564        assert!(
565            !shapes(&ctx, &mut chart, 0.2, vec![])
566                .iter()
567                .any(|shape| matches!(shape, Shape::Rect(rect) if rect.fill == BELOW))
568        );
569    }
570
571    /// Draws a dot at each value of its study, fills a corner of the plot above everything, and
572    /// claims the whole plot for the pointer.
573    struct Dots;
574
575    impl Primitive for Dots {
576        fn draw(&self, painter: &Painter, view: &PrimitiveView<'_>, pass: PrimitivePass) {
577            if pass == PrimitivePass::Above {
578                let corner = egui::Rect::from_min_size(view.mapping.rect.min, egui::vec2(9.0, 9.0));
579                painter.rect_filled(corner, 0.0, ABOVE);
580            }
581            for index in view
582                .visible
583                .clone()
584                .filter(|_| pass == PrimitivePass::Series)
585            {
586                if let Some(value) = view.study.and_then(|study| study.plot_value(0, index)) {
587                    let centre = Pos2::new(
588                        view.index_to_x(index as f64),
589                        view.mapping.price_to_y(value),
590                    );
591                    painter.circle_filled(centre, 2.0, SERIES);
592                }
593            }
594        }
595        fn hit_test(&self, pos: Pos2, view: &PrimitiveView<'_>) -> Option<CursorIcon> {
596            view.mapping
597                .rect
598                .contains(pos)
599                .then_some(CursorIcon::PointingHand)
600        }
601    }
602
603    #[test]
604    fn custom_series_draws_and_hit_tests_in_place_of_its_plots() {
605        let ctx = egui::Context::default();
606        let mut chart = chart(&[10.0], &[20.0]);
607        let color = Color32::from_rgb(10, 11, 12);
608        let id = chart.add_custom_series(
609            Box::new(
610                CustomIndicator::new("Range", |bars| {
611                    bars.iter()
612                        .map(|bar| IndicatorValue::Single(bar.high - bar.low))
613                        .collect()
614                })
615                .with_overlay(false)
616                .with_color(color),
617            ),
618            Dots,
619        );
620        #[cfg(feature = "drawings")]
621        {
622            let mut drawing = crate::drawings::Drawing::with_color(
623                1,
624                crate::drawings::DrawingToolType::HorizontalLine,
625                [255, 0, 0, 255],
626            );
627            drawing.chart_points = vec![crate::drawings::ChartPoint::new(50.0, 10.0)];
628            drawing.pane = chart.series_pane(id).map(|pane| pane.0);
629            chart.drawings_mut().drawings.push(drawing);
630        }
631        frame(&ctx, &mut chart, 0.0, vec![]);
632        let shapes = shapes(&ctx, &mut chart, 0.1, vec![]);
633        #[cfg(feature = "drawings")]
634        {
635            // The last pass of a study pane is above the pane's drawings.
636            let red = |shape: &Shape| match shape {
637                Shape::Path(path) => {
638                    path.stroke.color == egui::epaint::ColorMode::Solid(Color32::RED)
639                }
640                Shape::LineSegment { stroke, .. } => stroke.color == Color32::RED,
641                // Unchanged drawings paint their cached triangles in the same place in the list.
642                Shape::Mesh(mesh) => mesh.triangles().any(|triangle| {
643                    triangle
644                        .into_iter()
645                        .all(|index| mesh.vertices[index as usize].color == Color32::RED)
646                }),
647                _ => false,
648            };
649            let above = |shape: &Shape| matches!(shape, Shape::Rect(rect) if rect.fill == ABOVE);
650            let drawing = shapes
651                .iter()
652                .position(red)
653                .expect("the red drawing is painted");
654            let last_pass = shapes
655                .iter()
656                .position(above)
657                .expect("the last pass is painted");
658            assert!(
659                drawing < last_pass,
660                "the last pass must paint above the drawing"
661            );
662        }
663        assert!(
664            shapes
665                .iter()
666                .any(|shape| matches!(shape, Shape::Circle(dot) if dot.fill == SERIES))
667        );
668        assert!(!shapes.iter().any(|shape| match shape {
669            Shape::Path(path) => path.stroke.color == egui::epaint::ColorMode::Solid(color),
670            Shape::LineSegment { stroke, .. } => stroke.color == color,
671            _ => false,
672        }));
673        let pane = chart.last_rendered_indicator_panes[0].chart_rect;
674        // Away from the values, where the study's own line hit test would miss.
675        let pos = Pos2::new(pane.center().x, pane.bottom() - 2.0);
676        chart.drain_events().for_each(drop);
677        frame(&ctx, &mut chart, 0.2, vec![Event::PointerMoved(pos)]);
678        let events: Vec<_> = chart.drain_events().collect();
679        let Some(ChartEvent::CrosshairMoved(Some(pointer))) = events.last() else {
680            panic!("{events:?}")
681        };
682        assert_eq!(
683            pointer.target,
684            Some(PointerTarget::Element(ChartElementId::Indicator(id)))
685        );
686        let click = |pressed| Event::PointerButton {
687            pos,
688            button: egui::PointerButton::Primary,
689            pressed,
690            modifiers: Default::default(),
691        };
692        frame(&ctx, &mut chart, 0.3, vec![click(true), click(false)]);
693        assert_eq!(
694            chart.selection.selected_id(),
695            Some(ChartElementId::Indicator(id))
696        );
697        let pane = chart.series_pane(id).unwrap();
698        chart.set_preserve_empty_pane(pane, true);
699        #[cfg(feature = "drawings")]
700        chart.drawings_mut().drawings.clear();
701        let cover = chart.add_indicator(Box::new(CustomIndicator::new("Cover", |bars| {
702            vec![IndicatorValue::Single(15.0); bars.len()]
703        })));
704        for (step, destination) in [super::super::PaneId::MAIN, pane].into_iter().enumerate() {
705            chart.move_series_to_pane(id, destination);
706            chart.move_series_to_pane(cover, destination);
707            for (layer, (order, expected)) in [
708                (super::super::VisualOrder::SendToBack, cover),
709                (super::super::VisualOrder::BringToFront, id),
710            ]
711            .into_iter()
712            .enumerate()
713            {
714                chart.drain_events().for_each(drop);
715                chart.reorder_indicator(id, order);
716                let time = 1.0 + step as f64 + layer as f64 * 0.2;
717                frame(&ctx, &mut chart, time, vec![]);
718                let map = if destination == super::super::PaneId::MAIN {
719                    chart.get_chart_mapping().price
720                } else {
721                    chart.series_price_map(cover).unwrap()
722                };
723                let pos = Pos2::new(chart.get_chart_mapping().idx_to_x(70), map.price_to_y(15.0));
724                frame(&ctx, &mut chart, time + 0.1, vec![Event::PointerMoved(pos)]);
725                assert!(chart.drain_events().any(|event| matches!(event,
726                    ChartEvent::CrosshairMoved(Some(pointer))
727                        if pointer.target == Some(PointerTarget::Element(ChartElementId::Indicator(expected)))
728                )), "{destination:?} {order:?}");
729            }
730        }
731    }
732}