Skip to main content

egui_charts/widget/pane/render/
mod.rs

1//! Rendering with separate panes above and below the price pane.
2
3use crate::chart::indicators::{
4    SelectionDotConfig, hit_test_pane_indicator, render_indicator_selection_dots,
5};
6use crate::chart::selection::ChartElementId;
7use crate::chart::series::calculate_dot_interval;
8use crate::localization as l10n;
9use crate::studies::{CustomIndicator, Indicator};
10use crate::theme::ThemeExt;
11use crate::tokens::DESIGN_TOKENS;
12use crate::widget::{
13    Chart, IndicatorCoordParams, IndicatorPane, IndicatorPaneConfig, PaneId, PaneInteraction,
14};
15use egui::{Pos2, Rect, Response, Sense, Ui};
16
17/// Information about a rendered indicator pane, used for hit testing and coordinate mapping.
18///
19/// After calling [`Chart::show`], each visible separate-pane indicator
20/// (RSI, MACD, etc.) produces a `RenderedIndicatorPane` entry stored in the chart.
21/// Platform code can use these to implement click-on-indicator-line selection,
22/// tooltip display, or other interactive features.
23///
24/// Retrieve with [`Chart::get_rendered_indicator_panes`].
25#[derive(Clone, Debug)]
26#[non_exhaustive]
27pub struct RenderedIndicatorPane {
28    /// Position of the study in [`Chart::indicators`] when the pane was drawn. After a pane's
29    /// close button removed its study, that pane stays in the list until the next frame, and it
30    /// and the panes after it point one position too far.
31    pub indicator_idx: usize,
32    /// Full panel rect (including y-axis labels)
33    pub panel_rect: Rect,
34    /// Chart drawing area rect (excluding y-axis labels)
35    pub chart_rect: Rect,
36    /// Y-axis minimum value
37    pub y_min: f64,
38    /// Y-axis maximum value
39    pub y_max: f64,
40    /// Coordinate parameters for x-axis calculation
41    pub coords: IndicatorCoordParams,
42    /// Full mapping, including manual ranges, scale modes and inversion.
43    pub mapping: crate::chart::coords::ChartMapping,
44}
45
46impl Chart {
47    /// Turns the heights requested for the `expanded` panes into stretch factors that give each
48    /// its height out of `total`, leaving the others the space they had relative to each other.
49    fn apply_pane_heights(&mut self, expanded: &[usize], total: f32) {
50        let panes = &mut self.pane_layout.panes;
51        let requested: f32 = expanded.iter().filter_map(|idx| panes[*idx].height).sum();
52        if requested <= 0.0 {
53            return;
54        }
55        let others: Vec<_> = expanded
56            .iter()
57            .filter(|idx| panes[**idx].height.is_none())
58            .collect();
59        let shared: f32 = others.iter().map(|idx| panes[**idx].stretch).sum();
60        let scale = if others.is_empty() {
61            1.0
62        } else {
63            shared / (total - requested).max(40.0 * others.len() as f32)
64        };
65        for idx in expanded {
66            if let Some(height) = panes[*idx].height.take() {
67                panes[*idx].stretch = height * scale;
68            }
69        }
70    }
71
72    /// Renders the price pane and every other drawn pane in their order, sharing the height by
73    /// stretch factor, with x-axes aligned to the price pane and one time axis below them all.
74    pub(in crate::widget) fn show_with_panes(
75        &mut self,
76        ui: &mut Ui,
77        footprint: Option<(
78            &crate::data::Footprint,
79            &crate::chart::overlays::FootprintConfig,
80        )>,
81    ) -> Response {
82        // Panes are stacked edge to edge with explicit gaps; restore the host spacing after.
83        let host_spacing = std::mem::take(&mut ui.spacing_mut().item_spacing);
84
85        let available = ui.available_rect_before_wrap();
86        let maximized = self.pane_layout.maximized;
87        // The drawn panes from top to bottom, the price pane included, and whether each is
88        // collapsed.
89        let mut panes: Vec<_> = self
90            .pane_layout
91            .panes
92            .iter()
93            .enumerate()
94            .filter(|(_, pane)| {
95                pane.id == PaneId::MAIN
96                    || self.pane_shown(pane) && maximized.is_none_or(|id| id == pane.id)
97            })
98            .map(|(idx, pane)| (idx, pane.id, false))
99            .collect();
100        let pane_count = panes.len().saturating_sub(1);
101        let axis_height = self.time_axis_height().min(available.height());
102        let gap = if maximized.is_some() {
103            0.0
104        } else {
105            DESIGN_TOKENS
106                .spacing
107                .hairline
108                .min((available.height() - axis_height).max(0.0) / pane_count.max(1) as f32)
109        };
110        let usable_height = (available.height() - axis_height - gap * pane_count as f32).max(0.0);
111        let auto_collapse = usable_height / (3.0 + pane_count as f32) < 40.0;
112        let row_height = 24.0_f32.min(usable_height / pane_count.max(1) as f32);
113        let mut expanded_budget = ((usable_height - row_height * pane_count as f32 - 40.0)
114            / (40.0 - row_height))
115            .max(0.0) as usize;
116        for (idx, id, collapsed) in &mut panes {
117            if *id == PaneId::MAIN {
118                continue;
119            }
120            *collapsed = maximized.is_none()
121                && (self.pane_layout.panes[*idx]
122                    .collapsed
123                    .unwrap_or(auto_collapse)
124                    || expanded_budget == 0);
125            if !*collapsed {
126                expanded_budget = expanded_budget.saturating_sub(1);
127            }
128        }
129        let collapsed_count = panes.iter().filter(|(_, _, collapsed)| *collapsed).count();
130        let remaining = (usable_height - row_height * collapsed_count as f32).max(0.0);
131        let expanded: Vec<_> = panes
132            .iter()
133            .filter(|(_, _, collapsed)| !collapsed)
134            .map(|(idx, _, _)| *idx)
135            .collect();
136        if maximized.is_none() && remaining > 0.0 {
137            self.apply_pane_heights(&expanded, remaining);
138        }
139        let stretch = |idx: &usize| self.pane_layout.panes[*idx].stretch;
140        let total: f32 = expanded.iter().map(stretch).sum();
141        let pane_total: f32 = expanded
142            .iter()
143            .filter(|idx| self.pane_layout.panes[**idx].id != PaneId::MAIN)
144            .map(stretch)
145            .sum();
146        let mut desired: Vec<_> = panes
147            .iter()
148            .map(|(idx, id, collapsed)| {
149                if *id == PaneId::MAIN || *collapsed {
150                    0.0
151                } else {
152                    // Fractions first, so that large stretch factors cannot overflow.
153                    let share = |total| remaining * (stretch(idx) / total);
154                    share(total).max(40.0).min(share(pane_total))
155                }
156            })
157            .collect();
158        if maximized.is_some_and(|id| id != PaneId::MAIN) {
159            for (height, (_, id, _)) in desired.iter_mut().zip(&panes) {
160                if *id != PaneId::MAIN {
161                    *height = usable_height;
162                }
163            }
164        }
165        let main_min = if maximized.is_some_and(|id| id != PaneId::MAIN) {
166            0.0
167        } else {
168            40.0
169        };
170        let expanded_count = expanded.len() - 1;
171        let pane_budget = (remaining - main_min).max(0.0);
172        let minimum = (pane_budget / expanded_count.max(1) as f32).min(40.0);
173        let excess: f32 = desired
174            .iter()
175            .map(|height| (*height - minimum).max(0.0))
176            .sum();
177        let factor =
178            ((pane_budget - minimum * expanded_count as f32).max(0.0) / excess.max(1.0)).min(1.0);
179        for (height, (_, id, collapsed)) in desired.iter_mut().zip(&panes) {
180            if *id != PaneId::MAIN && !collapsed {
181                *height = minimum + (*height - minimum).max(0.0) * factor;
182            }
183        }
184        let main_height = (remaining - desired.iter().sum::<f32>()).max(0.0);
185        let mut top = available.top();
186        let mut rects = Vec::with_capacity(panes.len());
187        for (at, ((idx, id, collapsed), height)) in panes.iter().zip(&desired).enumerate() {
188            if at > 0 {
189                top += gap;
190            }
191            let height = if *id == PaneId::MAIN {
192                main_height
193            } else if *collapsed {
194                row_height
195            } else {
196                *height
197            };
198            let rect = Rect::from_min_size(
199                Pos2::new(available.left(), top),
200                egui::vec2(available.width(), height),
201            );
202            top += height;
203            self.pane_layout.panes[*idx].rect = rect;
204            rects.push(rect);
205        }
206        let main_at = panes
207            .iter()
208            .position(|(_, id, _)| *id == PaneId::MAIN)
209            .expect("the price pane is always drawn");
210        let main_rect = rects[main_at];
211        let plot = self.pane_layout_rects(main_rect, false).price_rect;
212        let time_axis = Rect::from_min_max(
213            Pos2::new(plot.left(), available.bottom() - axis_height),
214            Pos2::new(plot.right(), available.bottom()),
215        );
216        let maximized_plot = maximized.filter(|id| *id != PaneId::MAIN).map(|id| {
217            ui.interact(
218                Rect::from_min_max(
219                    Pos2::new(plot.left(), available.top()),
220                    Pos2::new(plot.right(), time_axis.top()),
221                ),
222                ui.scope_id().with((id, "plot")),
223                Sense::click_and_drag(),
224            )
225        });
226        let response = ui
227            .scope_builder(
228                egui::UiBuilder::new()
229                    .max_rect(main_rect)
230                    .layout(egui::Layout::top_down(egui::Align::LEFT)),
231                |ui| self.show_internal(ui, Some(time_axis), maximized_plot.as_ref(), footprint),
232            )
233            .inner;
234
235        let mut host_clicks = Vec::new();
236        let studies = if self.chart_type.is_time_independent() {
237            &self.element_studies.studies
238        } else {
239            &self.studies
240        };
241        // Render the other panes around the price pane
242        let (start_idx, end_idx) = self.state.visible_range();
243        let visible_range = start_idx..end_idx;
244
245        // Get coordinate parameters from time scale for x-axis alignment
246        let time_scale = self.state.time_scale();
247        let mut coords = IndicatorCoordParams::new(
248            time_scale.bar_spacing(),
249            time_scale.right_offset_precise(),
250            self.state.shown().len().saturating_sub(1),
251            start_idx,
252        );
253        coords.numeric_positions = time_scale.numeric_positions().cloned();
254
255        let horizontal = coords.to_mapping(plot, 0.0, 1.0);
256        let displayed_range =
257            horizontal.x_to_idx(plot.left())..horizontal.x_to_idx(plot.right()).saturating_add(1);
258        // The plot column of every pane, where the pointer drives their shared crosshair.
259        let hover_rect = Rect::from_min_max(
260            Pos2::new(
261                plot.left(),
262                if main_at == 0 {
263                    plot.top()
264                } else {
265                    available.top()
266                },
267            ),
268            Pos2::new(plot.right(), time_axis.top()),
269        );
270
271        // A pane click is resolved after the loop so the immutable study
272        // borrow can be released before mutating selection state. `Some`
273        // means a pane was clicked; the inner value is the element to
274        // select, or `None` to clear (click on empty pane area).
275        let mut pending_pane_selection: Option<Option<ChartElementId>> = None;
276        // Study whose pane legend "x" was clicked this frame, if any.
277        // Removed after the immutable study borrow ends.
278        let mut pending_indicator_action = None;
279        // A host series whose legend asked to remove it, hide it, or open its settings.
280        let mut pending_series_action = None;
281        let mut pending_pane_collapse = None;
282        let mut pending_pane_maximize = None;
283        let current_selection = self.selection.selected_id();
284        // Draws the background, grid, axis and crosshair of a pane without a visible study.
285        let frame = CustomIndicator::new("", |_| Vec::new())
286            .with_overlay(false)
287            .with_line_count(0);
288
289        for ((pane_idx, pane_id, collapsed), panel_rect) in
290            panes.iter().copied().zip(rects.iter().copied())
291        {
292            if pane_id == PaneId::MAIN {
293                continue;
294            }
295            #[cfg(feature = "drawings")]
296            let eraser_mode = self.drawings.eraser_mode;
297            #[cfg(not(feature = "drawings"))]
298            let eraser_mode = false;
299            #[cfg(feature = "drawings")]
300            let drawing_tool_active = self.drawings.active_tool.is_some();
301            #[cfg(not(feature = "drawings"))]
302            let drawing_tool_active = false;
303
304            let height = panel_rect.height();
305            let members = self.pane_layout.panes[pane_idx].members.clone();
306            let primary = if self.config.show_right_axis {
307                crate::model::SeriesScale::Right
308            } else {
309                crate::model::SeriesScale::Left
310            };
311            let series: Vec<_> = members
312                .iter()
313                .filter_map(|id| {
314                    self.price_series
315                        .iter()
316                        .position(|item| item.id == *id && item.visible)
317                })
318                .collect();
319            // Panes without a visible bar series (study panes) keep their own formats.
320            let thousands_sep = self.config.thousands_sep.filter(|_| !series.is_empty());
321            // The fitted ranges of the visible host series in view, and the prices of their
322            // markers anchored in view, which share the pane scale.
323            let prices: Vec<f64> = series
324                .iter()
325                .flat_map(|at| {
326                    let item = &self.price_series[*at];
327                    self.series_fit(item)
328                        .into_iter()
329                        .flat_map(|(min, max)| [min, max])
330                        .chain(self.marker_prices(&item.markers, &[], item.marker_options))
331                        .chain(self.primitive_prices(item.id))
332                })
333                .filter(|price| price.is_finite())
334                .collect();
335            let mut shared_range = (f64::INFINITY, f64::NEG_INFINITY);
336            let mut primitive_prices = Vec::new();
337            for id in &members {
338                let Some(idx) = self.study_ids.iter().position(|study| study == id) else {
339                    continue;
340                };
341                let study = studies.indicators()[idx].as_ref();
342                if study.is_visible()
343                    && !matches!(study.scale(), crate::studies::StudyScale::Shared(key) if key != primary)
344                {
345                    let range = IndicatorPane::with_config(IndicatorPaneConfig::from_study(study))
346                        .calculate_y_range(study, &displayed_range);
347                    shared_range.0 = shared_range.0.min(range.0);
348                    shared_range.1 = shared_range.1.max(range.1);
349                    primitive_prices.extend(self.primitive_prices(*id));
350                }
351            }
352            for price in primitive_prices.iter().filter(|price| price.is_finite()) {
353                shared_range.0 = shared_range.0.min(*price);
354                shared_range.1 = shared_range.1.max(*price);
355            }
356            if let (Some(min), Some(max)) = (
357                prices.iter().copied().reduce(f64::min),
358                prices.iter().copied().reduce(f64::max),
359            ) {
360                // Pane studies fit with 10% padding; series in the pane do likewise.
361                let padding = crate::scales::auto_scale_span(min, max) * 0.1;
362                shared_range.0 = shared_range.0.min(min - padding);
363                shared_range.1 = shared_range.1.max(max + padding);
364            }
365            let first_close = series.iter().find_map(|at| {
366                let item = &self.price_series[*at];
367                item.bars[self.series_range(item)]
368                    .iter()
369                    .find(|bar| !bar.is_whitespace())
370                    .map(|bar| bar.close)
371            });
372            let pane_scale = &mut self.pane_layout.panes[pane_idx].scale;
373            let mode = pane_scale.options().mode;
374            let base = members
375                .iter()
376                .find_map(|id| {
377                    let idx = self.study_ids.iter().position(|study| study == id)?;
378                    let study = studies.indicators()[idx].as_ref();
379                    if !study.is_visible() {
380                        return None;
381                    }
382                    (0..study.line_count())
383                        .filter(|line| {
384                            let plot = study.plot_style(*line);
385                            plot.visible
386                                && !matches!(
387                                    plot.plot_type,
388                                    crate::studies::PlotType::BarColor
389                                        | crate::studies::PlotType::BackgroundColor
390                                )
391                        })
392                        .find_map(|line| {
393                            displayed_range
394                                .clone()
395                                .find_map(|bar| study.plot_value(line, bar))
396                        })
397                })
398                .or(first_close)
399                .filter(|base| *base != 0.0)
400                .unwrap_or(1.0);
401            let transform = crate::scales::PriceTransform { mode, base };
402            pane_scale.rebase(transform);
403            // Logarithmic scales fit drawable positive values, excluding zero histogram baselines.
404            if mode == crate::scales::PriceScaleMode::Logarithmic {
405                shared_range = (f64::INFINITY, f64::NEG_INFINITY);
406                for id in &members {
407                    let Some(idx) = self.study_ids.iter().position(|study| study == id) else {
408                        continue;
409                    };
410                    let study = studies.indicators()[idx].as_ref();
411                    if study.is_visible()
412                        && !matches!(study.scale(), crate::studies::StudyScale::Shared(key) if key != primary)
413                    {
414                        for line in (0..study.line_count()).filter(|line| {
415                            let plot = study.plot_style(*line);
416                            plot.visible
417                                && !matches!(
418                                    plot.plot_type,
419                                    crate::studies::PlotType::BarColor
420                                        | crate::studies::PlotType::BackgroundColor
421                                )
422                        }) {
423                            for value in displayed_range
424                                .clone()
425                                .filter_map(|bar| study.plot_value(line, bar))
426                                .filter(|value| *value > 0.0)
427                            {
428                                shared_range.0 = shared_range.0.min(value);
429                                shared_range.1 = shared_range.1.max(value);
430                            }
431                        }
432                    }
433                }
434                for value in prices
435                    .iter()
436                    .chain(&primitive_prices)
437                    .filter(|value| **value > 0.0)
438                {
439                    shared_range.0 = shared_range.0.min(*value);
440                    shared_range.1 = shared_range.1.max(*value);
441                }
442            }
443            pane_scale.autoscale(
444                transform.to_scale(shared_range.0),
445                transform.to_scale(shared_range.1),
446            );
447            let marker_map = pane_scale.map(transform, panel_rect.top(), panel_rect.bottom());
448            let marker_bounds: Vec<_> = series
449                .iter()
450                .flat_map(|at| {
451                    self.series_marker_bounds(ui.painter(), self.price_series[*at].id, marker_map)
452                })
453                .collect();
454            let pane_scale = &mut self.pane_layout.panes[pane_idx].scale;
455            Self::fit_marker_bounds(pane_scale, panel_rect, marker_bounds);
456            let map = pane_scale
457                .map(transform, panel_rect.top(), panel_rect.bottom())
458                .with_thousands_sep(thousands_sep);
459            let map = series.first().map_or(map, |at| {
460                self.series_display_map(self.price_series[*at].id, map)
461            });
462            let mut drawing_response = None;
463            let mut axis_tags = Vec::new();
464            let hovered = self.last_hover_bar_idx.and_then(|index| {
465                let index = index.round() as usize;
466                (index < self.state.shown().len()).then_some(index)
467            });
468            // The legend of each visible host series, with its close at the crosshair or its
469            // last close.
470            let legends: Vec<_> = series
471                .iter()
472                .map(|at| {
473                    let item = &self.price_series[*at];
474                    let close = match hovered {
475                        Some(bar) => self.series_close_at(item, bar),
476                        None => crate::model::last_with_value(&item.bars).map(|bar| bar.close),
477                    };
478                    (
479                        item.id,
480                        crate::widget::series::SeriesLegend::new(
481                            item,
482                            close,
483                            self.series_label_formatter(
484                                item.id,
485                                crate::scales::PriceScaleMode::Normal,
486                            ),
487                            map.price_precision(),
488                            map.thousands_sep(),
489                        ),
490                    )
491                })
492                .collect();
493            let pane_plot = Rect::from_x_y_ranges(plot.x_range(), panel_rect.y_range());
494            let mut pane_mapping = coords.to_mapping(pane_plot, 0.0, 1.0);
495            pane_mapping.price = map;
496            let on_top = ui
497                .input(|input| input.pointer.hover_pos())
498                .filter(|pos| {
499                    self.chart_options.hovered_series_on_top
500                        && !collapsed
501                        && pane_plot.contains(*pos)
502                })
503                .and_then(|pos| {
504                    members.iter().rev().find_map(|id| {
505                        if let Some(item) = self
506                            .price_series
507                            .iter()
508                            .find(|item| item.id == *id && item.visible)
509                        {
510                            self.hit_price_series(item, pos, pane_plot, map)
511                                .map(|_| *id)
512                        } else {
513                            let idx = self.study_ids.iter().position(|study| study == id)?;
514                            let study = studies.indicators()[idx].as_ref();
515                            let hit = crate::widget::primitives::primitive_at(
516                                self.primitives_of(*id).map(|item| {
517                                    (
518                                        item,
519                                        self.primitive_view(
520                                            *id,
521                                            if matches!(
522                                                study.scale(),
523                                                crate::studies::StudyScale::Shared(_)
524                                            ) {
525                                                crate::chart::rendering::study_mapping(
526                                                    study,
527                                                    &pane_mapping,
528                                                )
529                                            } else {
530                                                pane_mapping.clone()
531                                            },
532                                        ),
533                                    )
534                                }),
535                                pos,
536                            )
537                            .is_some();
538                            (hit || (!self.has_renderer(*id)
539                                && hit_test_pane_indicator(
540                                    pos,
541                                    study,
542                                    idx,
543                                    visible_range.clone(),
544                                    pane_plot,
545                                    &pane_mapping,
546                                )
547                                .is_some()))
548                            .then_some(*id)
549                        }
550                    })
551                });
552            // Each visible study and host series draws its legend in turn, with the study index
553            // of a study; the frame draws a pane with neither.
554            let mut entries: Vec<_> = members
555                .iter()
556                .filter_map(
557                    |id| match self.study_ids.iter().position(|study| study == id) {
558                        Some(idx) => studies.indicators()[idx]
559                            .is_visible()
560                            .then_some((*id, Some(idx))),
561                        None => legends
562                            .iter()
563                            .any(|(series, _)| series == id)
564                            .then_some((*id, None)),
565                    },
566                )
567                .map(Some)
568                .collect();
569            if entries.is_empty() {
570                entries.push(None);
571            }
572            // The crosshair lines of a pane with host series are drawn above their bars.
573            let mut series_crosshair = None;
574            let first = entries
575                .iter()
576                .min_by_key(|entry| entry.is_some_and(|(id, _)| Some(id) == on_top))
577                .copied();
578            let mut panels = Vec::with_capacity(entries.len());
579            let mut legend_offset = 0.0;
580            for entry in entries {
581                let id = entry.map(|(id, _)| id);
582                let study = entry.and_then(|(_, idx)| idx);
583                let indicator: &dyn Indicator = match (study, id) {
584                    (Some(idx), _) => studies.indicators()[idx].as_ref(),
585                    (None, Some(id)) => legends
586                        .iter()
587                        .find(|(series, _)| *series == id)
588                        .map(|(_, legend)| legend)
589                        .expect("a series entry has a legend"),
590                    (None, None) => &frame,
591                };
592                let mut config = IndicatorPaneConfig::from_study(indicator);
593                config.show_name = self.config.show_indicator_labels;
594                config.show_values = self.config.show_indicator_last_val;
595                config.inputs = match study {
596                    Some(idx) if self.config.show_indicator_inputs => {
597                        self.study_inputs[idx].clone()
598                    }
599                    _ => String::new(),
600                };
601                if self.config.show_indicator_inputs
602                    && indicator.is_volume()
603                    && indicator.plot_style(2).visible
604                    && config.inputs.is_empty()
605                {
606                    config.inputs = indicator.line_names().get(2).cloned().unwrap_or_default();
607                }
608                config.show_legend = entry.is_some()
609                    && (config.show_name || config.show_values || !config.inputs.is_empty());
610                config.height = height;
611                config.show_grid &= Some(entry) == first;
612                let mut panel = IndicatorPane::with_config(config);
613                panel.legend_backdrop = self.config.indicator_backdrop_opacity;
614                panel.axis_options = crate::config::PriceAxisOptions {
615                    show_study_values: self.config.show_indicator_last_val
616                        && self.config.price_axis.show_study_values,
617                    ..self.config.price_axis
618                };
619                panel.price_map = Some(
620                    if matches!(indicator.scale(), crate::studies::StudyScale::Shared(_)) {
621                        crate::chart::rendering::study_mapping(indicator, &pane_mapping).price
622                    } else {
623                        map
624                    },
625                );
626                if study.is_none() && id.is_some() {
627                    panel.price_formatter =
628                        id.and_then(|id| self.series_label_formatter(id, map.transform().mode));
629                }
630                if let crate::studies::StudyScale::Shared(key) = indicator.scale()
631                    && let Some((_, axis)) = self
632                        .secondary_axes(available, plot)
633                        .into_iter()
634                        .find(|(scale, _)| *scale == key)
635                {
636                    panel.axis_rect =
637                        Some(Rect::from_x_y_ranges(axis.x_range(), panel_rect.y_range()));
638                }
639                panel.legend_offset = legend_offset;
640                panel.pane_controls = true;
641                // A series legend reports its value at any bar, the last close without a crosshair.
642                panel.bar_index = hovered.or(id.filter(|_| study.is_none()).map(|_| 0));
643                let legend_row_height = panel
644                    .legend_galley(
645                        ui.painter(),
646                        pane_plot,
647                        indicator,
648                        ui.active_theme().ui.text,
649                    )
650                    .size()
651                    .y
652                    + ui.active_theme().spacing.md;
653                legend_offset += legend_row_height;
654                panels.push((entry, indicator, panel, legend_row_height));
655            }
656            panels.sort_by_key(|(entry, _, _, _)| entry.is_some_and(|(id, _)| Some(id) == on_top));
657            for (paint_row, (entry, indicator, mut panel, legend_row_height)) in
658                panels.into_iter().enumerate()
659            {
660                let id = entry.map(|(id, _)| id);
661                let study = entry.and_then(|(_, idx)| idx);
662                let key = (pane_id, id);
663                panel.overlay = paint_row > 0;
664                panel.axis_tags = axis_tags.clone();
665                let mut hover = self
666                    .axis_hover
667                    .map(|(pos, _)| pos)
668                    .or_else(|| {
669                        ui.input(|input| input.pointer.hover_pos()).filter(|pos| {
670                            hover_rect.contains(*pos) && ui.rect_contains_pointer(hover_rect)
671                        })
672                    })
673                    .or_else(|| {
674                        self.synced_crosshair_bar_idx
675                            .filter(|index| index.is_finite() && *index >= 0.0)
676                            .map(|index| {
677                                let index =
678                                    if self.chart_options.time_scale.ignore_whitespace_indices
679                                        || self.chart_options.time_scale.ignore_hidden_indices
680                                    {
681                                        index
682                                    } else {
683                                        index.round()
684                                    };
685                                Pos2::new(pane_mapping.index_to_x(index), plot.center().y)
686                            })
687                            .filter(|pos| plot.contains(*pos))
688                    })
689                    .filter(|_| !self.menu_open && !egui::Popup::is_any_open(ui.ctx()))
690                    .and_then(|pos| self.crosshair_position(pos, &pane_mapping));
691                let mut crosshair_options = self.chart_options.crosshair.resolved(ui.ctx());
692                if eraser_mode && crosshair_options.style != crate::config::CrosshairStyle::Hidden {
693                    crosshair_options.style = crate::config::CrosshairStyle::Arrow;
694                }
695
696                if panel.overlay {
697                    hover = None;
698                } else if crosshair_options.mode.snaps() {
699                    if let Some(pos) = &mut hover
700                        && panel_rect.contains(*pos)
701                        && let Some(bar) = panel.bar_index
702                        && let Some(value) = crate::chart::rendering::snap_to_values(
703                            pos.y,
704                            &map,
705                            self.pane_layout.panes[pane_idx]
706                                .members
707                                .iter()
708                                .filter_map(|id| {
709                                    self.study_ids.iter().position(|study| study == id)
710                                })
711                                .map(|idx| studies.indicators()[idx].as_ref())
712                                .filter(|study| study.is_visible())
713                                .flat_map(|study| {
714                                    (0..study.line_count())
715                                        .filter(|line| {
716                                            let plot = study.plot_style(*line);
717                                            plot.visible
718                                                && !matches!(
719                                                    plot.plot_type,
720                                                    crate::studies::PlotType::BarColor
721                                                        | crate::studies::PlotType::BackgroundColor
722                                                )
723                                        })
724                                        .filter_map(move |line| study.plot_value(line, bar))
725                                })
726                                .chain(series.iter().filter_map(|at| {
727                                    self.series_close_at(&self.price_series[*at], bar)
728                                })),
729                        )
730                    {
731                        pos.y = map.price_to_y(value);
732                    }
733                    crosshair_options.mode = crate::config::CrosshairMode::Normal;
734                }
735                if !series.is_empty()
736                    && !collapsed
737                    && let Some(pos) = hover
738                {
739                    // The pane draws the price tag; the lines go above the series' bars.
740                    series_crosshair = Some((pos, crosshair_options));
741                    crosshair_options.vert_line_color = egui::Color32::TRANSPARENT;
742                    crosshair_options.horz_line_color = egui::Color32::TRANSPARENT;
743                }
744
745                // Use show_aligned_interactive to get pane info for hit testing
746                let mut child =
747                    ui.new_child(egui::UiBuilder::new().id_salt(key).max_rect(panel_rect));
748                if let Some(interaction) = panel.show_aligned_interactive_in(
749                    &mut child,
750                    indicator,
751                    self.state.shown(),
752                    coords.clone(),
753                    Some(plot.x_range()),
754                    collapsed,
755                    if matches!(
756                        indicator.scale(),
757                        crate::studies::StudyScale::Shared(crate::model::SeriesScale::Overlay(_))
758                    ) {
759                        None
760                    } else if matches!(
761                        indicator.scale(),
762                        crate::studies::StudyScale::Shared(
763                            crate::model::SeriesScale::Left
764                                | crate::model::SeriesScale::Separate(_)
765                        )
766                    ) {
767                        Some(crate::scales::PriceScaleId::Left)
768                    } else if self.config.show_right_axis {
769                        Some(crate::scales::PriceScaleId::Right)
770                    } else if self.config.show_left_axis {
771                        Some(crate::scales::PriceScaleId::Left)
772                    } else {
773                        None
774                    },
775                    hover.map(|pos| (pos, crosshair_options)),
776                    &id.into_iter()
777                        .flat_map(|id| self.primitives_of(id))
778                        .collect::<Vec<_>>(),
779                    study.is_none() && id.is_some(),
780                    self.merge_groups.factor(),
781                ) {
782                    if !collapsed
783                        && let Some(item) =
784                            id.and_then(|id| self.price_series.iter().find(|item| item.id == id))
785                    {
786                        let painter = ui
787                            .painter()
788                            .with_clip_rect(interaction.chart_rect.intersect(ui.clip_rect()));
789                        self.paint_series(&painter, interaction.chart_rect, item, map);
790                        let view = self.primitive_view(item.id, pane_mapping.clone());
791                        for primitive in self.primitives_of(item.id) {
792                            primitive.draw(
793                                &painter,
794                                &view,
795                                crate::widget::PrimitivePass::Series,
796                                self.merge_groups.factor(),
797                            );
798                        }
799                    }
800                    self.measured_axis_width =
801                        self.measured_axis_width.max(panel.measured_axis_width);
802                    axis_tags = panel.axis_tags.clone();
803                    let PaneInteraction {
804                        panel_rect,
805                        chart_rect,
806                        y_min,
807                        y_max,
808                        response: pane_response,
809                        close_response,
810                        visibility_response,
811                        settings_response,
812                        menu,
813                    } = interaction;
814
815                    let pane_response = match &maximized_plot {
816                        Some(plot) => pane_response.union(plot.clone()),
817                        None => pane_response.interact(Sense::click_and_drag()),
818                    };
819
820                    // A click on the legend's close "x" removes this study or series.
821                    let close_clicked = close_response.as_ref().is_some_and(|r| r.clicked());
822                    let visibility_clicked =
823                        visibility_response.as_ref().is_some_and(|r| r.clicked());
824                    let settings_clicked = settings_response.as_ref().is_some_and(|r| r.clicked());
825                    let control_clicked =
826                        close_clicked || visibility_clicked || settings_clicked || menu.is_some();
827                    use crate::chart::rendering::StudyAction;
828                    match (study, id) {
829                        (Some(idx), _) if close_clicked => {
830                            pending_indicator_action = Some(StudyAction::Remove(idx));
831                        }
832                        (Some(idx), _) if visibility_clicked => {
833                            pending_indicator_action = Some(StudyAction::ToggleVisibility(idx));
834                        }
835                        (Some(idx), _) if settings_clicked => {
836                            pending_indicator_action = Some(StudyAction::Settings(idx));
837                        }
838                        (Some(idx), _) if menu.is_some() => {
839                            pending_indicator_action = Some(StudyAction::Menu {
840                                index: idx,
841                                pos: menu.unwrap(),
842                            });
843                        }
844                        (None, Some(id)) if control_clicked => {
845                            pending_series_action = Some((id, close_clicked, visibility_clicked));
846                        }
847                        _ => {}
848                    }
849                    let header = Rect::from_min_max(
850                        chart_rect.min + egui::vec2(0.0, panel.legend_offset),
851                        Pos2::new(
852                            visibility_response
853                                .as_ref()
854                                .map_or(chart_rect.right() - 42.0, |r| {
855                                    r.rect.left().min(chart_rect.right())
856                                }),
857                            (chart_rect.top() + panel.legend_offset + legend_row_height)
858                                .min(chart_rect.bottom()),
859                        ),
860                    );
861                    let toggle = ui
862                        .interact(header, ui.scope_id().with((key, "toggle")), Sense::click())
863                        .on_hover_text(l10n::text(
864                            ui.ctx(),
865                            if collapsed {
866                                "Expand pane"
867                            } else {
868                                "Collapse pane"
869                            },
870                        ));
871                    let translated_label = l10n::text(
872                        ui.ctx(),
873                        if collapsed {
874                            "Expand pane"
875                        } else {
876                            "Collapse pane"
877                        },
878                    );
879                    toggle.widget_info(|| {
880                        egui::WidgetInfo::labeled(
881                            egui::Role::Button,
882                            toggle.enabled(),
883                            &translated_label,
884                        )
885                    });
886                    ui.ctx()
887                        .accesskit_node_builder(toggle.id, |node| node.set_expanded(!collapsed));
888                    if toggle.clicked() && !control_clicked {
889                        pending_pane_collapse = Some((pane_id, !collapsed));
890                        ui.ctx().request_repaint();
891                    }
892                    let mut pane_coords = coords.to_mapping(chart_rect, y_min, y_max);
893                    pane_coords.price = map;
894                    // Primitives are hit tested at the pointer, before the chart state is
895                    // borrowed mutably.
896                    let pointer = ui
897                        .input(|input| input.pointer.interact_pos())
898                        .filter(|pos| chart_rect.contains(*pos));
899                    let pane_primitives = || {
900                        self.primitives
901                            .iter()
902                            .filter(|item| {
903                                self.pane_layout.panes[pane_idx]
904                                    .members
905                                    .contains(&item.series)
906                                    && self.is_series_visible(item.series)
907                            })
908                            .map(|item| {
909                                let mut view =
910                                    self.primitive_view(item.series, pane_coords.clone());
911                                if let Some(study) = view.study
912                                    && matches!(
913                                        study.scale(),
914                                        crate::studies::StudyScale::Shared(_)
915                                    )
916                                {
917                                    view.mapping =
918                                        crate::chart::rendering::study_mapping(study, &pane_coords);
919                                }
920                                (item, view)
921                            })
922                    };
923                    let primitive_hit = pointer.and_then(|pos| {
924                        crate::widget::primitives::primitive_at(
925                            pane_primitives().filter(|(item, _)| !item.replaces),
926                            pos,
927                        )
928                    });
929
930                    // Resolve a click on this pane: a hit on the line selects
931                    // it, an empty-pane click clears the whole selection. A
932                    // close-"x" click is handled above and must not also
933                    // change the selection.
934                    let study_at = |pos| {
935                        self.pane_layout.panes[pane_idx]
936                            .members
937                            .iter()
938                            .rev()
939                            .find_map(|id| {
940                                if self.has_renderer(*id) {
941                                    let hit = crate::widget::primitives::primitive_at(
942                                        pane_primitives().filter(|(item, _)| {
943                                            item.replaces && item.series == *id
944                                        }),
945                                        pos,
946                                    )?;
947                                    return match hit.target {
948                                        crate::widget::PointerTarget::Element(id) => Some(id),
949                                        _ => None,
950                                    };
951                                }
952
953                                if let Some(item) = self
954                                    .price_series
955                                    .iter()
956                                    .find(|item| item.id == *id && item.visible)
957                                {
958                                    return self
959                                        .hit_price_series(item, pos, chart_rect, map)
960                                        .map(|_| ChartElementId::Series(*id));
961                                }
962                                let idx = self.study_ids.iter().position(|study| study == id)?;
963                                hit_test_pane_indicator(
964                                    pos,
965                                    studies.indicators()[idx].as_ref(),
966                                    idx,
967                                    visible_range.clone(),
968                                    chart_rect,
969                                    &pane_coords,
970                                )
971                                .map(|hit| {
972                                    ChartElementId::Indicator(self.study_ids[hit.indicator_idx])
973                                })
974                            })
975                    };
976                    if !collapsed
977                        && !toggle.clicked()
978                        && !control_clicked
979                        && pane_response.clicked()
980                        && !drawing_tool_active
981                        && !eraser_mode
982                        && let Some(click_pos) = pane_response.interact_pointer_pos()
983                        && self.mark_at(click_pos).is_none()
984                    {
985                        let hit = study_at(click_pos);
986                        if hit.is_some() || pending_pane_selection.is_none() {
987                            pending_pane_selection = Some(hit);
988                        }
989                    }
990                    // What the pointer is over, for host pointer events: drawings sit on top.
991                    if !collapsed && let Some(pos) = pointer {
992                        #[cfg(feature = "drawings")]
993                        let drawing = self.drawings.hit_test(pos);
994                        #[cfg(not(feature = "drawings"))]
995                        let drawing: Option<usize> = None;
996                        let renderer = study_at(pos).and_then(|element| {
997                            let ChartElementId::Indicator(id) = element else {
998                                return None;
999                            };
1000                            crate::widget::primitives::primitive_at(
1001                                pane_primitives()
1002                                    .filter(|(item, _)| item.replaces && item.series == id),
1003                                pos,
1004                            )
1005                        });
1006                        if let Some(hit) = primitive_hit.or(renderer).filter(|_| {
1007                            drawing.is_none()
1008                                && pane_response.hovered()
1009                                && !pane_response.dragged()
1010                                && !eraser_mode
1011                                && !drawing_tool_active
1012                        }) {
1013                            ui.ctx().set_cursor_icon(hit.cursor);
1014                        }
1015                        self.host_events.target = drawing
1016                            .map(crate::widget::PointerTarget::Drawing)
1017                            .or(primitive_hit.map(|hit| hit.target))
1018                            .or_else(|| study_at(pos).map(crate::widget::PointerTarget::Element));
1019                    }
1020
1021                    // Draw selection handles when this pane is selected.
1022                    if !collapsed
1023                        && let Some(id) = id
1024                        && current_selection == Some(ChartElementId::Indicator(id))
1025                    {
1026                        let dot_config = SelectionDotConfig {
1027                            dot_interval: calculate_dot_interval(pane_coords.bar_spacing),
1028                            ..Default::default()
1029                        };
1030                        for line_idx in 0..indicator.line_count() {
1031                            render_indicator_selection_dots(
1032                                &ui.painter()
1033                                    .with_clip_rect(chart_rect.intersect(ui.clip_rect())),
1034                                indicator,
1035                                line_idx,
1036                                visible_range.clone(),
1037                                &pane_coords,
1038                                |value| pane_coords.price_to_y(value),
1039                                &dot_config,
1040                            );
1041                        }
1042                    }
1043
1044                    drawing_response = Some((pane_response, chart_rect, pane_coords.clone()));
1045                    // Store the pane info for hit testing by platform
1046                    if let Some(idx) = study {
1047                        self.last_rendered_indicator_panes
1048                            .push(RenderedIndicatorPane {
1049                                indicator_idx: idx,
1050                                panel_rect,
1051                                chart_rect,
1052                                y_min,
1053                                y_max,
1054                                coords: coords.clone(),
1055                                mapping: pane_coords,
1056                            });
1057                    }
1058                }
1059            }
1060            if let Some((_, chart_rect, _)) = &drawing_response {
1061                let chart_rect = *chart_rect;
1062                let painter = ui
1063                    .painter()
1064                    .with_clip_rect(panel_rect.intersect(ui.clip_rect()));
1065                let items: Vec<_> = series.iter().map(|at| &self.price_series[*at]).collect();
1066                let axis_edge = if self.config.show_right_axis {
1067                    Some(panel_rect.right())
1068                } else if self.config.show_left_axis {
1069                    Some(chart_rect.left())
1070                } else {
1071                    None
1072                };
1073                if !collapsed && let Some((mut pos, options)) = series_crosshair {
1074                    if let Some(index) = hovered {
1075                        pos.x = horizontal.idx_to_x(index);
1076                    }
1077                    crate::chart::rendering::render_crosshair_lines(
1078                        &crate::chart::rendering::RenderContext::new(&painter, chart_rect),
1079                        pos,
1080                        &options,
1081                        chart_rect.bottom(),
1082                    );
1083                }
1084                if !collapsed && let Some(axis_edge) = axis_edge {
1085                    #[cfg(feature = "drawings")]
1086                    for (value, fill) in self
1087                        .drawings
1088                        .drawings
1089                        .iter()
1090                        .filter(|drawing| {
1091                            drawing.is_visible_on_timeframe(&self.drawings.curr_timeframe)
1092                                && (drawing.pane == Some(pane_id.0)
1093                                    || members.iter().any(|id| drawing.pane == Some(id.0)))
1094                        })
1095                        .flat_map(|drawing| {
1096                            drawing.position_price_tags(ui.active_theme().chart.axis_text_secondary)
1097                        })
1098                        .filter(|(value, _)| map.range().contains(map.transform().to_scale(*value)))
1099                    {
1100                        let label = map.label(
1101                            map.transform().to_scale(value),
1102                            crate::chart::rendering::axes::tag_decimals(
1103                                &map,
1104                                map.price_to_y(value),
1105                            ),
1106                            self.price_formatter.as_deref(),
1107                        );
1108                        axis_tags.push(crate::chart::rendering::axes::render_axis_value(
1109                            &painter,
1110                            chart_rect,
1111                            axis_edge,
1112                            map.price_to_y(value),
1113                            label,
1114                            (crate::chart::rendering::axes::tag_text_color(fill), fill),
1115                            &axis_tags,
1116                            self.config.price_axis.allow_label_overlap,
1117                        ));
1118                    }
1119                    for (item, (value, colors, last)) in items
1120                        .iter()
1121                        .flat_map(|item| {
1122                            self.series_axis_values(item)
1123                                .map(move |value| (item, value))
1124                        })
1125                        .filter(|(_, (value, ..))| {
1126                            map.range().contains(map.transform().to_scale(*value))
1127                        })
1128                    {
1129                        let tag = crate::chart::rendering::axes::render_axis_value(
1130                            &painter,
1131                            chart_rect,
1132                            axis_edge,
1133                            map.price_to_y(value),
1134                            self.series_tag_label(item, &map, value, last),
1135                            colors,
1136                            &axis_tags,
1137                            self.config.price_axis.allow_label_overlap,
1138                        );
1139                        axis_tags.push(tag);
1140                    }
1141                }
1142            }
1143            if !collapsed && let Some((pane_response, chart_rect, pane_coords)) = drawing_response {
1144                if pane_response.clicked() || pane_response.secondary_clicked() {
1145                    host_clicks.push(pane_response.clone());
1146                }
1147                #[cfg(feature = "drawings")]
1148                let drawing_hit = {
1149                    let history = self.history.start_drawing_action(&mut self.drawings);
1150                    let mut drawings = std::mem::take(&mut self.drawings);
1151                    let mut cursors = std::mem::take(&mut self.cursor_modes);
1152                    let timescale = self.state.time_scale().clone();
1153                    let painter = ui.painter().clone();
1154                    let time_formatter = self.build_time_formatter();
1155                    self.handle_pane_drawings(
1156                        ui,
1157                        &mut drawings,
1158                        &mut cursors,
1159                        &pane_response,
1160                        chart_rect,
1161                        &map,
1162                        &painter,
1163                        &timescale,
1164                        (time_formatter.as_ref(), self.time_label_seconds()),
1165                        Some(pane_id),
1166                    );
1167                    let drawing_hit = pane_response
1168                        .interact_pointer_pos()
1169                        .and_then(|pos| drawings.hit_test(pos));
1170                    crate::widget::tool_interaction::render_pane_eraser_highlight(
1171                        &ui.painter().with_clip_rect(chart_rect),
1172                        &drawings,
1173                        &cursors,
1174                    );
1175                    self.drawings = drawings;
1176                    self.cursor_modes = cursors;
1177                    self.history
1178                        .finish_drawing_action(&mut self.drawings, history);
1179                    drawing_hit
1180                };
1181                #[cfg(not(feature = "drawings"))]
1182                let drawing_hit: Option<usize> = None;
1183                // Study settings stay on the legend button and the context menu.
1184                if pane_response.double_clicked() && drawing_hit.is_none() {
1185                    pending_pane_maximize = Some(pane_id);
1186                }
1187                if pane_response.clicked() && drawing_hit.is_some() {
1188                    pending_pane_selection = None;
1189                    self.selection.deselect();
1190                }
1191                if pane_response.secondary_clicked()
1192                    && let Some(pos) = pane_response.interact_pointer_pos()
1193                {
1194                    self.right_click = Some(if let Some(id) = drawing_hit {
1195                        #[cfg(feature = "drawings")]
1196                        {
1197                            if self.drawings.is_selected(id) {
1198                                let ids: Vec<_> = std::iter::once(id)
1199                                    .chain(self.drawings.selected_ids())
1200                                    .collect();
1201                                self.drawings.select_drawings(ids);
1202                            } else {
1203                                self.drawings.select(id);
1204                            }
1205                        }
1206                        crate::widget::RightClickTarget::Drawing {
1207                            drawing_id: id,
1208                            pos,
1209                        }
1210                    } else {
1211                        crate::widget::RightClickTarget::Background {
1212                            pos,
1213                            price: map.y_to_price(pos.y),
1214                        }
1215                    });
1216                }
1217                // The last pass of the pane's primitives, above its drawings.
1218                let painter = ui.painter().with_clip_rect(chart_rect);
1219                for item in self.primitives.iter().filter(|item| {
1220                    self.pane_layout.panes[pane_idx]
1221                        .members
1222                        .contains(&item.series)
1223                        && self.is_series_visible(item.series)
1224                }) {
1225                    let view = self.primitive_view(item.series, pane_coords.clone());
1226                    item.draw(
1227                        &painter,
1228                        &view,
1229                        crate::widget::PrimitivePass::Above,
1230                        self.merge_groups.factor(),
1231                    );
1232                }
1233            }
1234            if !collapsed {
1235                let mut table_rect = plot;
1236                for id in &members {
1237                    if let Some(idx) = self.study_ids.iter().position(|study| study == id) {
1238                        let study = &studies.indicators()[idx];
1239                        if study.is_visible()
1240                            && let Some(table) = study.table()
1241                        {
1242                            table_rect.max.y = crate::studies::table::paint(
1243                                ui,
1244                                table_rect,
1245                                table,
1246                                ui.scope_id().with((pane_id, id)),
1247                            )
1248                            .top()
1249                                - 8.0;
1250                        }
1251                    }
1252                }
1253            }
1254        }
1255        for response in host_clicks {
1256            self.record_host_click(ui, &response);
1257        }
1258        let drawn: Vec<_> = panes
1259            .iter()
1260            .zip(&rects)
1261            .map(|((idx, _, collapsed), rect)| (*idx, *rect, *collapsed))
1262            .collect();
1263        self.pane_separators(ui, &drawn);
1264        for ((_, id, collapsed), rect) in panes.iter().copied().zip(rects) {
1265            if id == PaneId::MAIN {
1266                continue;
1267            }
1268            if let Some(idx) = self.pane_layout.panes.iter().position(|pane| pane.id == id) {
1269                self.pane_scale_interaction(ui, idx, rect, plot);
1270                self.pane_actions(ui, idx, id, rect, plot, collapsed);
1271            }
1272        }
1273
1274        // Apply the deferred pane selection now that the studies are no longer
1275        // borrowed.
1276        if let Some(decision) = pending_pane_selection {
1277            match decision {
1278                Some(id) => self.selection.select(id, None),
1279                None => self.selection.deselect(),
1280            }
1281        }
1282
1283        // The panes reflow on the next frame.
1284        if let Some(id) = pending_pane_maximize {
1285            self.user_action(|chart| {
1286                let maximized = chart.pane_layout.maximized == Some(id);
1287                chart.pane_layout.maximized = (!maximized).then_some(id);
1288            });
1289        }
1290        if let Some((id, collapsed)) = pending_pane_collapse {
1291            self.user_action(|chart| {
1292                if let Some(pane) = chart
1293                    .pane_layout
1294                    .panes
1295                    .iter_mut()
1296                    .find(|pane| pane.id == id)
1297                {
1298                    pane.collapsed = Some(collapsed);
1299                }
1300            });
1301        }
1302        if let Some(action) = pending_indicator_action {
1303            self.apply_study_action(action);
1304        }
1305        match pending_series_action {
1306            Some((id, true, _)) => {
1307                self.user_action(|chart| chart.remove_series(id));
1308            }
1309            Some((id, _, true)) => {
1310                self.user_action(|chart| chart.set_series_visible(id, false));
1311            }
1312            Some((id, ..)) => self.indicator_settings_request = Some(id),
1313            None => {}
1314        }
1315
1316        ui.advance_cursor_after_rect(available);
1317        ui.spacing_mut().item_spacing = host_spacing;
1318        response
1319    }
1320}
1321
1322#[cfg(all(test, feature = "studies"))]
1323mod tests;