1use super::Widget;
16use alloc::string::String;
17use core::marker::PhantomData;
18use embedded_graphics::{
19 mono_font::MonoFont, pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
20};
21use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase, UiAction, WidgetId};
22use zest_theme::{ButtonCatalog, ButtonClass, Status, Theme};
23
24pub struct Button<'a, C: PixelColor, M: Clone> {
26 rect: Rectangle,
27 label: String,
28 on_press: Option<M>,
29 id: Option<WidgetId>,
30 focused: bool,
31 pressed: bool,
32 class: ButtonClass,
33 font_override: Option<&'a MonoFont<'a>>,
34 intrinsic_size: Size,
35 width: Length,
36 height: Length,
37 _color: PhantomData<C>,
40}
41
42impl<'a, C: PixelColor, M: Clone> Button<'a, C, M> {
43 pub fn new(label: impl Into<String>) -> Self {
47 Self {
48 rect: Rectangle::zero(),
49 label: label.into(),
50 on_press: None,
51 id: None,
52 focused: false,
53 pressed: false,
54 class: ButtonClass::Standard,
55 font_override: None,
56 intrinsic_size: Size::new(48, 32),
57 width: Length::Fill,
58 height: Length::Fill,
59 _color: PhantomData,
60 }
61 }
62
63 #[must_use]
65 pub fn width(mut self, width: impl Into<Length>) -> Self {
66 self.width = width.into();
67 self
68 }
69
70 #[must_use]
72 pub fn height(mut self, height: impl Into<Length>) -> Self {
73 self.height = height.into();
74 self
75 }
76
77 #[must_use]
79 pub fn on_press(mut self, msg: M) -> Self {
80 self.on_press = Some(msg);
81 self
82 }
83
84 #[must_use]
87 pub fn on_press_maybe(mut self, msg: Option<M>) -> Self {
88 self.on_press = msg;
89 self
90 }
91
92 #[must_use]
94 pub fn id(mut self, id: WidgetId) -> Self {
95 self.id = Some(id);
96 self
97 }
98
99 #[must_use]
101 pub fn label(mut self, label: impl Into<String>) -> Self {
102 self.label = label.into();
103 self
104 }
105
106 #[must_use]
110 pub fn class(mut self, class: ButtonClass) -> Self {
111 self.class = class;
112 self
113 }
114
115 #[must_use]
117 pub fn font(mut self, font: &'a MonoFont<'a>) -> Self {
118 self.font_override = Some(font);
119 self
120 }
121
122 #[must_use]
125 pub fn intrinsic_size(mut self, size: Size) -> Self {
126 self.intrinsic_size = size;
127 self
128 }
129
130 pub fn is_enabled(&self) -> bool {
133 self.on_press.is_some()
134 }
135
136 fn status(&self) -> Status {
137 if !self.is_enabled() {
138 Status::Disabled
139 } else if self.pressed {
140 Status::Pressed
141 } else if self.focused {
142 Status::Focused
143 } else {
144 Status::Active
145 }
146 }
147
148 fn resolved_font<'t>(&'t self, theme: &'t Theme<'a, C>) -> &'t MonoFont<'a> {
149 self.font_override.unwrap_or(theme.default_font())
150 }
151
152 fn hit_test(&self, point: Point) -> bool {
153 let top_left = self.rect.top_left;
154 let bot_right =
155 top_left + Point::new(self.rect.size.width as i32, self.rect.size.height as i32);
156 point.x >= top_left.x
157 && point.x < bot_right.x
158 && point.y >= top_left.y
159 && point.y < bot_right.y
160 }
161}
162
163impl<'a, C: PixelColor, M: Clone> Widget<C, M> for Button<'a, C, M> {
164 fn measure(&mut self, constraints: Constraints) -> Size {
165 let w = self
166 .width
167 .resolve(self.intrinsic_size.width, constraints.max.width);
168 let h = self
169 .height
170 .resolve(self.intrinsic_size.height, constraints.max.height);
171 constraints.clamp(Size::new(w, h))
172 }
173
174 fn preferred_size(&self) -> (Length, Length) {
175 (self.width, self.height)
176 }
177
178 fn arrange(&mut self, rect: Rectangle) {
179 self.rect = rect;
180 }
181
182 fn rect(&self) -> Rectangle {
183 self.rect
184 }
185
186 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
187 if !self.is_enabled() {
188 return None;
189 }
190 if !self.hit_test(point) {
191 return None;
192 }
193 match phase {
194 TouchPhase::Down => {
195 self.pressed = true;
196 None
197 }
198 TouchPhase::Up => {
199 if self.pressed {
200 self.on_press.clone()
201 } else {
202 None
203 }
204 }
205 TouchPhase::Moved => None,
206 }
207 }
208
209 fn mark_pressed(&mut self, point: Point) {
210 if self.is_enabled() && self.hit_test(point) {
211 self.pressed = true;
212 }
213 }
214
215 fn widget_id(&self) -> Option<WidgetId> {
216 self.id
217 }
218
219 fn is_focusable(&self) -> bool {
220 self.id.is_some() && self.is_enabled()
221 }
222
223 fn handle_action(&mut self, action: UiAction) -> Option<M> {
224 if !self.is_enabled() {
225 return None;
226 }
227
228 match action {
229 UiAction::Activate => self.on_press.clone(),
230 _ => None,
231 }
232 }
233
234 fn sync_focus(&mut self, focused: Option<WidgetId>) {
235 self.focused = self.id.is_some() && self.id == focused;
236 }
237
238 fn focus_at(&self, point: Point) -> Option<WidgetId> {
239 if self.is_focusable() && self.hit_test(point) {
240 self.id
241 } else {
242 None
243 }
244 }
245
246 fn draw<'t>(
247 &self,
248 renderer: &mut dyn Renderer<C>,
249 theme: &Theme<'t, C>,
250 ) -> Result<(), RenderError> {
251 let appearance = theme.button(self.class, self.status());
252 let font = self.resolved_font(theme);
253
254 if let Some(bg) = appearance.background {
255 renderer.fill_rect(self.rect, bg)?;
256 }
257 if let Some(border) = appearance.border {
258 renderer.stroke_rect(self.rect, border)?;
259 }
260
261 let center_x = self.rect.top_left.x + self.rect.size.width as i32 / 2;
262 let center_y = self.rect.top_left.y
263 + self.rect.size.height as i32 / 2
264 + font.character_size.height as i32 / 3;
265
266 renderer.draw_text(
267 &self.label,
268 Point::new(center_x, center_y),
269 font,
270 appearance.text,
271 Alignment::Center,
272 )?;
273
274 Ok(())
275 }
276}