Skip to main content

smudge_widgets/
button.rs

1//! The button: a focusable panel with a label that presses and that Enter and
2//! Space activate.
3
4use smudge_core::element::{Element, Value};
5use smudge_core::geometry::Size;
6use smudge_core::semantics::Role;
7use smudge_core::theme::{ColorToken, RadiusToken};
8use smudge_core::widget::{Panel, panel};
9use smudge_text::label;
10
11/// The padding around the label.
12const PADDING: Size = Size {
13    width: 12.0,
14    height: 6.0,
15};
16
17/// The width of the focus ring.
18///
19/// The button always lays out with a border this wide and paints it only
20/// while focused, so taking the focus repaints the button without moving it
21/// or its neighbors.
22const FOCUS_BORDER: f32 = 2.0;
23
24/// Creates a push button that shows `label_text`.
25///
26/// The button is focusable and can be pressed with
27/// [`on_press`](Element::on_press), which is chained on the element it
28/// returns. While it holds the keyboard focus, a press of Enter or Space
29/// fires the same handler; the activation is what
30/// [`activate_on_keys`](Element::activate_on_keys) provides, and a key repeat
31/// fires nothing. The focus ring draws only while the focus last moved by
32/// keyboard navigation, so a press on the button draws none.
33///
34/// The colors come from the theme: [`ColorToken::SURFACE`] at rest,
35/// [`ColorToken::SURFACE_HOVER`] under the pointer,
36/// [`ColorToken::SURFACE_PRESSED`] while a press is held, and a
37/// [`ColorToken::FOCUS_RING`] border of two logical pixels while the focus
38/// came from the keyboard. The border's space is reserved in every state, so
39/// the ring appears without a layout change.
40///
41/// ```
42/// use smudge_core::reactive::{prelude::*, signal_local};
43/// use smudge_widgets::button;
44///
45/// let (clicks, set_clicks) = signal_local(0u32);
46/// let ok = button("OK").on_press(move || {
47///     set_clicks.set(clicks.get() + 1);
48/// });
49/// ```
50pub fn button(label_text: impl Into<Value<String>>) -> Element<Panel> {
51    let label_text = label_text.into();
52    panel()
53        .padding_xy(PADDING)
54        .corner_radius(RadiusToken::MEDIUM)
55        .background(ColorToken::SURFACE)
56        .hover(|p| p.background(ColorToken::SURFACE_HOVER))
57        .pressed(|p| p.background(ColorToken::SURFACE_PRESSED))
58        .border(FOCUS_BORDER)
59        .focused(|p| p.border_color(ColorToken::FOCUS_RING))
60        .focusable()
61        .activate_on_keys()
62        .role(Role::Button)
63        .accessible_label(label_text.clone())
64        .child(label(label_text))
65}
66
67/// The appearance variants of a [`button`].
68///
69/// Import the trait to use them:
70///
71/// ```
72/// use smudge_widgets::ButtonExt;
73/// use smudge_widgets::button;
74///
75/// let save = button("Save").primary();
76/// ```
77pub trait ButtonExt: Sized {
78    /// Styles the button as the primary action of its context.
79    ///
80    /// The background becomes [`ColorToken::ACCENT`], with
81    /// [`ColorToken::ACCENT_HOVER`] under the pointer and
82    /// [`ColorToken::ACCENT_PRESSED`] while a press is held, and the label
83    /// paints in [`ColorToken::ACCENT_TEXT`]. The focus ring paints in
84    /// [`ColorToken::ACCENT_TEXT`] too, because a ring in the focus color
85    /// could match the accent it is drawn over. The hovered, pressed and
86    /// focused patches replace the ones the button had, so call it before
87    /// setting patches of your own.
88    fn primary(self) -> Self;
89}
90
91impl ButtonExt for Element<Panel> {
92    fn primary(self) -> Self {
93        self.background(ColorToken::ACCENT)
94            .hover(|p| p.background(ColorToken::ACCENT_HOVER))
95            .pressed(|p| p.background(ColorToken::ACCENT_PRESSED))
96            .focused(|p| p.border_color(ColorToken::ACCENT_TEXT))
97            .style_children(|style| {
98                style.paint.text_color = Some(ColorToken::ACCENT_TEXT.into());
99            })
100    }
101}