Skip to main content

egui_charts/ui/top_toolbar/components/
sync_button.rs

1//! Multi-Chart Sync Options Button
2//!
3//! A button with dropdown for configuring multi-chart synchronization options.
4//! Only visible when in multi-chart layout mode.
5
6use crate::icons::icons as embedded_icons;
7use crate::localization as l10n;
8use crate::theme::ThemeExt;
9use crate::tokens::DESIGN_TOKENS;
10use crate::ui::multi_chart::ChartSyncOptions;
11use egui::{Color32, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2};
12
13/// Sync options button widget
14pub struct SyncButton {
15    /// Is dropdown open
16    is_open: bool,
17    /// Current sync options
18    options: ChartSyncOptions,
19}
20
21impl Default for SyncButton {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl SyncButton {
28    /// Create new sync button with default options
29    pub fn new() -> Self {
30        Self {
31            is_open: false,
32            options: ChartSyncOptions::default(),
33        }
34    }
35
36    /// Set current sync options
37    pub fn set_options(&mut self, options: ChartSyncOptions) {
38        self.options = options;
39    }
40
41    /// Get current sync options
42    pub fn options(&self) -> &ChartSyncOptions {
43        &self.options
44    }
45
46    /// Show the button and return updated options if changed
47    pub fn show(&mut self, ui: &mut Ui) -> Option<ChartSyncOptions> {
48        let mut result = None;
49
50        // Main button
51        let btn_response = self.draw_button(ui);
52        if btn_response.clicked() {
53            self.is_open = !self.is_open;
54        }
55
56        // Dropdown
57        if self.is_open {
58            let btn_rect = btn_response.rect;
59            if let Some(new_options) = self.draw_dropdown(ui, btn_rect) {
60                self.options = new_options.clone();
61                result = Some(new_options);
62            }
63
64            // Close on click outside
65            if ui.input(|i| i.pointer.any_click())
66                && let Some(pos) = ui.input(|i| i.pointer.hover_pos())
67            {
68                let dropdown_rect = self.dropdown_rect(ui, btn_rect);
69                if !btn_rect.contains(pos) && !dropdown_rect.contains(pos) {
70                    self.is_open = false;
71                }
72            }
73        }
74
75        result
76    }
77
78    fn draw_button(&self, ui: &mut Ui) -> Response {
79        let visuals = ui.style().visuals.clone();
80        let hover_color = visuals.widgets.hovered.bg_fill;
81        let border_color = visuals.widgets.noninteractive.bg_stroke.color;
82        let accent_color = ui.active_theme().ui.accent;
83
84        // Check if any sync is enabled
85        let any_sync_enabled = self.options.sync_time_axis
86            || self.options.sync_crosshair
87            || self.options.sync_drawings
88            || self.options.sync_symbol
89            || self.options.sync_timeframe;
90
91        let desired_size = Vec2::splat(DESIGN_TOKENS.sizing.button_md);
92        let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
93        let translated_label = l10n::text(ui.ctx(), "Chart Sync Options");
94        response.widget_info(|| {
95            egui::WidgetInfo::selected(
96                egui::Role::Button,
97                response.enabled(),
98                any_sync_enabled,
99                &translated_label,
100            )
101        });
102        crate::styles::focus::draw_focus_ring(ui, &response, ui.active_theme().radius.md);
103
104        // Background - highlight if sync is enabled
105        let bg_color = if self.is_open || response.hovered() {
106            hover_color
107        } else if any_sync_enabled {
108            accent_color.gamma_multiply(0.3)
109        } else {
110            Color32::TRANSPARENT
111        };
112        ui.painter()
113            .rect_filled(rect, ui.active_theme().radius.md, bg_color);
114        ui.painter().rect_stroke(
115            rect,
116            ui.active_theme().radius.md,
117            Stroke::new(DESIGN_TOKENS.stroke.hairline, border_color),
118            StrokeKind::Outside,
119        );
120
121        // Sync icon (using layout grid as visual representation)
122        let icon_rect =
123            Rect::from_center_size(rect.center(), Vec2::splat(DESIGN_TOKENS.sizing.icon_sm));
124        let icon_color = if any_sync_enabled {
125            accent_color
126        } else if response.hovered() {
127            ui.visuals().widgets.hovered.fg_stroke.color
128        } else {
129            ui.visuals().widgets.noninteractive.fg_stroke.color
130        };
131        embedded_icons::LAYOUT_GRID
132            .as_image_tinted(Vec2::splat(DESIGN_TOKENS.sizing.icon_sm), icon_color)
133            .paint_at(ui, icon_rect);
134
135        response.on_hover_text(l10n::text(ui.ctx(), "Chart Sync Options"))
136    }
137
138    fn dropdown_rect(&self, ui: &Ui, btn_rect: Rect) -> Rect {
139        let item_height = DESIGN_TOKENS.sizing.button_md;
140        let item_count = 5; // 5 sync options
141        let dropdown_height = item_count as f32 * item_height + ui.active_theme().spacing.md * 2.0;
142        let dropdown_width = 180.0;
143        Rect::from_min_size(
144            Pos2::new(
145                btn_rect.min.x,
146                btn_rect.max.y + ui.active_theme().spacing.xs,
147            ),
148            Vec2::new(dropdown_width, dropdown_height),
149        )
150    }
151
152    fn draw_dropdown(&mut self, ui: &mut Ui, btn_rect: Rect) -> Option<ChartSyncOptions> {
153        let visuals = ui.style().visuals.clone();
154        let bg_color = visuals.window_fill;
155        let border_color = visuals.widgets.noninteractive.bg_stroke.color;
156        let hover_color = visuals.widgets.hovered.bg_fill;
157        let text_color = visuals.text_color();
158        let colors = ui.active_theme().ui;
159        let (accent_color, check_color) = (colors.accent, colors.accent_text);
160
161        let dropdown_rect = self.dropdown_rect(ui, btn_rect);
162
163        // Shadow
164        let shadow_rect = dropdown_rect.translate(Vec2::splat(DESIGN_TOKENS.shadow.offset_sm));
165        ui.painter().rect_filled(
166            shadow_rect,
167            ui.active_theme().radius.md,
168            Color32::from_black_alpha(60),
169        );
170
171        // Background
172        ui.painter()
173            .rect_filled(dropdown_rect, ui.active_theme().radius.md, bg_color);
174        ui.painter().rect_stroke(
175            dropdown_rect,
176            ui.active_theme().radius.md,
177            Stroke::new(DESIGN_TOKENS.stroke.hairline, border_color),
178            StrokeKind::Outside,
179        );
180
181        let mut changed = false;
182        let mut new_options = self.options.clone();
183        let item_height = DESIGN_TOKENS.sizing.button_md;
184        let mut y = dropdown_rect.min.y + ui.active_theme().spacing.sm;
185
186        // Render each sync option as a checkbox row
187        let sync_items = [
188            ("Time Axis", new_options.sync_time_axis, 0),
189            ("Crosshair", new_options.sync_crosshair, 1),
190            ("Drawings", new_options.sync_drawings, 2),
191            ("Symbol", new_options.sync_symbol, 3),
192            ("Timeframe", new_options.sync_timeframe, 4),
193        ];
194
195        for (label, enabled, idx) in sync_items {
196            let item_rect = Rect::from_min_size(
197                Pos2::new(dropdown_rect.min.x + ui.active_theme().spacing.sm, y),
198                Vec2::new(
199                    dropdown_rect.width() - ui.active_theme().spacing.md * 2.0,
200                    item_height,
201                ),
202            );
203
204            let item_response = ui.allocate_rect(item_rect, Sense::click());
205            let translated_label = l10n::text(ui.ctx(), label);
206            item_response.widget_info(|| {
207                egui::WidgetInfo::selected(
208                    egui::Role::CheckBox,
209                    item_response.enabled(),
210                    enabled,
211                    &translated_label,
212                )
213            });
214            crate::styles::focus::draw_focus_ring(ui, &item_response, ui.active_theme().radius.sm);
215
216            // Highlight on hover
217            if item_response.hovered() {
218                ui.painter()
219                    .rect_filled(item_rect, ui.active_theme().radius.sm, hover_color);
220            }
221
222            // Checkbox
223            let checkbox_size = 14.0;
224            let checkbox_rect = Rect::from_min_size(
225                Pos2::new(
226                    item_rect.min.x + ui.active_theme().spacing.sm,
227                    item_rect.center().y - checkbox_size / 2.0,
228                ),
229                Vec2::splat(checkbox_size),
230            );
231
232            if enabled {
233                ui.painter().rect_filled(checkbox_rect, 2.0, accent_color);
234                // Checkmark
235                ui.painter().text(
236                    checkbox_rect.center(),
237                    egui::Align2::CENTER_CENTER,
238                    l10n::text(ui.ctx(), "\u{2713}"),
239                    egui::FontId::proportional(checkbox_size * 0.8),
240                    check_color,
241                );
242            } else {
243                ui.painter().rect_stroke(
244                    checkbox_rect,
245                    2.0,
246                    Stroke::new(1.0_f32, border_color),
247                    StrokeKind::Inside,
248                );
249            }
250
251            // Label
252            ui.painter().text(
253                Pos2::new(
254                    item_rect.min.x
255                        + ui.active_theme().spacing.sm
256                        + checkbox_size
257                        + ui.active_theme().spacing.sm,
258                    item_rect.center().y,
259                ),
260                egui::Align2::LEFT_CENTER,
261                l10n::text(ui.ctx(), label),
262                egui::FontId::proportional(ui.active_theme().text.small),
263                text_color,
264            );
265
266            if item_response.clicked() {
267                // Toggle the appropriate option
268                match idx {
269                    0 => new_options.sync_time_axis = !enabled,
270                    1 => new_options.sync_crosshair = !enabled,
271                    2 => new_options.sync_drawings = !enabled,
272                    3 => new_options.sync_symbol = !enabled,
273                    4 => new_options.sync_timeframe = !enabled,
274                    _ => {}
275                }
276                changed = true;
277            }
278
279            y += item_height;
280        }
281
282        if changed { Some(new_options) } else { None }
283    }
284}