Skip to main content

egui_charts/ui/top_toolbar/components/settings_menu/
menu.rs

1//! Quick settings menu popup.
2//!
3//! A compact popup menu for quick chart configuration changes (chart type,
4//! grid visibility, scale mode, and label options).
5
6use super::config::ChartSettings;
7use crate::config::ChartConfig;
8use crate::localization as l10n;
9use crate::model::ChartType;
10use crate::scales::PriceScaleMode;
11use crate::theme::ThemeExt;
12use crate::tokens::DESIGN_TOKENS;
13use crate::ui_kit::ext::UiExt;
14use egui::{Align2, Context, Id, Pos2, Rect, Response, Ui};
15
16// ============================================================================
17// Settings Menu (Quick Popup)
18// ============================================================================
19
20/// Quick settings popup menu for chart configuration
21pub struct SettingsMenu {
22    /// Current chart settings
23    pub settings: ChartSettings,
24    /// Whether the menu popup is open
25    pub is_open: bool,
26    /// Rect of the trigger button (for popup positioning)
27    btn_rect: Option<Rect>,
28}
29
30impl SettingsMenu {
31    /// Create a new settings menu with default chart settings
32    pub fn new() -> Self {
33        Self {
34            settings: ChartSettings::default(),
35            is_open: false,
36            btn_rect: None,
37        }
38    }
39
40    /// Get a reference to the current chart settings
41    pub fn settings(&self) -> &ChartSettings {
42        &self.settings
43    }
44
45    /// Get a mutable reference to the current chart settings
46    pub fn settings_mut(&mut self) -> &mut ChartSettings {
47        &mut self.settings
48    }
49
50    /// Apply current settings to a ChartConfig
51    pub fn apply_to_config(&self, mut config: ChartConfig) -> ChartConfig {
52        config.show_grid = self.settings.show_horizontal_grid || self.settings.show_vertical_grid;
53        config.show_horizontal_grid = self.settings.show_horizontal_grid;
54        config.show_vertical_grid = self.settings.show_vertical_grid;
55        config.show_right_axis = self.settings.show_right_axis;
56        config.show_left_axis = self.settings.show_left_axis;
57        config.show_symbol_labels = self.settings.show_symbol_labels;
58        config.show_symbol_last_val = self.settings.show_symbol_last_val;
59        config.show_symbol_prev_close = self.settings.show_symbol_prev_close;
60        config.show_indicator_labels = self.settings.show_indicator_labels;
61        config.show_indicator_last_val = self.settings.show_indicator_last_val;
62        config.show_countdown = self.settings.show_countdown;
63        config.lock_price_to_bar_ratio = self.settings.lock_scale;
64        config.scale_price_chart_only = self.settings.scale_price_chart_only;
65        config
66    }
67
68    /// Get the currently selected chart type
69    pub fn chart_type(&self) -> ChartType {
70        self.settings.chart_type
71    }
72
73    /// Show the settings button (bottom-right corner)
74    pub fn show_btn(&mut self, ui: &mut Ui, chart_rect: Rect) -> Response {
75        let btn_size = egui::vec2(32.0, 32.0);
76        let btn_pos = Pos2::new(
77            chart_rect.max.x - btn_size.x - 10.0,
78            chart_rect.max.y - btn_size.y - 10.0,
79        );
80
81        let btn_rect = Rect::from_min_size(btn_pos, btn_size);
82        let btn_id = ui.scope_id().with("settings_btn");
83
84        let response = ui.interact(btn_rect, btn_id, egui::Sense::click());
85        let translated_label = l10n::text(ui.ctx(), "Chart settings");
86        response.widget_info(|| {
87            egui::WidgetInfo::labeled(egui::Role::Button, response.enabled(), &translated_label)
88        });
89        crate::styles::focus::draw_focus_ring(ui, &response, ui.active_theme().radius.md);
90
91        let visuals = ui.style().interact(&response);
92        let bg_color = if response.hovered() {
93            visuals.bg_fill
94        } else {
95            ui.visuals().widgets.inactive.bg_fill
96        };
97
98        ui.painter()
99            .rect_filled(btn_rect, ui.active_theme().radius.md, bg_color);
100        ui.painter().text(
101            btn_rect.center(),
102            Align2::CENTER_CENTER,
103            l10n::text(ui.ctx(), "S"),
104            egui::FontId::proportional(ui.active_theme().text.dialog_title),
105            visuals.text_color(),
106        );
107
108        if response.clicked() {
109            self.is_open = !self.is_open;
110        }
111
112        self.btn_rect = Some(btn_rect);
113        response
114    }
115
116    /// Show the settings popup menu
117    pub fn show_menu(&mut self, ctx: &Context) {
118        if !self.is_open {
119            return;
120        }
121        let Some(btn_rect) = self.btn_rect else {
122            return;
123        };
124
125        let popup_id = Id::unique("settings_popup");
126        let popup_pos = Pos2::new(btn_rect.min.x - 200.0, btn_rect.min.y - 400.0);
127
128        egui::Area::new(popup_id)
129            .fixed_pos(popup_pos)
130            .order(egui::Order::Foreground)
131            .show(ctx, |ui| {
132                egui::Frame::popup(ui.style()).show(ui, |ui| {
133                    ui.set_min_width(180.0);
134                    ui.set_max_width(220.0);
135                    self.draw_menu_sections(ui);
136                });
137            });
138
139        self.handle_click_outside(ctx, btn_rect, popup_pos);
140    }
141
142    fn draw_menu_sections(&mut self, ui: &mut egui::Ui) {
143        self.draw_chart_type_section(ui);
144        self.draw_appearance_section(ui);
145        self.draw_scale_section(ui);
146        self.draw_labels_section(ui);
147
148        ui.separator();
149        if ui
150            .button(l10n::text(ui.ctx(), "Scales Properties..."))
151            .clicked()
152        {
153            self.is_open = false;
154        }
155    }
156
157    fn draw_chart_type_section(&mut self, ui: &mut egui::Ui) {
158        ui.strong_label("Chart Type");
159        ui.separator();
160
161        for chart_type in &[
162            ChartType::Candles,
163            ChartType::Bars,
164            ChartType::Line,
165            ChartType::Area,
166        ] {
167            if ui
168                .selectable_label(
169                    self.settings.chart_type == *chart_type,
170                    l10n::text(ui.ctx(), chart_type.as_str()),
171                )
172                .clicked()
173            {
174                self.settings.chart_type = *chart_type;
175            }
176        }
177        ui.separator();
178    }
179
180    fn draw_appearance_section(&mut self, ui: &mut egui::Ui) {
181        ui.strong_label("Appearance");
182        ui.separator();
183        ui.checkbox(
184            &mut self.settings.show_horizontal_grid,
185            l10n::text(ui.ctx(), "Horizontal Grid"),
186        );
187        ui.checkbox(
188            &mut self.settings.show_vertical_grid,
189            l10n::text(ui.ctx(), "Vertical Grid"),
190        );
191        ui.separator();
192    }
193
194    fn draw_scale_section(&mut self, ui: &mut egui::Ui) {
195        ui.strong_label("Scale");
196        ui.separator();
197
198        if ui.button(l10n::text(ui.ctx(), "Reset Scale")).clicked() {
199            self.is_open = false;
200        }
201
202        ui.checkbox(
203            &mut self.settings.show_left_axis,
204            l10n::text(ui.ctx(), "Left Axis"),
205        );
206        ui.checkbox(
207            &mut self.settings.show_right_axis,
208            l10n::text(ui.ctx(), "Right Axis"),
209        );
210
211        if ui
212            .selectable_label(
213                self.settings.scale_mode == PriceScaleMode::Normal,
214                l10n::text(ui.ctx(), "Auto Scale"),
215            )
216            .clicked()
217        {
218            self.settings.scale_mode = PriceScaleMode::Normal;
219        }
220
221        ui.checkbox(
222            &mut self.settings.lock_scale,
223            l10n::text(ui.ctx(), "Lock price to bar ratio"),
224        );
225        ui.checkbox(
226            &mut self.settings.scale_price_chart_only,
227            l10n::text(ui.ctx(), "Scale Price Chart Only"),
228        );
229        ui.separator();
230
231        if ui
232            .selectable_label(
233                self.settings.scale_mode == PriceScaleMode::Percentage,
234                l10n::text(ui.ctx(), "Percentage"),
235            )
236            .clicked()
237        {
238            self.settings.scale_mode = PriceScaleMode::Percentage;
239        }
240        if ui
241            .selectable_label(
242                self.settings.scale_mode == PriceScaleMode::Logarithmic,
243                l10n::text(ui.ctx(), "Log Scale"),
244            )
245            .clicked()
246        {
247            self.settings.scale_mode = PriceScaleMode::Logarithmic;
248        }
249        ui.separator();
250    }
251
252    fn draw_labels_section(&mut self, ui: &mut egui::Ui) {
253        ui.strong_label("Labels");
254        ui.separator();
255
256        ui.checkbox(
257            &mut self.settings.show_symbol_labels,
258            l10n::text(ui.ctx(), "Symbol Labels"),
259        );
260        ui.checkbox(
261            &mut self.settings.show_symbol_last_val,
262            l10n::text(ui.ctx(), "Symbol Last Value"),
263        );
264        ui.checkbox(
265            &mut self.settings.show_symbol_prev_close,
266            l10n::text(ui.ctx(), "Symbol Prev. Close Value"),
267        );
268        ui.separator();
269
270        ui.checkbox(
271            &mut self.settings.show_indicator_labels,
272            l10n::text(ui.ctx(), "Indicator Labels"),
273        );
274        ui.checkbox(
275            &mut self.settings.show_indicator_last_val,
276            l10n::text(ui.ctx(), "Indicator Last Value"),
277        );
278        ui.separator();
279
280        ui.checkbox(
281            &mut self.settings.show_countdown,
282            l10n::text(ui.ctx(), "Countdown"),
283        );
284        ui.checkbox(
285            &mut self.settings.no_overlapping_labels,
286            l10n::text(ui.ctx(), "No Overlapping Labels"),
287        );
288    }
289
290    fn handle_click_outside(&mut self, ctx: &Context, btn_rect: Rect, popup_pos: Pos2) {
291        if ctx.input(|i| i.pointer.any_released())
292            && let Some(pos) = ctx.input(|i| i.pointer.interact_pos())
293        {
294            let menu_rect = Rect::from_min_size(
295                popup_pos,
296                egui::vec2(
297                    DESIGN_TOKENS.sizing.dialog.menu_settings_width,
298                    DESIGN_TOKENS.sizing.dialog.menu_settings_height,
299                ),
300            );
301            if !menu_rect.contains(pos) && !btn_rect.contains(pos) {
302                self.is_open = false;
303            }
304        }
305    }
306}
307
308impl Default for SettingsMenu {
309    fn default() -> Self {
310        Self::new()
311    }
312}