Skip to main content

egui_charts/ui/chart_controls/
pane_controls.rs

1//! Pane control buttons that appear at top-right of chart panes.
2//!
3//! Shows maximize, minimize, close, and settings buttons on hover.
4
5use crate::icons::{Icon, icons as embedded_icons};
6use crate::localization as l10n;
7use crate::theme::ThemeExt;
8use crate::tokens::DESIGN_TOKENS;
9use egui::{Pos2, Rect, Sense, Ui, Vec2};
10
11/// Unique identifier for a pane
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub struct PaneId(pub usize);
14
15/// Actions from pane controls
16#[derive(Debug, Clone, PartialEq)]
17#[non_exhaustive]
18pub enum PaneControlAction {
19    /// No action taken
20    None,
21    /// Maximize the specified pane
22    Maximize(PaneId),
23    /// Minimize the specified pane
24    Minimize(PaneId),
25    /// Close the specified pane
26    Close(PaneId),
27    /// Open settings for the specified pane
28    Settings(PaneId),
29}
30
31/// Configuration for pane controls
32#[derive(Clone, Debug)]
33pub struct PaneControlsConfig {
34    /// Button size (square)
35    pub button_size: f32,
36    /// Gap between buttons
37    pub button_gap: f32,
38    /// Padding inside the control bar
39    pub padding: f32,
40    /// Corner radius for the control bar background
41    pub rounding: f32,
42    /// Offset from pane corner
43    pub offset_x: f32,
44    /// Offset vertical, in egui logical points.
45    pub offset_y: f32,
46}
47
48impl Default for PaneControlsConfig {
49    fn default() -> Self {
50        Self {
51            button_size: DESIGN_TOKENS.sizing.button_sm,
52            button_gap: crate::theme::Theme::default().spacing.xs,
53            padding: crate::theme::Theme::default().spacing.xs,
54            rounding: crate::theme::Theme::default().radius.sm,
55            offset_x: crate::theme::Theme::default().spacing.sm,
56            offset_y: crate::theme::Theme::default().spacing.sm,
57        }
58    }
59}
60
61/// Pane control buttons (maximize, minimize, close, settings)
62pub struct PaneControls {
63    /// Display and layout options for this component.
64    pub config: PaneControlsConfig,
65}
66
67impl Default for PaneControls {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl PaneControls {
74    /// Create a new pane controls widget with default configuration
75    pub fn new() -> Self {
76        Self {
77            config: PaneControlsConfig::default(),
78        }
79    }
80
81    /// Set a custom configuration
82    pub fn with_config(mut self, config: PaneControlsConfig) -> Self {
83        self.config = config;
84        self
85    }
86
87    /// Show pane controls at the top-right of the given pane rect
88    /// Only shows if pane is hovered
89    /// Returns any action triggered
90    pub fn show(
91        &self,
92        ui: &mut Ui,
93        pane_rect: Rect,
94        pane_id: PaneId,
95        pane_hovered: bool,
96    ) -> PaneControlAction {
97        if !pane_hovered {
98            return PaneControlAction::None;
99        }
100
101        let theme = ui.active_theme();
102        let defaults = PaneControlsConfig::default();
103        let mut config = self.config.clone();
104        for (value, default, themed) in [
105            (
106                &mut config.button_gap,
107                defaults.button_gap,
108                theme.spacing.xs,
109            ),
110            (&mut config.padding, defaults.padding, theme.spacing.xs),
111            (&mut config.rounding, defaults.rounding, theme.radius.sm),
112            (&mut config.offset_x, defaults.offset_x, theme.spacing.sm),
113            (&mut config.offset_y, defaults.offset_y, theme.spacing.sm),
114        ] {
115            if *value == default {
116                *value = themed;
117            }
118        }
119
120        let mut action = PaneControlAction::None;
121
122        // Get colors from theme
123        let bg_color = ui.style().visuals.window_fill;
124        let border_stroke = ui.style().visuals.window_stroke;
125        let icon_color = ui.style().visuals.widgets.inactive.fg_stroke.color;
126        let icon_hover_color = ui.style().visuals.widgets.hovered.fg_stroke.color;
127        let btn_hover_bg = ui.style().visuals.widgets.hovered.bg_fill;
128
129        // Button definitions: (icon, tooltip, action_fn)
130        #[allow(clippy::type_complexity)]
131        let buttons: [(&Icon, &str, fn(PaneId) -> PaneControlAction); 4] = [
132            (
133                &embedded_icons::SETTINGS,
134                "Pane Settings",
135                PaneControlAction::Settings,
136            ),
137            (
138                &embedded_icons::REMOVE,
139                "Minimize",
140                PaneControlAction::Minimize,
141            ),
142            (
143                &embedded_icons::FULLSCREEN,
144                "Maximize",
145                PaneControlAction::Maximize,
146            ),
147            (
148                &embedded_icons::CLOSE,
149                "Close Pane",
150                PaneControlAction::Close,
151            ),
152        ];
153
154        let num_buttons = buttons.len() as f32;
155        let bar_width = (config.button_size * num_buttons)
156            + (config.button_gap * (num_buttons - 1.0))
157            + (config.padding * 2.0);
158        let bar_height = config.button_size + (config.padding * 2.0);
159
160        // Position at top-right of pane
161        let bar_pos = Pos2::new(
162            pane_rect.max.x - bar_width - config.offset_x,
163            pane_rect.min.y + config.offset_y,
164        );
165        let bar_rect = Rect::from_min_size(bar_pos, Vec2::new(bar_width, bar_height));
166
167        // Check if bar area is visible
168        if !ui.is_rect_visible(bar_rect) {
169            return PaneControlAction::None;
170        }
171
172        // Draw bar background
173        ui.painter().rect(
174            bar_rect,
175            config.rounding,
176            bg_color,
177            border_stroke,
178            egui::StrokeKind::Inside,
179        );
180
181        let mut x = bar_rect.min.x + config.padding;
182        let center_y = bar_rect.center().y;
183        let icon_size = 14.0;
184
185        for (icon, tooltip, action_fn) in buttons {
186            let btn_rect = Rect::from_center_size(
187                Pos2::new(x + config.button_size / 2.0, center_y),
188                Vec2::splat(config.button_size),
189            );
190
191            let btn_response = ui.allocate_rect(btn_rect, Sense::click());
192            let translated_label = l10n::text(ui.ctx(), tooltip);
193            btn_response.widget_info(|| {
194                egui::WidgetInfo::labeled(
195                    egui::Role::Button,
196                    btn_response.enabled(),
197                    &translated_label,
198                )
199            });
200            crate::styles::focus::draw_focus_ring(ui, &btn_response, ui.active_theme().radius.sm);
201            let is_hovered = btn_response.hovered();
202            let was_clicked = btn_response.clicked();
203
204            // Button background on hover
205            if is_hovered {
206                ui.painter()
207                    .rect_filled(btn_rect, ui.active_theme().radius.sm, btn_hover_bg);
208            }
209
210            // Icon
211            let icon_rect = Rect::from_center_size(btn_rect.center(), Vec2::splat(icon_size));
212            let icon_color_final = if is_hovered {
213                icon_hover_color
214            } else {
215                icon_color
216            };
217            icon.as_image_tinted(Vec2::splat(icon_size), icon_color_final)
218                .paint_at(ui, icon_rect);
219
220            btn_response.on_hover_text(l10n::text(ui.ctx(), tooltip));
221
222            if was_clicked {
223                action = action_fn(pane_id);
224            }
225
226            x += config.button_size + config.button_gap;
227        }
228
229        action
230    }
231}