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}