egui_charts/ui/chart_controls/
pane_controls.rs1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub struct PaneId(pub usize);
14
15#[derive(Debug, Clone, PartialEq)]
17#[non_exhaustive]
18pub enum PaneControlAction {
19 None,
21 Maximize(PaneId),
23 Minimize(PaneId),
25 Close(PaneId),
27 Settings(PaneId),
29}
30
31#[derive(Clone, Debug)]
33pub struct PaneControlsConfig {
34 pub button_size: f32,
36 pub button_gap: f32,
38 pub padding: f32,
40 pub rounding: f32,
42 pub offset_x: f32,
44 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
61pub struct PaneControls {
63 pub config: PaneControlsConfig,
65}
66
67impl Default for PaneControls {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl PaneControls {
74 pub fn new() -> Self {
76 Self {
77 config: PaneControlsConfig::default(),
78 }
79 }
80
81 pub fn with_config(mut self, config: PaneControlsConfig) -> Self {
83 self.config = config;
84 self
85 }
86
87 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 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 #[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 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 if !ui.is_rect_visible(bar_rect) {
169 return PaneControlAction::None;
170 }
171
172 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 if is_hovered {
206 ui.painter()
207 .rect_filled(btn_rect, ui.active_theme().radius.sm, btn_hover_bg);
208 }
209
210 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}