Skip to main content

aetna_core/widgets/
button.rs

1//! Button component.
2//!
3//! Default `button("Save")` is the secondary style. Apply variants from
4//! [`crate::style`] to opt into others:
5//!
6//! - `.primary()` — filled accent color, semibold text.
7//! - `.secondary()` — secondary surface (the default look).
8//! - `.ghost()` — no fill, no border, muted text.
9//! - `.outline()` — outline-only.
10//! - `.destructive()` — solid red, contrasting text.
11//!
12//! Buttons hug their text width and default to [`tokens::CONTROL_HEIGHT`]
13//! — the same height used by `select`, `text_input`, and tab triggers,
14//! so they line up in form rows. Override `.width(Size::Fill(1.0))` to
15//! stretch; the label stays horizontally centered.
16//!
17//! # Dogfood note
18//!
19//! This builder uses only the public widget-author surface — `Kind::Custom`
20//! for the inspector tag, `.focusable()` to opt into the focus ring,
21//! `.paint_overflow()` to give the ring somewhere to render, and
22//! `.text_align(TextAlign::Center)` to center the label. An app crate
23//! can write an equivalent button against the same API; nothing here
24//! reaches into library internals. See `widget_kit.md`.
25
26use std::panic::Location;
27
28use crate::anim::Timing;
29use crate::cursor::Cursor;
30use crate::metrics::MetricsRole;
31use crate::style::StyleProfile;
32use crate::tokens;
33use crate::tree::*;
34use crate::{IntoIconSource, icon, text};
35
36#[track_caller]
37pub fn button(label: impl Into<String>) -> El {
38    El::new(Kind::Custom("button"))
39        .at_loc(Location::caller())
40        .style_profile(StyleProfile::Solid)
41        .metrics_role(MetricsRole::Button)
42        .surface_role(SurfaceRole::Raised)
43        .focusable()
44        .paint_overflow(Sides::all(tokens::RING_WIDTH))
45        .cursor(Cursor::Pointer)
46        .text(label)
47        .text_align(TextAlign::Center)
48        .text_role(TextRole::Label)
49        .fill(tokens::SECONDARY)
50        .stroke(tokens::BORDER)
51        .text_color(tokens::SECONDARY_FOREGROUND)
52        .default_radius(tokens::RADIUS_MD)
53        .default_width(Size::Hug)
54        .default_height(Size::Fixed(tokens::CONTROL_HEIGHT))
55        .default_padding(Sides::xy(tokens::SPACE_3, 0.0))
56        .animate(Timing::SPRING_QUICK)
57}
58
59#[track_caller]
60pub fn icon_button(source: impl IntoIconSource) -> El {
61    El::new(Kind::Custom("icon_button"))
62        .at_loc(Location::caller())
63        .style_profile(StyleProfile::Solid)
64        .metrics_role(MetricsRole::IconButton)
65        .surface_role(SurfaceRole::Raised)
66        .focusable()
67        .paint_overflow(Sides::all(tokens::RING_WIDTH))
68        .cursor(Cursor::Pointer)
69        .icon_source(source)
70        .icon_size(tokens::ICON_SM)
71        .icon_stroke_width(2.0)
72        .fill(tokens::SECONDARY)
73        .stroke(tokens::BORDER)
74        .text_color(tokens::SECONDARY_FOREGROUND)
75        .default_radius(tokens::RADIUS_MD)
76        .default_width(Size::Fixed(tokens::CONTROL_HEIGHT))
77        .default_height(Size::Fixed(tokens::CONTROL_HEIGHT))
78        .animate(Timing::SPRING_QUICK)
79}
80
81#[track_caller]
82pub fn button_with_icon(source: impl IntoIconSource, label: impl Into<String>) -> El {
83    El::new(Kind::Custom("button_with_icon"))
84        .at_loc(Location::caller())
85        .style_profile(StyleProfile::Solid)
86        .metrics_role(MetricsRole::Button)
87        .surface_role(SurfaceRole::Raised)
88        .focusable()
89        .paint_overflow(Sides::all(tokens::RING_WIDTH))
90        .cursor(Cursor::Pointer)
91        .axis(Axis::Row)
92        .default_gap(tokens::SPACE_2)
93        .align(Align::Center)
94        .justify(Justify::Center)
95        .child(
96            icon(source)
97                .icon_size(tokens::ICON_SM)
98                .color(tokens::SECONDARY_FOREGROUND),
99        )
100        .child(text(label).label())
101        .fill(tokens::SECONDARY)
102        .stroke(tokens::BORDER)
103        .text_color(tokens::SECONDARY_FOREGROUND)
104        .default_radius(tokens::RADIUS_MD)
105        .default_width(Size::Hug)
106        .default_height(Size::Fixed(tokens::CONTROL_HEIGHT))
107        .default_padding(Sides::xy(tokens::SPACE_3, 0.0))
108        .animate(Timing::SPRING_QUICK)
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn buttons_ease_variant_changes() {
117        assert!(button("Save").animate.is_some());
118        assert!(button("Save").primary().animate.is_some());
119        assert!(icon_button("settings").animate.is_some());
120        assert!(button_with_icon("folder", "Open").animate.is_some());
121    }
122}