Skip to main content

egui_charts/ui/drawing_toolbar/components/
icon_button.rs

1//! Icon button components with theme-aware styling.
2//!
3//! Single icon btns used throughout the toolbar for tools,
4//! toggles, and actions.
5
6use crate::icons::Icon;
7use crate::localization as l10n;
8use crate::theme::ThemeExt;
9use egui::{
10    Color32, Rect, RectAlign, Response, Role, Sense, Stroke, StrokeKind, Tooltip, Ui, Vec2,
11    WidgetInfo,
12};
13
14use super::svg_helpers::{rail_icon_color, rail_icon_size, render_svg_at_rect_themed};
15use crate::tokens::DESIGN_TOKENS;
16
17/// Single icon button with theme-aware rendering.
18///
19/// Draws a square button with:
20/// - Hover background
21/// - Active/selected background
22/// - Centered SVG icon with theme colors
23///
24/// # Arguments
25///
26/// * `ui` - The egui UI context
27/// * `icon` - The Icon to display
28/// * `tooltip` - Hover tooltip text
29/// * `is_active` - Whether button is in active/selected state
30/// * `size` - Button size (square)
31///
32/// # Returns
33///
34/// The egui Response for click handling.
35pub fn draw_icon_btn(
36    ui: &mut Ui,
37    icon: &Icon,
38    tooltip: &'static str,
39    is_active: bool,
40    size: f32,
41) -> Response {
42    let (rect, response) = ui.allocate_exact_size(Vec2::splat(size), Sense::click());
43    crate::styles::focus::draw_focus_ring(ui, &response, ui.active_theme().radius.sm);
44
45    let bg_color = if is_active && response.hovered() {
46        super::active_hover_color(ui)
47    } else if is_active {
48        ui.visuals().widgets.active.bg_fill
49    } else if response.hovered() {
50        ui.visuals().widgets.hovered.bg_fill
51    } else {
52        Color32::TRANSPARENT // Let parent Frame's background show through
53    };
54
55    ui.painter()
56        .rect_filled(rect, ui.active_theme().radius.sm, bg_color);
57
58    render_svg_at_rect_themed(ui, icon, rect, response.hovered(), is_active);
59
60    let translated_label = l10n::text(ui.ctx(), tooltip);
61    response.widget_info(|| {
62        WidgetInfo::selected(
63            Role::Button,
64            response.enabled(),
65            is_active,
66            &translated_label,
67        )
68    });
69    response.on_hover_text(l10n::text(ui.ctx(), tooltip))
70}
71
72/// Toggle button with icon (alias for draw_icon_btn since functionally identical).
73///
74/// Used for toggleable states like magnet mode, keep drawing, lock, etc.
75///
76/// # Arguments
77///
78/// * `ui` - The egui UI context
79/// * `icon` - The Icon to display
80/// * `tooltip` - Hover tooltip text
81/// * `is_toggled` - Whether toggle is on
82/// * `size` - Button size (square)
83///
84/// # Returns
85///
86/// The egui Response for click handling.
87pub fn draw_toggle_btn(
88    ui: &mut Ui,
89    icon: &Icon,
90    tooltip: &'static str,
91    is_toggled: bool,
92    size: f32,
93) -> Response {
94    draw_icon_btn(ui, icon, tooltip, is_toggled, size)
95}
96
97/// Icon button with left padding.
98///
99/// Layout: [left_padding | icon_btn]
100/// Used for tool btns that need consistent alignment with PairButton.
101///
102/// # Arguments
103///
104/// * `ui` - The egui UI context
105/// * `icon` - The Icon to display
106/// * `tooltip` - Hover tooltip text
107/// * `is_active` - Whether button is in active/selected state
108/// * `icon_size` - Icon size
109/// * `padding` - Left padding amount
110///
111/// # Returns
112///
113/// The egui Response for click handling.
114pub fn draw_icon_button_padded(
115    ui: &mut Ui,
116    icon: &Icon,
117    tooltip: &str,
118    is_active: bool,
119    icon_size: f32,
120    padding: f32,
121) -> Response {
122    // Full rect for allocation
123    let size = Vec2::splat(icon_size + padding * 2.0);
124    let (rect, response) = ui.allocate_exact_size(size, Sense::click());
125    crate::styles::focus::draw_focus_ring(ui, &response, ui.active_theme().radius.sm);
126
127    // Add pressed state for click feedback
128    let bg_color = if is_active {
129        ui.visuals().widgets.active.bg_fill
130    } else if response.is_pointer_button_down_on() {
131        super::active_hover_color(ui)
132    } else if response.hovered() {
133        ui.visuals().widgets.hovered.bg_fill
134    } else {
135        Color32::TRANSPARENT // Let parent Frame's background show through
136    };
137
138    // Draw icon button - SQUARE, centered in button
139    let icon_button_rect = egui::Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
140    ui.painter()
141        .rect_filled(icon_button_rect, ui.active_theme().radius.sm, bg_color);
142
143    let is_hovered = response.hovered() || response.is_pointer_button_down_on();
144    render_svg_at_rect_themed(ui, icon, icon_button_rect, is_hovered, is_active);
145
146    let translated_label = l10n::text(ui.ctx(), tooltip);
147    response.widget_info(|| {
148        WidgetInfo::selected(
149            Role::Button,
150            response.enabled(),
151            is_active,
152            &translated_label,
153        )
154    });
155    response.on_hover_text(l10n::text(ui.ctx(), tooltip))
156}
157
158/// Left toolbar button (no dropdown arrow).
159///
160/// Layout:
161/// - Outer rect: 52×38px
162/// - Inner hover rect: 8.5px horizontal margin, 1.5px vertical margin
163/// - Inner rounding: the theme's medium radius
164/// - Icon: optically sized and centered in outer rect
165///
166/// A selected tool has a neutral fill; a latched utility uses contrasting ink and fill.
167/// The tooltip opens to the right of the bar, so it never covers the next button.
168///
169/// # Arguments
170///
171/// * `ui` - The egui UI context
172/// * `icon` - The Icon to display
173/// * `tooltip` - Hover tooltip text, also the accessible name
174/// * `is_selected` - Whether button is selected/active
175///
176/// # Returns
177///
178/// The egui Response for click handling.
179pub fn draw_tool_button(ui: &mut Ui, icon: &Icon, tooltip: &str, is_selected: bool) -> Response {
180    draw_rail_button(ui, icon, tooltip, is_selected, false)
181}
182
183/// Draw a utility whose latched state is distinct from the selected drawing tool.
184pub(crate) fn draw_latched_button(ui: &mut Ui, icon: &Icon, tooltip: &str, on: bool) -> Response {
185    draw_rail_button(ui, icon, tooltip, on, true)
186}
187
188fn draw_rail_button(
189    ui: &mut Ui,
190    icon: &Icon,
191    tooltip: &str,
192    is_selected: bool,
193    latched: bool,
194) -> Response {
195    let tokens = &DESIGN_TOKENS.sizing.toolbar;
196    let margin = Vec2::new(tokens.hover_margin_h - 0.5, tokens.hover_margin_v - 0.5);
197
198    let (rect, response) = ui.allocate_exact_size(
199        Vec2::new(tokens.button_width, tokens.button_height),
200        Sense::click(),
201    );
202    let translated_label = l10n::text(ui.ctx(), tooltip);
203    response.widget_info(|| {
204        WidgetInfo::selected(
205            Role::Button,
206            response.enabled(),
207            is_selected,
208            &translated_label,
209        )
210    });
211
212    if ui.is_rect_visible(rect) {
213        let theme = ui.active_theme();
214        let inner_rect = rect.shrink2(margin);
215
216        let selected_fill = if latched {
217            if theme.dark {
218                theme.ui.text
219            } else {
220                Color32::from_gray(26)
221            }
222        } else if theme.dark {
223            theme.ui.btn_bg_active
224        } else {
225            Color32::from_gray(235)
226        };
227        let selected_ink = if latched {
228            if theme.dark {
229                theme.ui.panel_bg
230            } else {
231                Color32::WHITE
232            }
233        } else {
234            rail_icon_color(ui)
235        };
236        let bg = match (is_selected, response.hovered()) {
237            (true, _) => Some(selected_fill),
238            (false, true) => Some(ui.visuals().widgets.hovered.bg_fill),
239            (false, false) => None,
240        };
241        if let Some(bg) = bg {
242            ui.painter().rect_filled(inner_rect, theme.radius.md, bg);
243        }
244        // The inner rect keeps all four sides of the ring inside the bar.
245        // A latched utility's ring follows its contrasting ink.
246        if response.has_focus() {
247            let (ring, ring_rect) = if is_selected {
248                (selected_ink, inner_rect.shrink(DESIGN_TOKENS.stroke.thick))
249            } else {
250                (crate::styles::focus::focus_ring_color(ui), inner_rect)
251            };
252            ui.painter().rect_stroke(
253                ring_rect,
254                theme.radius.md,
255                Stroke::new(DESIGN_TOKENS.stroke.thick, ring),
256                StrokeKind::Inside,
257            );
258        }
259
260        // Icon centered in OUTER rect
261        let icon_rect = Rect::from_center_size(rect.center(), rail_icon_size(icon));
262        let icon_color = if !ui.is_enabled() {
263            theme.ui.text_disabled
264        } else if is_selected {
265            selected_ink
266        } else {
267            rail_icon_color(ui)
268        };
269        icon.as_image_tinted(icon_rect.size(), icon_color)
270            .paint_at(ui, icon_rect);
271    }
272
273    let mut tip = Tooltip::for_enabled(&response);
274    tip.popup = tip.popup.align(RectAlign::RIGHT);
275    tip.show(|ui| ui.label(l10n::text(ui.ctx(), tooltip)));
276    response
277}