1use alloc::boxed::Box;
9use rlvgl_core::event::Event;
10use rlvgl_core::image::{BlitOpts, ImageDescriptor};
11use rlvgl_core::renderer::Renderer;
12use rlvgl_core::widget::{Rect, Widget};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ImageButtonState {
17 Released,
19 Pressed,
21 Disabled,
23 CheckedReleased,
25 CheckedPressed,
27 CheckedDisabled,
29}
30
31#[derive(Default)]
38pub struct ImageButtonSources {
39 pub left: Option<Box<ImageDescriptor<'static>>>,
41 pub middle: Option<Box<ImageDescriptor<'static>>>,
43 pub right: Option<Box<ImageDescriptor<'static>>>,
45}
46
47fn state_index(s: ImageButtonState) -> usize {
48 match s {
49 ImageButtonState::Released => 0,
50 ImageButtonState::Pressed => 1,
51 ImageButtonState::Disabled => 2,
52 ImageButtonState::CheckedReleased => 3,
53 ImageButtonState::CheckedPressed => 4,
54 ImageButtonState::CheckedDisabled => 5,
55 }
56}
57
58pub struct ImageButton {
77 bounds: Rect,
78 state: ImageButtonState,
80 checked: bool,
82 is_pressed: bool,
84 sources: [ImageButtonSources; 6],
86}
87
88impl ImageButton {
89 pub fn new(bounds: Rect) -> Self {
91 Self {
92 bounds,
93 state: ImageButtonState::Released,
94 checked: false,
95 is_pressed: false,
96 sources: [
97 ImageButtonSources::default(),
98 ImageButtonSources::default(),
99 ImageButtonSources::default(),
100 ImageButtonSources::default(),
101 ImageButtonSources::default(),
102 ImageButtonSources::default(),
103 ],
104 }
105 }
106
107 pub fn set_src(
113 &mut self,
114 state: ImageButtonState,
115 left: Option<ImageDescriptor<'static>>,
116 middle: Option<ImageDescriptor<'static>>,
117 right: Option<ImageDescriptor<'static>>,
118 ) {
119 let slot = &mut self.sources[state_index(state)];
120 slot.left = left.map(Box::new);
121 slot.middle = middle.map(Box::new);
122 slot.right = right.map(Box::new);
123 }
124
125 pub fn src_left(&self, state: ImageButtonState) -> Option<&ImageDescriptor<'static>> {
127 self.sources[state_index(state)].left.as_deref()
128 }
129
130 pub fn src_middle(&self, state: ImageButtonState) -> Option<&ImageDescriptor<'static>> {
132 self.sources[state_index(state)].middle.as_deref()
133 }
134
135 pub fn src_right(&self, state: ImageButtonState) -> Option<&ImageDescriptor<'static>> {
137 self.sources[state_index(state)].right.as_deref()
138 }
139
140 pub fn set_state(&mut self, state: ImageButtonState) {
145 self.state = state;
146 }
147
148 pub fn state(&self) -> ImageButtonState {
150 self.state
151 }
152
153 pub fn set_checked(&mut self, checked: bool) {
155 self.checked = checked;
156 }
157
158 pub fn checked(&self) -> bool {
160 self.checked
161 }
162
163 pub fn draw_state(&self) -> ImageButtonState {
166 if matches!(
167 self.state,
168 ImageButtonState::Disabled | ImageButtonState::CheckedDisabled
169 ) {
170 if self.checked {
171 return ImageButtonState::CheckedDisabled;
172 }
173 return ImageButtonState::Disabled;
174 }
175 match (self.checked, self.is_pressed) {
176 (true, true) => ImageButtonState::CheckedPressed,
177 (true, false) => ImageButtonState::CheckedReleased,
178 (false, true) => ImageButtonState::Pressed,
179 (false, false) => ImageButtonState::Released,
180 }
181 }
182
183 fn resolved_state(&self) -> ImageButtonState {
187 let chain: &[ImageButtonState] = match self.draw_state() {
188 ImageButtonState::Released => &[ImageButtonState::Released],
189 ImageButtonState::Pressed => &[ImageButtonState::Pressed, ImageButtonState::Released],
190 ImageButtonState::Disabled => &[ImageButtonState::Disabled, ImageButtonState::Released],
191 ImageButtonState::CheckedReleased => &[
192 ImageButtonState::CheckedReleased,
193 ImageButtonState::Released,
194 ],
195 ImageButtonState::CheckedPressed => &[
196 ImageButtonState::CheckedPressed,
197 ImageButtonState::CheckedReleased,
198 ImageButtonState::Pressed,
199 ImageButtonState::Released,
200 ],
201 ImageButtonState::CheckedDisabled => &[
202 ImageButtonState::CheckedDisabled,
203 ImageButtonState::CheckedReleased,
204 ImageButtonState::Released,
205 ],
206 };
207 for &s in chain {
208 if self.sources[state_index(s)].middle.is_some() {
209 return s;
210 }
211 }
212 *chain.last().unwrap_or(&ImageButtonState::Released)
214 }
215
216 fn inside_bounds(&self, x: i32, y: i32) -> bool {
217 let b = self.bounds;
218 x >= b.x && x < b.x + b.width && y >= b.y && y < b.y + b.height
219 }
220
221 fn is_disabled(&self) -> bool {
222 matches!(
223 self.state,
224 ImageButtonState::Disabled | ImageButtonState::CheckedDisabled
225 )
226 }
227}
228
229impl Widget for ImageButton {
230 fn bounds(&self) -> Rect {
231 self.bounds
232 }
233
234 fn set_bounds(&mut self, bounds: Rect) {
235 self.bounds = bounds;
236 }
237
238 fn draw(&self, renderer: &mut dyn Renderer) {
239 let s = self.resolved_state();
240 let slot = &self.sources[state_index(s)];
241 let opts = BlitOpts::default();
242
243 match (&slot.left, &slot.middle, &slot.right) {
244 (Some(left), Some(middle), Some(right)) => {
245 let lw = left.width as i32;
246 let rw = right.width as i32;
247 let mid_w = (self.bounds.width - lw - rw).max(0);
248
249 let left_rect = Rect {
250 x: self.bounds.x,
251 y: self.bounds.y,
252 width: lw,
253 height: self.bounds.height,
254 };
255 renderer.blit_image(left_rect, left, &opts);
256
257 if mid_w > 0 {
258 let mid_rect = Rect {
259 x: self.bounds.x + lw,
260 y: self.bounds.y,
261 width: mid_w,
262 height: self.bounds.height,
263 };
264 renderer.blit_image(mid_rect, middle, &opts);
265 }
266
267 let right_rect = Rect {
268 x: self.bounds.x + self.bounds.width - rw,
269 y: self.bounds.y,
270 width: rw,
271 height: self.bounds.height,
272 };
273 renderer.blit_image(right_rect, right, &opts);
274 }
275 (_, Some(middle), _) => {
276 renderer.blit_image(self.bounds, middle, &opts);
278 }
279 _ => {
280 }
282 }
283 }
284
285 fn handle_event(&mut self, event: &Event) -> bool {
286 match event {
287 Event::PressDown { x, y } if !self.is_disabled() && self.inside_bounds(*x, *y) => {
288 self.is_pressed = true;
289 true
290 }
291 Event::PressRelease { x, y }
292 if !self.is_disabled() && self.is_pressed && self.inside_bounds(*x, *y) =>
293 {
294 self.is_pressed = false;
295 true }
297 Event::PointerUp { .. } => {
298 self.is_pressed = false;
299 false
300 }
301 _ => false,
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use rlvgl_core::image::PixelFormat;
310 use rlvgl_core::widget::Color;
311
312 fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect {
313 Rect {
314 x,
315 y,
316 width: w,
317 height: h,
318 }
319 }
320
321 fn empty_descriptor() -> ImageDescriptor<'static> {
322 ImageDescriptor::owned(PixelFormat::Argb8888, 4, 4, alloc::vec![0u8; 64])
323 }
324
325 struct NullRenderer;
326 impl rlvgl_core::renderer::Renderer for NullRenderer {
327 fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
328 fn draw_text(&mut self, _pos: (i32, i32), _text: &str, _color: Color) {}
329 }
330
331 #[test]
332 fn new_starts_released_unchecked() {
333 let btn = ImageButton::new(rect(0, 0, 100, 30));
334 assert_eq!(btn.state(), ImageButtonState::Released);
335 assert!(!btn.checked());
336 }
337
338 #[test]
339 fn set_src_and_retrieve_without_fallback() {
340 let mut btn = ImageButton::new(rect(0, 0, 100, 30));
341 btn.set_src(
342 ImageButtonState::Released,
343 None,
344 Some(empty_descriptor()),
345 None,
346 );
347 assert!(btn.src_middle(ImageButtonState::Released).is_some());
348 assert!(btn.src_left(ImageButtonState::Released).is_none());
349 }
350
351 #[test]
352 fn pressed_event_sets_is_pressed() {
353 let mut btn = ImageButton::new(rect(0, 0, 100, 30));
354 let consumed = btn.handle_event(&Event::PressDown { x: 50, y: 15 });
355 assert!(consumed);
356 assert_eq!(btn.draw_state(), ImageButtonState::Pressed);
357 }
358
359 #[test]
360 fn press_release_inside_activates() {
361 let mut btn = ImageButton::new(rect(0, 0, 100, 30));
362 btn.handle_event(&Event::PressDown { x: 50, y: 15 });
363 let activated = btn.handle_event(&Event::PressRelease { x: 50, y: 15 });
364 assert!(activated);
365 assert_eq!(btn.draw_state(), ImageButtonState::Released);
366 }
367
368 #[test]
369 fn press_outside_bounds_not_consumed() {
370 let mut btn = ImageButton::new(rect(0, 0, 100, 30));
371 let consumed = btn.handle_event(&Event::PressDown { x: 200, y: 15 });
372 assert!(!consumed);
373 }
374
375 #[test]
376 fn disabled_state_ignores_pointer() {
377 let mut btn = ImageButton::new(rect(0, 0, 100, 30));
378 btn.set_state(ImageButtonState::Disabled);
379 let consumed = btn.handle_event(&Event::PressDown { x: 50, y: 15 });
380 assert!(!consumed);
381 }
382
383 #[test]
384 fn checked_state_produces_checked_released() {
385 let mut btn = ImageButton::new(rect(0, 0, 100, 30));
386 btn.set_checked(true);
387 assert_eq!(btn.draw_state(), ImageButtonState::CheckedReleased);
388 }
389
390 #[test]
391 fn checked_and_pressed_produces_checked_pressed() {
392 let mut btn = ImageButton::new(rect(0, 0, 100, 30));
393 btn.set_checked(true);
394 btn.handle_event(&Event::PressDown { x: 50, y: 15 });
395 assert_eq!(btn.draw_state(), ImageButtonState::CheckedPressed);
396 }
397
398 #[test]
399 fn fallback_checked_released_to_released() {
400 let mut btn = ImageButton::new(rect(0, 0, 100, 30));
401 btn.set_src(
402 ImageButtonState::Released,
403 None,
404 Some(empty_descriptor()),
405 None,
406 );
407 btn.set_checked(true);
409 assert_eq!(btn.resolved_state(), ImageButtonState::Released);
410 }
411
412 #[test]
413 fn draw_does_not_panic_with_no_sources() {
414 let btn = ImageButton::new(rect(0, 0, 100, 30));
415 let mut r = NullRenderer;
416 btn.draw(&mut r);
417 }
418
419 #[test]
420 fn set_bounds_adopted() {
421 let mut btn = ImageButton::new(rect(0, 0, 100, 30));
422 btn.set_bounds(rect(10, 20, 200, 50));
423 assert_eq!(btn.bounds(), rect(10, 20, 200, 50));
424 }
425
426 #[test]
427 fn pointer_up_clears_pressed_returns_false() {
428 let mut btn = ImageButton::new(rect(0, 0, 100, 30));
429 btn.handle_event(&Event::PressDown { x: 50, y: 15 });
430 let r = btn.handle_event(&Event::PointerUp { x: 50, y: 15 });
431 assert!(!r);
432 assert_eq!(btn.draw_state(), ImageButtonState::Released);
433 }
434}