agg_gui/widgets/button.rs
1//! `Button` — a clickable, compositional button with a `Label` child.
2
3// `handle_event` lives in `button_events.rs` as a child module so it
4// has direct access to Button's private fields/methods, and lifts the
5// 90-line event-dispatch block out of this file to keep it under the
6// 800-line cap.
7#[path = "button_events.rs"]
8mod events;
9
10use std::rc::Rc;
11use std::sync::Arc;
12
13use crate::color::Color;
14use crate::draw_ctx::DrawCtx;
15use crate::event::{Event, EventResult};
16use crate::geometry::{Rect, Size};
17use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
18use crate::text::{measure_advance, Font};
19use crate::widget::Widget;
20use crate::widgets::label::{Label, LabelAlign};
21
22/// Icon glyph drawn at the leading edge of a [`Button`]'s label.
23/// The glyph is rendered with a separate font so callers can pair
24/// e.g. a Font Awesome glyph with a Latin-only text font.
25#[derive(Clone)]
26pub struct ButtonIcon {
27 pub glyph: char,
28 pub font: Arc<Font>,
29 pub font_size: f64,
30}
31
32/// Spacing between the icon glyph and the label text, in pixels.
33const ICON_GAP: f64 = 8.0;
34
35/// Default horizontal padding used to inset a left- or right-aligned label
36/// from the button edge. Center-aligned labels ignore this and centre
37/// inside the button bounds.
38const LEFT_LABEL_PAD: f64 = 8.0;
39
40pub use super::button_theme::ButtonTheme;
41
42/// A clickable button.
43///
44/// Build with [`Button::new`] and optionally chain builder methods.
45pub struct Button {
46 bounds: Rect,
47 /// Always exactly one child: the `Label` for the button's text.
48 children: Vec<Box<dyn Widget>>,
49 base: WidgetBase,
50 /// Source of truth for the label text, kept so `build_label` can rebuild.
51 label_text: String,
52 font: Arc<Font>,
53 font_size: f64,
54 pub theme: ButtonTheme,
55 on_click: Option<Box<dyn FnMut()>>,
56 /// Optional gate: when `Some`, the button is enabled only while the
57 /// closure returns `true`. Queried each paint / event so the caller
58 /// can base it on live state (e.g. "only enable Relaunch when the
59 /// selected MSAA differs from the running one") without rebuilding
60 /// the widget tree. `None` = always enabled.
61 enabled_fn: Option<Rc<dyn Fn() -> bool>>,
62 /// Optional toggle: when `Some` and the closure returns `true`, the
63 /// button paints with the accent / selected appearance regardless of
64 /// hover / press state. When the closure returns `false`, an active-
65 /// aware button uses the subtle (`widget_bg`) variant so segmented
66 /// selectors look right. `None` = legacy behaviour: always painted as
67 /// the accent button.
68 active_fn: Option<Rc<dyn Fn() -> bool>>,
69 /// `true` selects the muted "secondary" visual style (theme widget_bg
70 /// + theme text colour) instead of the accent appearance. Combined
71 /// with `active_fn`, this drives segmented toggles: each segment is a
72 /// subtle button that flips to the accent look when its `active_fn`
73 /// returns true.
74 subtle: bool,
75 /// When `true` AND in the inactive state, the inactive background
76 /// is fully transparent (no fill) so the button reads as part of
77 /// its parent — sidebar list rows want this. Hovered / pressed
78 /// inactive states paint a faint text-coloured overlay instead of
79 /// the `widget_bg` shade. Active state is unaffected.
80 ghost: bool,
81 /// When `true`, draw a 1-px stroke around the button rect using the
82 /// theme's `widget_stroke` colour while inactive — gives subtle
83 /// segmented buttons a defined edge so they don't visually bleed
84 /// into a parent that has the same `widget_bg` shade. Active state
85 /// already has a high-contrast accent fill and skips the stroke.
86 outlined: bool,
87 /// How the child label is positioned inside the button rect.
88 /// `Center` (default) centres horizontally; `Left` insets by
89 /// [`LEFT_LABEL_PAD`] and is the right choice for full-width
90 /// sidebar rows where the label hugs the leading edge.
91 label_align: LabelAlign,
92 /// Custom horizontal inset applied when `label_align` is `Left` or
93 /// `Right`. Defaults to [`LEFT_LABEL_PAD`]; sidebar entries with
94 /// indent > 0 set this to push the label past a group-marker
95 /// triangle.
96 label_pad_h: f64,
97
98 /// Optional icon glyph painted at the leading edge of the label.
99 /// See [`with_icon`](Self::with_icon).
100 icon: Option<ButtonIcon>,
101
102 /// When true, drop the 48 px touch-target width floor and shrink
103 /// the horizontal padding. Right for icon-only toolbar buttons
104 /// that want to sit tightly next to each other; defaults false
105 /// so regular buttons keep the comfortable touch target.
106 compact: bool,
107
108 hovered: bool,
109 pressed: bool,
110 focused: bool,
111}
112
113impl Button {
114 /// Create a button with the given label.
115 pub fn new(label: impl Into<String>, font: Arc<Font>) -> Self {
116 let label_text: String = label.into();
117 let font_size = 14.0;
118 let theme = ButtonTheme::default();
119 let child = Self::build_label(&label_text, &font, font_size, &theme);
120 Self {
121 bounds: Rect::default(),
122 children: vec![child],
123 base: WidgetBase::new(),
124 label_text,
125 font,
126 font_size,
127 theme,
128 on_click: None,
129 enabled_fn: None,
130 active_fn: None,
131 subtle: false,
132 ghost: false,
133 outlined: false,
134 label_align: LabelAlign::Center,
135 label_pad_h: LEFT_LABEL_PAD,
136 icon: None,
137 compact: false,
138 hovered: false,
139 pressed: false,
140 focused: false,
141 }
142 }
143
144 pub fn with_font_size(mut self, size: f64) -> Self {
145 self.font_size = size;
146 self.children[0] = Self::build_label(&self.label_text, &self.font, size, &self.theme);
147 self
148 }
149
150 pub fn with_theme(mut self, theme: ButtonTheme) -> Self {
151 self.theme = theme;
152 self.children[0] =
153 Self::build_label(&self.label_text, &self.font, self.font_size, &self.theme);
154 self
155 }
156
157 pub fn on_click(mut self, cb: impl FnMut() + 'static) -> Self {
158 self.on_click = Some(Box::new(cb));
159 self
160 }
161
162 /// Gate the button on a live predicate. Returned-`false` frames paint
163 /// the button in its disabled style and ignore mouse / keyboard input.
164 pub fn with_enabled_fn(mut self, f: impl Fn() -> bool + 'static) -> Self {
165 self.enabled_fn = Some(Rc::new(f));
166 self
167 }
168
169 /// Bind the button's "selected" state to a live predicate. When the
170 /// closure returns `true`, the button paints with the accent surface
171 /// regardless of hover / press; when it returns `false`, an
172 /// active-aware button (i.e. `with_subtle()` is also set) reverts to
173 /// the muted `widget_bg` appearance. Used to compose segmented
174 /// toggles out of plain `Button`s without hand-rolled paint code.
175 pub fn with_active_fn(mut self, f: impl Fn() -> bool + 'static) -> Self {
176 self.active_fn = Some(Rc::new(f));
177 self
178 }
179
180 /// Override how the child label is aligned inside the button rect.
181 /// Defaults to [`LabelAlign::Center`]. Use [`LabelAlign::Left`] for
182 /// full-width sidebar rows where the label hugs the leading edge.
183 /// Also rebuilds the child Label so its own internal alignment matches.
184 pub fn with_label_align(mut self, align: LabelAlign) -> Self {
185 self.label_align = align;
186 self.children[0] = Box::new(
187 Label::new(&self.label_text, Arc::clone(&self.font))
188 .with_font_size(self.font_size)
189 .with_color(self.theme.label_color)
190 .with_align(align),
191 );
192 self
193 }
194
195 /// Override the horizontal padding used when `label_align` is `Left`
196 /// or `Right`. Defaults to a small visual gutter; bump it up to indent
197 /// the label past a group-marker triangle in sidebar rows.
198 pub fn with_label_pad_h(mut self, pad: f64) -> Self {
199 self.label_pad_h = pad;
200 self
201 }
202
203 /// Compact mode: drop the 48 px width floor and use a tighter
204 /// horizontal padding. Use for icon-only toolbar buttons where
205 /// you want them packed close to the glyph; the 48 px touch-
206 /// target default is right for stand-alone buttons but wastes
207 /// horizontal space when 5+ icon buttons need to sit next to
208 /// each other on a narrow mobile bar.
209 pub fn with_compact(mut self) -> Self {
210 self.compact = true;
211 self
212 }
213
214 /// Paint an icon glyph at the leading edge of the label.
215 /// `icon_font` carries the glyph (e.g. a Font Awesome face);
216 /// the label text continues to render in the button's main
217 /// font, so callers can pair a Latin text font with an
218 /// icon-only font without merging them.
219 ///
220 /// Defaults `font_size` to the button's current `font_size`.
221 /// Use [`with_icon_sized`](Self::with_icon_sized) to scale the
222 /// icon independently.
223 pub fn with_icon(mut self, glyph: char, icon_font: Arc<Font>) -> Self {
224 let font_size = self.font_size;
225 self.icon = Some(ButtonIcon {
226 glyph,
227 font: icon_font,
228 font_size,
229 });
230 self
231 }
232
233 /// Like [`with_icon`](Self::with_icon) but with an explicit
234 /// icon font size — useful when the icon font's glyphs read
235 /// larger or smaller than the text at the same point size.
236 pub fn with_icon_sized(mut self, glyph: char, icon_font: Arc<Font>, font_size: f64) -> Self {
237 self.icon = Some(ButtonIcon {
238 glyph,
239 font: icon_font,
240 font_size,
241 });
242 self
243 }
244
245 /// Use a transparent inactive background + faint text-coloured
246 /// hover/pressed overlay instead of the muted `widget_bg` fill.
247 /// Implies [`with_subtle`] (theme text colour, accent on active).
248 /// Right for sidebar list rows where the inactive state should
249 /// blend with the panel.
250 pub fn with_ghost(mut self) -> Self {
251 self.subtle = true;
252 self.ghost = true;
253 let theme_text = crate::theme::current_visuals().text_color;
254 self.children[0] =
255 Self::build_label_with_color(&self.label_text, &self.font, self.font_size, theme_text);
256 self
257 }
258
259 /// Switch to the muted (secondary) visual style: theme `widget_bg`
260 /// fill, theme `text_color` label. Pair with [`with_active_fn`] to
261 /// build segmented controls — inactive segments paint subtle, the
262 /// selected segment flips to the accent surface.
263 /// Draw a 1-px `widget_stroke` outline around the button while inactive.
264 /// Combined with [`Self::with_subtle`] this gives top-bar segmented
265 /// controls a defined edge so they don't visually bleed into a parent
266 /// that shares the same `widget_bg` colour. Active state already paints
267 /// a high-contrast accent fill and skips the stroke.
268 pub fn with_outlined(mut self) -> Self {
269 self.outlined = true;
270 self
271 }
272
273 pub fn with_subtle(mut self) -> Self {
274 self.subtle = true;
275 // Subtle buttons use the theme's text colour, not the white-on-accent
276 // default. Rebuild the label with the active visuals' text colour
277 // (the paint pass also retints each frame, so this just gives a
278 // sensible first-paint colour before the visuals are queried).
279 let theme_text = crate::theme::current_visuals().text_color;
280 self.children[0] =
281 Self::build_label_with_color(&self.label_text, &self.font, self.font_size, theme_text);
282 self
283 }
284
285 fn is_enabled(&self) -> bool {
286 self.enabled_fn.as_ref().map(|f| f()).unwrap_or(true)
287 }
288
289 fn is_active(&self) -> bool {
290 self.active_fn.as_ref().map(|f| f()).unwrap_or(true)
291 }
292
293 /// Spacing reserved between the leading icon glyph and the label.
294 /// Collapses to zero for icon-only buttons (empty label) so the glyph
295 /// centres in the button instead of being shoved left by a gap that
296 /// precedes no text.
297 fn icon_gap(&self) -> f64 {
298 if self.label_text.is_empty() {
299 0.0
300 } else {
301 ICON_GAP
302 }
303 }
304
305 pub fn with_margin(mut self, m: Insets) -> Self {
306 self.base.margin = m;
307 self
308 }
309 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
310 self.base.h_anchor = h;
311 self
312 }
313 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
314 self.base.v_anchor = v;
315 self
316 }
317 pub fn with_min_size(mut self, s: Size) -> Self {
318 self.base.min_size = s;
319 self
320 }
321 pub fn with_max_size(mut self, s: Size) -> Self {
322 self.base.max_size = s;
323 self
324 }
325
326 fn fire_click(&mut self) {
327 if let Some(cb) = self.on_click.as_mut() {
328 cb();
329 }
330 }
331
332 fn position_label(&mut self, size: Size, label_size: Size) {
333 // Width contributed by the leading icon glyph (icon advance
334 // + spacing gap). Zero when no icon is configured.
335 let icon_block_w = self
336 .icon
337 .as_ref()
338 .map(|i| measure_advance(&i.font, &i.glyph.to_string(), i.font_size) + self.icon_gap())
339 .unwrap_or(0.0);
340 // The (icon + gap + label) group is positioned as a unit;
341 // align uses the COMBINED width so the icon stays directly
342 // left of the label for any alignment mode.
343 let group_w = label_size.width + icon_block_w;
344 let group_x = match self.label_align {
345 LabelAlign::Left => self.label_pad_h.min(size.width),
346 LabelAlign::Right => (size.width - group_w - self.label_pad_h).max(0.0),
347 LabelAlign::Center => ((size.width - group_w) * 0.5).max(0.0),
348 };
349 let label_x = group_x + icon_block_w;
350 let label_y = ((size.height - label_size.height) * 0.5).max(0.0);
351 self.children[0].set_bounds(Rect::new(
352 label_x,
353 label_y,
354 label_size.width,
355 label_size.height,
356 ));
357 }
358
359 fn disabled_colors(v: &crate::theme::Visuals) -> (Color, Color, Color) {
360 let luma = v.bg_color.r * 0.299 + v.bg_color.g * 0.587 + v.bg_color.b * 0.114;
361 if luma < 0.5 {
362 (
363 v.window_fill,
364 Color::rgba(1.0, 1.0, 1.0, 0.22),
365 v.text_dim.with_alpha(0.42),
366 )
367 } else {
368 (v.track_bg, v.widget_stroke.with_alpha(0.45), v.text_dim)
369 }
370 }
371
372 /// Construct a label child from the button's current state.
373 ///
374 /// Called from `new()`, `with_theme()`, and `with_font_size()` so the
375 /// child always reflects the button's configuration.
376 fn build_label(
377 text: &str,
378 font: &Arc<Font>,
379 font_size: f64,
380 theme: &ButtonTheme,
381 ) -> Box<dyn Widget> {
382 Self::build_label_with_color(text, font, font_size, theme.label_color)
383 }
384
385 fn build_label_with_color(
386 text: &str,
387 font: &Arc<Font>,
388 font_size: f64,
389 color: Color,
390 ) -> Box<dyn Widget> {
391 Box::new(
392 Label::new(text, Arc::clone(font))
393 .with_font_size(font_size)
394 .with_color(color)
395 .with_align(LabelAlign::Center),
396 )
397 }
398
399 /// Render the configured icon glyph centred vertically in the
400 /// button using the glyph's *actual* outline bounding box — not
401 /// the font's worst-case ascender/descender. Icon fonts (Font
402 /// Awesome especially) place each glyph in a sub-rectangle of
403 /// the design space; centring by the font metric leaves the glyph
404 /// visibly high on the button (the "icons floating to the top"
405 /// regression we've hit repeatedly). With the per-glyph bbox we
406 /// solve for the baseline that puts the glyph's vertical midpoint
407 /// at `button_h / 2`.
408 fn paint_icon(
409 ctx: &mut dyn DrawCtx,
410 icon: &Option<ButtonIcon>,
411 _label_font: &Arc<Font>,
412 _label_font_size: f64,
413 x: f64,
414 button_h: f64,
415 color: Color,
416 ) {
417 let Some(icon) = icon else { return };
418 // (y_min, y_max) is the glyph's actual extent in pixels
419 // relative to baseline, Y-up. y_min is usually negative
420 // (descender region) or ~0, y_max is the cap-height of the
421 // glyph. Pick the baseline so that
422 // baseline + (y_min + y_max) / 2 == button_h / 2
423 // i.e. the glyph's midpoint sits at the button's midpoint.
424 // Fall back to the font metric only if the glyph has no
425 // outline (e.g. a space or a missing glyph).
426 let baseline_y = match icon.font.glyph_visual_bounds(icon.glyph, icon.font_size) {
427 Some((y_min, y_max)) => (button_h * 0.5 - (y_min + y_max) * 0.5).max(0.0),
428 None => ((button_h - icon.font_size) * 0.5).max(0.0),
429 };
430 ctx.set_font(Arc::clone(&icon.font));
431 ctx.set_font_size(icon.font_size);
432 ctx.set_fill_color(color);
433 ctx.fill_text(&icon.glyph.to_string(), x, baseline_y);
434 }
435}
436
437impl Widget for Button {
438 fn type_name(&self) -> &'static str {
439 "Button"
440 }
441 fn bounds(&self) -> Rect {
442 self.bounds
443 }
444 fn set_bounds(&mut self, bounds: Rect) {
445 self.bounds = bounds;
446 }
447
448 fn children(&self) -> &[Box<dyn Widget>] {
449 &self.children
450 }
451 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
452 &mut self.children
453 }
454
455 fn is_focusable(&self) -> bool {
456 self.is_enabled()
457 }
458
459 fn margin(&self) -> Insets {
460 self.base.margin
461 }
462 fn widget_base(&self) -> Option<&WidgetBase> {
463 Some(&self.base)
464 }
465 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
466 Some(&mut self.base)
467 }
468 fn h_anchor(&self) -> HAnchor {
469 self.base.h_anchor
470 }
471 fn v_anchor(&self) -> VAnchor {
472 self.base.v_anchor
473 }
474 fn min_size(&self) -> Size {
475 self.base.min_size
476 }
477 fn max_size(&self) -> Size {
478 self.base.max_size
479 }
480
481 fn layout(&mut self, available: Size) -> Size {
482 // Honour an explicit `min_size.height` floor (symmetric with the
483 // `min_size.width` handling below) so callers can force uniform
484 // square icon buttons. Defaults to `Size::ZERO`, so unset buttons
485 // keep the font-derived natural height.
486 let natural_height = (self.font_size * 1.7)
487 .max(24.0)
488 .max(self.base.min_size.height);
489 let height = if available.height > 0.0 {
490 natural_height.min(available.height)
491 } else {
492 natural_height
493 };
494 // Measure the label first so we can report a "fit" width — label
495 // width plus horizontal padding — instead of stretching to the
496 // whole available width. This keeps Buttons polite siblings in a
497 // `FlexRow`. Parents that want a full-width button can:
498 // - wrap it in a `SizedBox` with an explicit width, or
499 // - apply `HAnchor::STRETCH`, or
500 // - set `with_min_size(Size::new(width, _))` for a width floor.
501 // Compact mode tightens the horizontal pad and drops the
502 // 48 px touch-target floor — icon-only toolbar buttons that
503 // need to sit next to each other on a narrow bar would
504 // otherwise eat all the row width.
505 let pad_h = if self.compact {
506 self.font_size * 0.7
507 } else {
508 self.font_size * 1.2
509 };
510 let label_size = self.children[0].layout(Size::new(available.width, height));
511 let icon_block_w = self
512 .icon
513 .as_ref()
514 .map(|i| measure_advance(&i.font, &i.glyph.to_string(), i.font_size) + self.icon_gap())
515 .unwrap_or(0.0);
516 let min_w = if self.compact { 0.0 } else { 48.0 };
517 let natural_w = (label_size.width + icon_block_w + pad_h)
518 .max(min_w)
519 .max(self.base.min_size.width);
520 let width = if self.base.h_anchor.is_stretch() {
521 available.width.max(natural_w)
522 } else {
523 natural_w
524 }
525 .min(available.width);
526 let size = Size::new(width, height);
527 self.position_label(size, label_size);
528 size
529 }
530
531 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
532 let w = self.bounds.width;
533 let h = self.bounds.height;
534 let r = self.theme.border_radius;
535 let enabled = self.is_enabled();
536 let v = ctx.visuals();
537 let use_visuals = self.theme == ButtonTheme::default();
538 let active = self.is_active();
539 // A subtle button paints in muted theme colours when inactive, and
540 // flips to the accent surface (white text on accent fill) when its
541 // `active_fn` returns true. Plain (non-subtle) buttons always use
542 // the accent surface — that's the existing primary-button look.
543 let muted = self.subtle && !active;
544
545 // Focus ring — drawn JUST INSIDE the button bounds so the parent's
546 // `clip_children_rect` (defaults to widget bounds) doesn't chop
547 // the leftmost stroke pixel when the button sits flush against
548 // a container edge. Painting outside-bounds with negative
549 // coordinates was the long-standing cause of "the left edge of
550 // my button looks clipped" reports.
551 if enabled && self.focused {
552 let ring = self.theme.focus_ring_width;
553 let focus_ring = if use_visuals {
554 v.accent_focus
555 } else {
556 self.theme.focus_ring_color
557 };
558 ctx.set_stroke_color(focus_ring);
559 ctx.set_line_width(ring);
560 ctx.begin_path();
561 let inset = ring * 0.5;
562 ctx.rounded_rect(
563 inset,
564 inset,
565 (w - ring).max(0.0),
566 (h - ring).max(0.0),
567 (r - inset).max(0.0),
568 );
569 ctx.stroke();
570 }
571
572 // Background — color depends on interaction state. Disabled buttons
573 // use neutral widget colors instead of a washed-out accent, so they
574 // don't look like secondary active actions.
575 let base_bg = if muted && self.ghost && self.pressed {
576 // Ghost (transparent-inactive) buttons paint a faint
577 // text-coloured overlay on hover / press instead of the
578 // widget_bg shade. Matches the egui-style sidebar row
579 // look the demo's `ToggleButton` had before refactor.
580 Color::rgba(v.text_color.r, v.text_color.g, v.text_color.b, 0.16)
581 } else if muted && self.ghost && self.hovered {
582 Color::rgba(v.text_color.r, v.text_color.g, v.text_color.b, 0.10)
583 } else if muted && self.ghost {
584 // Fully transparent when the user isn't interacting.
585 Color::rgba(0.0, 0.0, 0.0, 0.0)
586 } else if muted && (self.pressed || self.hovered) {
587 v.widget_bg_hovered
588 } else if muted {
589 v.widget_bg
590 } else if use_visuals && self.pressed {
591 v.accent_pressed
592 } else if use_visuals && self.hovered {
593 v.accent_hovered
594 } else if use_visuals {
595 v.accent
596 } else if self.pressed {
597 self.theme.background_pressed
598 } else if self.hovered {
599 self.theme.background_hovered
600 } else {
601 self.theme.background
602 };
603 let (disabled_bg, disabled_stroke, _) = Self::disabled_colors(&v);
604 let bg = if enabled { base_bg } else { disabled_bg };
605 ctx.set_fill_color(bg);
606 ctx.begin_path();
607 ctx.rounded_rect(0.0, 0.0, w, h, r);
608 ctx.fill();
609
610 // Optional outline — opt-in via `with_outlined()` for inactive
611 // segmented buttons that want a defined edge against a same-colour
612 // parent (e.g. top-bar tabs). Active state already has a
613 // high-contrast accent fill and skips this so the selected segment
614 // visually pops.
615 if enabled && self.outlined && !active {
616 ctx.set_stroke_color(v.widget_stroke);
617 ctx.set_line_width(1.0);
618 ctx.begin_path();
619 ctx.rounded_rect(0.5, 0.5, (w - 1.0).max(0.0), (h - 1.0).max(0.0), r);
620 ctx.stroke();
621 }
622
623 // Retint the child label so subtle / active states show the right
624 // foreground colour without rebuilding the Label widget. Calling
625 // through the dyn Widget keeps Button agnostic of the concrete
626 // Label type — `set_label_color` is a default no-op that Label
627 // overrides, see `Widget::set_label_color`.
628 let label_color = if muted {
629 v.text_color
630 } else {
631 self.theme.label_color
632 };
633 if let Some(child) = self.children.get_mut(0) {
634 child.set_label_color(label_color);
635 }
636
637 if !enabled {
638 ctx.set_stroke_color(disabled_stroke);
639 ctx.set_line_width(1.0);
640 ctx.begin_path();
641 ctx.rounded_rect(0.5, 0.5, (w - 1.0).max(0.0), (h - 1.0).max(0.0), r);
642 ctx.stroke();
643 }
644
645 // Text is NOT drawn here. `paint_subtree` recurses into the Label
646 // child automatically after this method returns.
647 }
648
649 fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
650 let enabled = self.is_enabled();
651 let w = self.bounds.width;
652 let h = self.bounds.height;
653 let r = self.theme.border_radius;
654 let v = ctx.visuals();
655
656 if !enabled {
657 // The normal child Label was built for the enabled foreground
658 // colour. Cover it and repaint the label with the disabled
659 // text colour. Icon (if any) renders in the same disabled
660 // text colour at the same group_x as layout positioned it.
661 let (disabled_bg, disabled_stroke, disabled_text) = Self::disabled_colors(&v);
662
663 ctx.set_fill_color(disabled_bg);
664 ctx.begin_path();
665 ctx.rounded_rect(0.0, 0.0, w, h, r);
666 ctx.fill();
667
668 ctx.set_stroke_color(disabled_stroke);
669 ctx.set_line_width(1.0);
670 ctx.begin_path();
671 ctx.rounded_rect(0.5, 0.5, (w - 1.0).max(0.0), (h - 1.0).max(0.0), r);
672 ctx.stroke();
673
674 let font = crate::font_settings::current_system_font()
675 .unwrap_or_else(|| Arc::clone(&self.font));
676 let icon_block_w = self
677 .icon
678 .as_ref()
679 .map(|i| measure_advance(&i.font, &i.glyph.to_string(), i.font_size) + self.icon_gap())
680 .unwrap_or(0.0);
681 ctx.set_font(font);
682 ctx.set_font_size(self.font_size * crate::font_settings::current_font_size_scale());
683 ctx.set_fill_color(disabled_text);
684 if let Some(m) = ctx.measure_text(&self.label_text) {
685 let group_w = m.width + icon_block_w;
686 let group_x = ((w - group_w) * 0.5).max(0.0);
687 let tx = group_x + icon_block_w;
688 let ty = m.centered_baseline_y(h).max(0.0);
689 ctx.fill_text(&self.label_text, tx, ty);
690 Self::paint_icon(
691 ctx,
692 &self.icon,
693 &self.font,
694 self.font_size,
695 group_x,
696 h,
697 disabled_text,
698 );
699 }
700 return;
701 }
702
703 // Enabled state — only paint the icon (label has already been
704 // drawn by the framework via the child Label's paint).
705 if let Some(icon) = self.icon.clone() {
706 let active = self.is_active();
707 let muted = self.subtle && !active;
708 let label_color = if muted {
709 v.text_color
710 } else {
711 self.theme.label_color
712 };
713 let label_x = self
714 .children
715 .first()
716 .map(|c| c.bounds().x)
717 .unwrap_or_default();
718 let icon_block_w =
719 measure_advance(&icon.font, &icon.glyph.to_string(), icon.font_size) + self.icon_gap();
720 let group_x = (label_x - icon_block_w).max(0.0);
721 Self::paint_icon(
722 ctx,
723 &Some(icon),
724 &self.font,
725 self.font_size,
726 group_x,
727 h,
728 label_color,
729 );
730 }
731 }
732
733 fn on_event(&mut self, event: &Event) -> EventResult {
734 self.handle_event(event)
735 }
736
737 fn properties(&self) -> Vec<(&'static str, String)> {
738 vec![
739 ("label", self.label_text.clone()),
740 ("font_size", format!("{:.1}", self.font_size)),
741 ]
742 }
743}