Skip to main content

egui_charts/ui/timeframe_toolbar/
timeframe_bar.rs

1//! Date range preset buttons with an optional adjustment toggle.
2//!
3//! [`TimeframeBar`] returns the selected [`QuickTimeframe`] range for the host to
4//! apply to the viewport; these controls do not change the bar interval.
5
6use crate::icons::icons as embedded_icons;
7use crate::localization as l10n;
8use crate::theme::ThemeExt;
9use crate::theme::{Theme, follow_active_theme, theme_derived};
10use crate::tokens::DESIGN_TOKENS;
11use chrono::{Local, Timelike};
12use egui::{Color32, Pos2, Rect, Sense, Stroke, Ui, Vec2};
13
14/// Quick timeframe options
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum QuickTimeframe {
17    /// 1 Day
18    OneDay,
19    /// 5 Days
20    FiveDays,
21    /// 1 Month
22    OneMonth,
23    /// 3 Months
24    ThreeMonths,
25    /// 6 Months
26    SixMonths,
27    /// Year to Date
28    YearToDate,
29    /// 1 Year
30    OneYear,
31    /// 5 Years
32    FiveYears,
33    /// All available data
34    All,
35}
36
37impl QuickTimeframe {
38    /// Display label for this value.
39    pub fn label(&self) -> &'static str {
40        match self {
41            QuickTimeframe::OneDay => "1D",
42            QuickTimeframe::FiveDays => "5D",
43            QuickTimeframe::OneMonth => "1M",
44            QuickTimeframe::ThreeMonths => "3M",
45            QuickTimeframe::SixMonths => "6M",
46            QuickTimeframe::YearToDate => "YTD",
47            QuickTimeframe::OneYear => "1Y",
48            QuickTimeframe::FiveYears => "5Y",
49            QuickTimeframe::All => "All",
50        }
51    }
52}
53
54/// Configuration for the timeframe bar
55#[derive(Clone, Debug)]
56pub struct TimeframeBarConfig {
57    /// Height, in egui logical points.
58    pub height: f32,
59    /// Background color.
60    pub bg_color: Color32,
61    /// Text color.
62    pub text_color: Color32,
63    /// Active text color.
64    pub active_text_color: Color32,
65    /// Active background.
66    pub active_bg: Color32,
67    /// Hover background.
68    pub hover_bg: Color32,
69    /// Border color.
70    pub border_color: Color32,
71    /// Button padding, in egui logical points.
72    pub btn_padding: f32,
73}
74
75impl TimeframeBarConfig {
76    /// Create config from `theme`
77    pub fn from_theme(theme: &Theme) -> Self {
78        let ui = &theme.ui;
79        Self {
80            height: DESIGN_TOKENS.sizing.toolbar.bottom_height,
81            bg_color: Color32::TRANSPARENT, // Let parent Frame show through
82            text_color: ui.text_secondary,
83            active_text_color: ui.text,
84            active_bg: ui.btn_bg_active,
85            hover_bg: ui.btn_bg_hover,
86            border_color: ui.border,
87            btn_padding: theme.spacing.lg,
88        }
89    }
90}
91
92impl Default for TimeframeBarConfig {
93    fn default() -> Self {
94        // Default uses light UI chrome theme
95        Self::from_theme(&Theme::dark())
96    }
97}
98
99theme_derived!(TimeframeBarConfig {
100    height,
101    bg_color,
102    text_color,
103    active_text_color,
104    active_bg,
105    hover_bg,
106    border_color,
107    btn_padding
108});
109
110/// Action returned by the timeframe bar
111#[derive(Clone, Debug, PartialEq)]
112#[non_exhaustive]
113pub enum TimeframeBarAction {
114    /// No action taken
115    None,
116    /// A quick timeframe was selected
117    TimeframeSelected(QuickTimeframe),
118    /// The date range selector icon was clicked
119    DateRangeClicked,
120    /// Dividend adjustment toggled (new state)
121    AdjToggled(bool),
122}
123
124/// Bottom Timeframe Quick-Select Bar
125pub struct TimeframeBar {
126    /// Display and layout options for this component.
127    pub config: TimeframeBarConfig,
128    /// Currently selected date range preset.
129    pub active_timeframe: QuickTimeframe,
130    /// Whether to display the adjustment toggle.
131    pub show_adj: bool,
132    /// Whether the adjustment toggle is selected.
133    pub adj_enabled: bool,
134    /// Theme and config `config` was last derived from; `None` when the caller supplied it.
135    config_theme: Option<(Theme, TimeframeBarConfig)>,
136}
137
138impl Default for TimeframeBar {
139    fn default() -> Self {
140        Self {
141            config: TimeframeBarConfig::default(),
142            active_timeframe: QuickTimeframe::OneDay,
143            show_adj: true,
144            adj_enabled: true,
145            config_theme: Some((Theme::dark(), TimeframeBarConfig::default())),
146        }
147    }
148}
149
150impl TimeframeBar {
151    /// Create a new timeframe bar with default configuration
152    pub fn new() -> Self {
153        Self::default()
154    }
155
156    /// Set a custom configuration
157    pub fn with_config(mut self, config: TimeframeBarConfig) -> Self {
158        self.config = config;
159        self.config_theme = None;
160        self
161    }
162
163    /// Set the initially active timeframe
164    pub fn with_active_timeframe(mut self, tf: QuickTimeframe) -> Self {
165        self.active_timeframe = tf;
166        self
167    }
168
169    /// Show the timeframe bar
170    pub fn show(&mut self, ui: &mut Ui) -> TimeframeBarAction {
171        follow_active_theme(
172            ui.ctx(),
173            &mut self.config_theme,
174            &mut self.config,
175            TimeframeBarConfig::from_theme,
176        );
177        let mut action = TimeframeBarAction::None;
178
179        let (response, painter) = ui.allocate_painter(
180            Vec2::new(ui.available_width(), self.config.height),
181            Sense::hover(),
182        );
183        let rect = response.rect;
184
185        // Background - REMOVED: Let parent Frame's fill show through
186        // painter.rect_filled(rect, 0.0, self.config.bg_color);
187
188        // Top border
189        painter.line_segment(
190            [rect.left_top(), rect.right_top()],
191            Stroke::new(DESIGN_TOKENS.stroke.hairline, self.config.border_color),
192        );
193
194        let mut x = rect.min.x + 8.0;
195        let center_y = rect.center().y;
196
197        // Date range selector icon
198        let date_icon_rect = Rect::from_center_size(
199            Pos2::new(x + 10.0, center_y),
200            Vec2::splat(DESIGN_TOKENS.sizing.icon_md),
201        );
202        let date_res = ui.allocate_rect(date_icon_rect, Sense::click());
203        let translated_label = l10n::text(ui.ctx(), "Go to date");
204        date_res.widget_info(|| {
205            egui::WidgetInfo::labeled(egui::Role::Button, date_res.enabled(), &translated_label)
206        });
207        crate::styles::focus::draw_focus_ring(ui, &date_res, ui.active_theme().radius.sm);
208        if date_res.hovered() {
209            painter.rect_filled(
210                date_icon_rect,
211                ui.active_theme().radius.sm,
212                self.config.hover_bg,
213            );
214        }
215        // Render calendar icon
216        let icon_size = 12.0;
217        let icon_rect = Rect::from_center_size(date_icon_rect.center(), Vec2::splat(icon_size));
218        embedded_icons::WIDGET_BAR_CALENDAR
219            .as_image_tinted(Vec2::splat(icon_size), self.config.text_color)
220            .paint_at(ui, icon_rect);
221        if date_res.clicked() {
222            action = TimeframeBarAction::DateRangeClicked;
223        }
224
225        x += 28.0;
226
227        // Separator
228        painter.line_segment(
229            [
230                Pos2::new(x, rect.min.y + 4.0),
231                Pos2::new(x, rect.max.y - 4.0),
232            ],
233            Stroke::new(DESIGN_TOKENS.stroke.hairline, self.config.border_color),
234        );
235        x += 8.0;
236
237        // Timeframe btns
238        let timeframes = [
239            QuickTimeframe::OneDay,
240            QuickTimeframe::FiveDays,
241            QuickTimeframe::OneMonth,
242            QuickTimeframe::ThreeMonths,
243            QuickTimeframe::SixMonths,
244            QuickTimeframe::YearToDate,
245            QuickTimeframe::OneYear,
246            QuickTimeframe::FiveYears,
247            QuickTimeframe::All,
248        ];
249
250        for tf in timeframes.iter() {
251            let label = tf.label();
252            let galley = painter.layout_no_wrap(
253                l10n::text(ui.ctx(), label),
254                egui::FontId::proportional(ui.active_theme().text.small),
255                self.config.text_color,
256            );
257
258            let btn_width = galley.size().x + self.config.btn_padding * 2.0;
259            let btn_rect = Rect::from_min_size(
260                Pos2::new(x, rect.min.y + 4.0),
261                Vec2::new(btn_width, self.config.height - 8.0),
262            );
263
264            let btn_res = ui.allocate_rect(btn_rect, Sense::click());
265            crate::styles::focus::draw_focus_ring(ui, &btn_res, ui.active_theme().radius.sm);
266            let is_active = *tf == self.active_timeframe;
267            let translated_label = l10n::text(ui.ctx(), label);
268            btn_res.widget_info(|| {
269                egui::WidgetInfo::selected(
270                    egui::Role::Button,
271                    btn_res.enabled(),
272                    is_active,
273                    &translated_label,
274                )
275            });
276            let is_hovered = btn_res.hovered();
277            let is_pressed = btn_res.is_pointer_button_down_on();
278
279            // Button background (with press feedback)
280            if is_active {
281                painter.rect_filled(btn_rect, ui.active_theme().radius.sm, self.config.active_bg);
282            } else if is_pressed {
283                // Pressed state - use active background
284                painter.rect_filled(btn_rect, ui.active_theme().radius.sm, self.config.active_bg);
285            } else if is_hovered {
286                painter.rect_filled(btn_rect, ui.active_theme().radius.sm, self.config.hover_bg);
287            }
288
289            // Button text
290            let text_color = if is_active || is_pressed {
291                self.config.active_text_color
292            } else {
293                self.config.text_color
294            };
295
296            let text_galley = painter.layout_no_wrap(
297                l10n::text(ui.ctx(), label),
298                egui::FontId::proportional(ui.active_theme().text.small),
299                text_color,
300            );
301            painter.galley(
302                Pos2::new(
303                    btn_rect.center().x - text_galley.size().x / 2.0,
304                    center_y - text_galley.size().y / 2.0,
305                ),
306                text_galley,
307                Color32::TRANSPARENT,
308            );
309
310            if btn_res.clicked() {
311                self.active_timeframe = *tf;
312                action = TimeframeBarAction::TimeframeSelected(*tf);
313            }
314
315            x += btn_width + 4.0;
316        }
317
318        // Right side - current time and ADJ toggle
319        let right_x = rect.max.x - 8.0;
320
321        // ADJ toggle
322        if self.show_adj {
323            let adj_text = "ADJ";
324            let adj_galley = painter.layout_no_wrap(
325                l10n::text(ui.ctx(), adj_text),
326                egui::FontId::proportional(ui.active_theme().text.tiny),
327                if self.adj_enabled {
328                    self.config.active_text_color
329                } else {
330                    self.config.text_color
331                },
332            );
333            let adj_button_rect = Rect::from_min_size(
334                Pos2::new(
335                    right_x - adj_galley.size().x - 12.0 - 70.0,
336                    rect.min.y + 4.0,
337                ),
338                Vec2::new(adj_galley.size().x + 12.0, self.config.height - 8.0),
339            );
340            let adj_res = ui.allocate_rect(adj_button_rect, Sense::click());
341            let translated_label = l10n::text(ui.ctx(), "Adjust for dividends");
342            adj_res.widget_info(|| {
343                egui::WidgetInfo::selected(
344                    egui::Role::Button,
345                    adj_res.enabled(),
346                    self.adj_enabled,
347                    &translated_label,
348                )
349            });
350            crate::styles::focus::draw_focus_ring(ui, &adj_res, ui.active_theme().radius.sm);
351
352            if adj_res.hovered() || self.adj_enabled {
353                painter.rect_filled(
354                    adj_button_rect,
355                    ui.active_theme().radius.sm,
356                    self.config.active_bg,
357                );
358            }
359
360            let adj_draw_galley = painter.layout_no_wrap(
361                l10n::text(ui.ctx(), adj_text),
362                egui::FontId::proportional(ui.active_theme().text.tiny),
363                if self.adj_enabled {
364                    self.config.active_text_color
365                } else {
366                    self.config.text_color
367                },
368            );
369            painter.galley(
370                Pos2::new(
371                    adj_button_rect.center().x - adj_draw_galley.size().x / 2.0,
372                    center_y - adj_draw_galley.size().y / 2.0,
373                ),
374                adj_draw_galley,
375                Color32::TRANSPARENT,
376            );
377
378            if adj_res.clicked() {
379                self.adj_enabled = !self.adj_enabled;
380                action = TimeframeBarAction::AdjToggled(self.adj_enabled);
381            }
382        }
383
384        // Current time (UTC)
385        let now = Local::now();
386        let time_str = format!(
387            "{:02}:{:02}:{:02} UTC",
388            now.hour(),
389            now.minute(),
390            now.second()
391        );
392        let time_galley = painter.layout_no_wrap(
393            l10n::text(ui.ctx(), time_str),
394            egui::FontId::proportional(ui.active_theme().text.tiny),
395            self.config.text_color,
396        );
397        painter.galley(
398            Pos2::new(
399                right_x - time_galley.size().x,
400                center_y - time_galley.size().y / 2.0,
401            ),
402            time_galley,
403            Color32::TRANSPARENT,
404        );
405
406        action
407    }
408
409    /// Select the displayed date range preset.
410    pub fn set_active_timeframe(&mut self, tf: QuickTimeframe) {
411        self.active_timeframe = tf;
412    }
413}