Skip to main content

densa_widgets/
button.rs

1use densa_core::{Color, Size};
2use densa_draw::Canvas;
3use densa_ui::{
4    AccessAction, AccessNode, AccessRequest, ActionKind, Constraints, Cursor, Event, EventCx,
5    Handled, Key, LayoutCx, NamedKey, PaintCx, PointerButton, PointerKind, Role, Theme, Widget,
6};
7
8use crate::paint::{centered, outline};
9use crate::text::Text;
10
11/// Space around the label, in logical pixels.
12const PADDING_X: f32 = 12.0;
13const PADDING_Y: f32 = 5.0;
14
15/// A push button: reports [`ActionKind::Clicked`] when released over itself, or when Enter
16/// or Space is pressed while it has focus.
17#[derive(Debug)]
18pub struct Button {
19    text: Text,
20    primary: bool,
21}
22
23impl Button {
24    /// A button labeled `label`.
25    pub fn new(label: impl Into<String>) -> Self {
26        Self {
27            text: Text::new(label),
28            primary: false,
29        }
30    }
31
32    /// The button in the accent color: the action a form or toolbar is for.
33    #[must_use]
34    pub fn primary(mut self) -> Self {
35        self.primary = true;
36        self
37    }
38
39    /// The label.
40    pub fn label(&self) -> &str {
41        self.text.as_str()
42    }
43
44    /// Shows `label` instead, measured again at the next layout.
45    pub fn set_label(&mut self, cx: &mut EventCx<'_>, label: impl Into<String>) {
46        self.text.set(label);
47        cx.request_layout();
48    }
49
50    fn text_color(&self, theme: &Theme) -> Color {
51        if self.primary {
52            theme.on_accent
53        } else {
54            theme.text
55        }
56    }
57
58    fn fill(&self, cx: &PaintCx<'_>) -> Color {
59        let theme = cx.theme();
60        match (self.primary, cx.is_pressed(), cx.is_hovered()) {
61            (true, true, _) => theme.accent.with_alpha(0.75),
62            (true, false, true) => theme.accent.with_alpha(0.88),
63            (true, false, false) => theme.accent,
64            (false, true, _) => theme.pressed,
65            (false, false, true) => theme.hover,
66            (false, false, false) => theme.raised,
67        }
68    }
69}
70
71impl Widget for Button {
72    fn measure(&mut self, cx: &mut LayoutCx<'_>, constraints: Constraints) -> Size {
73        let color = self.text_color(cx.theme());
74        let text = self.text.measure(cx, constraints, color);
75        Size::new(
76            text.width + 2.0 * cx.px(PADDING_X),
77            text.height + 2.0 * cx.px(PADDING_Y),
78        )
79    }
80
81    fn layout(&mut self, cx: &mut LayoutCx<'_>, size: Size) {
82        let color = self.text_color(cx.theme());
83        self.text.fit(cx, size.width, color);
84    }
85
86    fn paint(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
87        let rect = cx.rect();
88        canvas.fill_rect(rect, self.fill(cx));
89        if !self.primary {
90            outline(canvas, rect, cx.px(1.0).round(), cx.theme().border);
91        }
92        if cx.is_focused() {
93            outline(canvas, rect, cx.px(1.5).round(), cx.theme().focus);
94        }
95        let size = self.text.size();
96        self.text
97            .paint(canvas, centered(cx.content_rect(), size.width, size.height));
98    }
99
100    fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
101        match event {
102            Event::Pointer(pointer) => match pointer.kind {
103                PointerKind::Enter => {
104                    cx.set_cursor(Some(Cursor::Pointer));
105                    cx.request_paint();
106                    Handled::Yes
107                }
108                PointerKind::Leave | PointerKind::Down(PointerButton::Primary) => {
109                    cx.request_paint();
110                    Handled::Yes
111                }
112                PointerKind::Up(PointerButton::Primary) => {
113                    // Only a release ending this button's own press clicks it.
114                    if cx.is_pressed() && cx.rect().contains(pointer.position) {
115                        cx.emit(ActionKind::Clicked);
116                    }
117                    cx.request_paint();
118                    Handled::Yes
119                }
120                _ => Handled::No,
121            },
122            Event::Key(key)
123                if key.pressed
124                    && !key.repeat
125                    && matches!(
126                        key.key,
127                        Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space)
128                    ) =>
129            {
130                cx.emit(ActionKind::Clicked);
131                Handled::Yes
132            }
133            Event::FocusGained | Event::FocusLost => {
134                cx.request_paint();
135                Handled::Yes
136            }
137            _ => Handled::No,
138        }
139    }
140
141    fn accepts_focus(&self) -> bool {
142        true
143    }
144
145    fn accessibility(&self, _: &PaintCx<'_>, node: &mut AccessNode) {
146        node.set_role(Role::Button);
147        node.set_label(self.text.as_str());
148        node.add_action(AccessAction::Click);
149    }
150
151    fn access_action(&mut self, cx: &mut EventCx<'_>, request: &AccessRequest<'_>) -> Handled {
152        if request.action != AccessAction::Click {
153            return Handled::No;
154        }
155        cx.emit(ActionKind::Clicked);
156        Handled::Yes
157    }
158}