1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum QuickTimeframe {
17 OneDay,
19 FiveDays,
21 OneMonth,
23 ThreeMonths,
25 SixMonths,
27 YearToDate,
29 OneYear,
31 FiveYears,
33 All,
35}
36
37impl QuickTimeframe {
38 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#[derive(Clone, Debug)]
56pub struct TimeframeBarConfig {
57 pub height: f32,
59 pub bg_color: Color32,
61 pub text_color: Color32,
63 pub active_text_color: Color32,
65 pub active_bg: Color32,
67 pub hover_bg: Color32,
69 pub border_color: Color32,
71 pub btn_padding: f32,
73}
74
75impl TimeframeBarConfig {
76 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, 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 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#[derive(Clone, Debug, PartialEq)]
112#[non_exhaustive]
113pub enum TimeframeBarAction {
114 None,
116 TimeframeSelected(QuickTimeframe),
118 DateRangeClicked,
120 AdjToggled(bool),
122}
123
124pub struct TimeframeBar {
126 pub config: TimeframeBarConfig,
128 pub active_timeframe: QuickTimeframe,
130 pub show_adj: bool,
132 pub adj_enabled: bool,
134 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 pub fn new() -> Self {
153 Self::default()
154 }
155
156 pub fn with_config(mut self, config: TimeframeBarConfig) -> Self {
158 self.config = config;
159 self.config_theme = None;
160 self
161 }
162
163 pub fn with_active_timeframe(mut self, tf: QuickTimeframe) -> Self {
165 self.active_timeframe = tf;
166 self
167 }
168
169 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 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 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 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 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 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 if is_active {
281 painter.rect_filled(btn_rect, ui.active_theme().radius.sm, self.config.active_bg);
282 } else if is_pressed {
283 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 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 let right_x = rect.max.x - 8.0;
320
321 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 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 pub fn set_active_timeframe(&mut self, tf: QuickTimeframe) {
411 self.active_timeframe = tf;
412 }
413}