egui_charts/ui/top_toolbar/components/
sync_button.rs1use 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
13pub struct SyncButton {
15 is_open: bool,
17 options: ChartSyncOptions,
19}
20
21impl Default for SyncButton {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl SyncButton {
28 pub fn new() -> Self {
30 Self {
31 is_open: false,
32 options: ChartSyncOptions::default(),
33 }
34 }
35
36 pub fn set_options(&mut self, options: ChartSyncOptions) {
38 self.options = options;
39 }
40
41 pub fn options(&self) -> &ChartSyncOptions {
43 &self.options
44 }
45
46 pub fn show(&mut self, ui: &mut Ui) -> Option<ChartSyncOptions> {
48 let mut result = None;
49
50 let btn_response = self.draw_button(ui);
52 if btn_response.clicked() {
53 self.is_open = !self.is_open;
54 }
55
56 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 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 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 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 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; 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 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 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 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 if item_response.hovered() {
218 ui.painter()
219 .rect_filled(item_rect, ui.active_theme().radius.sm, hover_color);
220 }
221
222 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 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 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 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}