Skip to main content

egui_charts/ui/widget_bar/
tabs.rs

1//! Right Panel with Tab Navigation
2//!
3//! Displays a tabbed interface for Alerts and Object Tree panels.
4
5use crate::icons::{Icon, icons as embedded_icons};
6use crate::localization as l10n;
7use crate::styles::{icons, stroke};
8use crate::theme::ThemeExt;
9use crate::theme::{Theme, follow_active_theme, theme_derived};
10use crate::tokens::DESIGN_TOKENS;
11use egui::{Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2};
12
13/// Available tabs in the right panel
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum RightPanelTab {
16    /// Alerts panel.
17    Alerts,
18    /// Drawing and study object tree.
19    ObjectTree,
20}
21
22impl TryFrom<u8> for RightPanelTab {
23    type Error = ();
24
25    fn try_from(value: u8) -> Result<Self, Self::Error> {
26        match value {
27            0 => Ok(RightPanelTab::Alerts),
28            1 => Ok(RightPanelTab::ObjectTree),
29            _ => Err(()),
30        }
31    }
32}
33
34impl RightPanelTab {
35    /// Icon representing this value.
36    pub fn icon(&self) -> &'static Icon {
37        match self {
38            RightPanelTab::Alerts => &embedded_icons::BELL,
39            RightPanelTab::ObjectTree => &embedded_icons::OBJECT_TREE,
40        }
41    }
42
43    /// Display label for this value.
44    pub fn label(&self) -> &'static str {
45        match self {
46            RightPanelTab::Alerts => "Alerts",
47            RightPanelTab::ObjectTree => "Object Tree",
48        }
49    }
50}
51
52/// Configuration for the right panel tabs
53#[derive(Clone, Debug)]
54pub struct RightPanelTabsConfig {
55    /// Tab bar width, in egui logical points.
56    pub tab_bar_width: f32,
57    /// Tab button size, in egui logical points.
58    pub tab_button_size: f32,
59    /// Background color.
60    pub bg_color: Color32,
61    /// Tab bar background.
62    pub tab_bar_bg: Color32,
63    /// Active tab background.
64    pub active_tab_bg: Color32,
65    /// Hover background.
66    pub hover_bg: Color32,
67    /// Pressed background.
68    pub pressed_bg: Color32,
69    /// Icon color.
70    pub icon_color: Color32,
71    /// Active icon color.
72    pub active_icon_color: Color32,
73    /// Border color.
74    pub border_color: Color32,
75}
76
77impl RightPanelTabsConfig {
78    /// Create config from `theme`
79    pub fn from_theme(theme: &Theme) -> Self {
80        let ui = &theme.ui;
81        Self {
82            tab_bar_width: DESIGN_TOKENS.sizing.toolbar.right_icon_width,
83            tab_button_size: DESIGN_TOKENS.sizing.button_lg,
84            bg_color: ui.panel_bg,
85            tab_bar_bg: ui.panel_bg_secondary,
86            active_tab_bg: ui.panel_bg,
87            hover_bg: ui.btn_bg_hover,
88            pressed_bg: ui.btn_bg_active,
89            icon_color: ui.text_secondary,
90            active_icon_color: ui.accent,
91            border_color: ui.border,
92        }
93    }
94}
95
96impl Default for RightPanelTabsConfig {
97    fn default() -> Self {
98        Self::from_theme(&Theme::dark())
99    }
100}
101
102theme_derived!(RightPanelTabsConfig {
103    tab_bar_width,
104    tab_button_size,
105    bg_color,
106    tab_bar_bg,
107    active_tab_bg,
108    hover_bg,
109    pressed_bg,
110    icon_color,
111    active_icon_color,
112    border_color
113});
114
115/// Action returned by the right panel tabs
116#[derive(Clone, Debug, PartialEq)]
117#[non_exhaustive]
118pub enum RightPanelAction {
119    /// No action requested.
120    None,
121    /// Switch to the supplied panel tab.
122    TabChanged(RightPanelTab),
123    /// Close the panel.
124    Close,
125}
126
127/// Right Panel with Tab Navigation
128pub struct RightPanelTabs {
129    /// Currently selected tab.
130    pub active_tab: RightPanelTab,
131    /// Display and layout options for this component.
132    pub config: RightPanelTabsConfig,
133    /// Tabs offered in the panel, in display order.
134    pub available_tabs: Vec<RightPanelTab>,
135    /// Theme and config `config` was last derived from; `None` when the caller supplied it.
136    config_theme: Option<(Theme, RightPanelTabsConfig)>,
137}
138
139impl Default for RightPanelTabs {
140    fn default() -> Self {
141        Self {
142            active_tab: RightPanelTab::Alerts,
143            config: RightPanelTabsConfig::default(),
144            available_tabs: vec![RightPanelTab::Alerts, RightPanelTab::ObjectTree],
145            config_theme: Some((Theme::dark(), RightPanelTabsConfig::default())),
146        }
147    }
148}
149
150impl RightPanelTabs {
151    /// Create a [`RightPanelTabs`] with default settings.
152    pub fn new() -> Self {
153        Self::default()
154    }
155
156    /// Use the supplied display and layout options.
157    pub fn with_config(mut self, config: RightPanelTabsConfig) -> Self {
158        self.config = config;
159        self.config_theme = None;
160        self
161    }
162
163    /// Select the tab initially shown.
164    pub fn with_active_tab(mut self, tab: RightPanelTab) -> Self {
165        self.active_tab = tab;
166        self
167    }
168
169    /// Select and activate a tab programmatically
170    pub fn select_tab(&mut self, tab: RightPanelTab) {
171        self.active_tab = tab;
172    }
173
174    /// Show the tab bar on the right edge
175    /// Returns the action and consumed width for the tab bar
176    pub fn show_tab_bar(&mut self, ui: &mut Ui) -> RightPanelAction {
177        follow_active_theme(
178            ui.ctx(),
179            &mut self.config_theme,
180            &mut self.config,
181            RightPanelTabsConfig::from_theme,
182        );
183        let mut action = RightPanelAction::None;
184
185        let available_height = ui.available_height();
186        let (response, painter) = ui.allocate_painter(
187            Vec2::new(self.config.tab_bar_width, available_height),
188            Sense::hover(),
189        );
190        let rect = response.rect;
191
192        // Tab bar background
193        painter.rect_filled(rect, 0.0, self.config.tab_bar_bg);
194
195        // Left border
196        painter.line_segment(
197            [rect.left_top(), rect.left_bottom()],
198            Stroke::new(stroke::HAIRLINE, self.config.border_color),
199        );
200
201        let mut y = rect.min.y + ui.active_theme().spacing.lg;
202
203        // Draw each tab button
204        for tab in &self.available_tabs {
205            let is_active = *tab == self.active_tab;
206            let btn_rect = Rect::from_min_size(
207                Pos2::new(rect.min.x + ui.active_theme().spacing.sm, y),
208                Vec2::new(self.config.tab_button_size, self.config.tab_button_size),
209            );
210
211            // Check for clicks
212            let btn_res = ui.allocate_rect(btn_rect, Sense::click());
213            let translated_label = l10n::text(ui.ctx(), tab.label());
214            btn_res.widget_info(|| {
215                egui::WidgetInfo::labeled(egui::Role::Tab, btn_res.enabled(), &translated_label)
216            });
217            ui.ctx()
218                .accesskit_node_builder(btn_res.id, |node| node.set_selected(is_active));
219            crate::styles::focus::draw_focus_ring(ui, &btn_res, ui.active_theme().radius.md);
220            let is_hovered = btn_res.hovered();
221            let is_pressed = btn_res.is_pointer_button_down_on();
222
223            // Button background (with press feedback)
224            if is_active {
225                painter.rect_filled(
226                    btn_rect,
227                    ui.active_theme().radius.md,
228                    self.config.active_tab_bg,
229                );
230                // Active indicator on left edge
231                painter.rect_filled(
232                    Rect::from_min_size(
233                        Pos2::new(rect.min.x, btn_rect.min.y),
234                        Vec2::new(
235                            DESIGN_TOKENS.sizing.settings_dialog.indicator_width,
236                            self.config.tab_button_size,
237                        ),
238                    ),
239                    DESIGN_TOKENS.rounding.none,
240                    self.config.active_icon_color,
241                );
242            } else if is_pressed {
243                painter.rect_filled(
244                    btn_rect,
245                    ui.active_theme().radius.md,
246                    self.config.pressed_bg,
247                );
248            } else if is_hovered {
249                painter.rect_filled(btn_rect, ui.active_theme().radius.md, self.config.hover_bg);
250            }
251
252            // Draw icon
253            let icon_size = icons::SM;
254            let icon_rect = Rect::from_center_size(btn_rect.center(), Vec2::splat(icon_size));
255
256            // Get icon color based on state
257            let icon_color = if is_active || is_hovered {
258                ui.visuals().widgets.hovered.fg_stroke.color
259            } else {
260                ui.visuals().widgets.noninteractive.fg_stroke.color
261            };
262
263            tab.icon()
264                .as_image_tinted(Vec2::splat(icon_size), icon_color)
265                .paint_at(ui, icon_rect);
266
267            // Handle click
268            if btn_res.clicked() {
269                action = RightPanelAction::TabChanged(*tab);
270            }
271
272            // Tooltip
273            if is_hovered {
274                btn_res.on_hover_text(l10n::text(ui.ctx(), tab.label()));
275            }
276
277            y += self.config.tab_button_size + ui.active_theme().spacing.sm;
278        }
279
280        action
281    }
282
283    /// Show the header for the active tab content area
284    pub fn show_content_header(&self, ui: &mut Ui) -> Response {
285        let (response, painter) = ui.allocate_painter(
286            Vec2::new(
287                ui.available_width(),
288                DESIGN_TOKENS.sizing.context_menu.item_height,
289            ),
290            Sense::hover(),
291        );
292        let rect = response.rect;
293
294        // Background
295        painter.rect_filled(rect, DESIGN_TOKENS.rounding.none, self.config.bg_color);
296
297        // Bottom border
298        painter.line_segment(
299            [rect.left_bottom(), rect.right_bottom()],
300            Stroke::new(stroke::HAIRLINE, self.config.border_color),
301        );
302
303        // Tab title
304        let title_galley = painter.layout_no_wrap(
305            l10n::text(ui.ctx(), self.active_tab.label()),
306            egui::FontId::proportional(ui.active_theme().text.small),
307            ui.visuals().text_color(),
308        );
309        painter.galley(
310            Pos2::new(
311                rect.min.x + ui.active_theme().spacing.xl,
312                rect.center().y - title_galley.size().y / 2.0,
313            ),
314            title_galley,
315            Color32::TRANSPARENT,
316        );
317
318        response
319    }
320
321    /// Select the tab to display.
322    pub fn set_active_tab(&mut self, tab: RightPanelTab) {
323        self.active_tab = tab;
324    }
325
326    /// Currently selected panel tab.
327    pub fn active_tab(&self) -> RightPanelTab {
328        self.active_tab
329    }
330}