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
11const PADDING_X: f32 = 12.0;
13const PADDING_Y: f32 = 5.0;
14
15#[derive(Debug)]
18pub struct Button {
19 text: Text,
20 primary: bool,
21}
22
23impl Button {
24 pub fn new(label: impl Into<String>) -> Self {
26 Self {
27 text: Text::new(label),
28 primary: false,
29 }
30 }
31
32 #[must_use]
34 pub fn primary(mut self) -> Self {
35 self.primary = true;
36 self
37 }
38
39 pub fn label(&self) -> &str {
41 self.text.as_str()
42 }
43
44 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 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}