1#![cfg_attr(not(test), no_std)]
2
3extern crate alloc;
4extern crate core;
5
6use crate::geom::Size;
7use crate::scene::Scene;
8use crate::view::ViewId;
9use alloc::string::String;
10use embedded_graphics::mono_font::MonoFont;
11use embedded_graphics::pixelcolor::Rgb565;
12use geom::{Bounds, Point};
13use gfx::DrawingContext;
14use view::View;
15
16pub mod button;
17pub mod device;
18pub mod geom;
19pub mod gfx;
20pub mod grid;
21pub mod label;
22pub mod layouts;
23pub mod list_view;
24pub mod panel;
25pub mod scene;
26pub mod tabbed_panel;
27pub mod test;
28pub mod text_input;
29pub mod toggle_button;
30pub mod toggle_group;
31pub mod util;
32pub mod view;
33
34pub struct DrawEvent<'a> {
35 pub ctx: &'a mut dyn DrawingContext,
36 pub theme: &'a Theme,
37 pub focused: &'a Option<ViewId>,
38 pub view: &'a mut View,
39 pub bounds: &'a Bounds,
40}
41
42#[derive(Debug, Clone)]
43pub enum Action {
44 Generic,
45 Command(String),
46}
47pub type DrawFn = fn(event: &mut DrawEvent);
48pub type LayoutFn = fn(layout: &mut LayoutEvent);
49pub type InputFn = fn(event: &mut GuiEvent) -> Option<Action>;
50
51#[derive(Debug)]
52pub struct Theme {
53 pub bg: Rgb565,
54 pub fg: Rgb565,
55 pub panel_bg: Rgb565,
56 pub selected_bg: Rgb565,
57 pub selected_fg: Rgb565,
58 pub font: MonoFont<'static>,
59 pub bold_font: MonoFont<'static>,
60}
61
62pub type Callback = fn(event: &mut GuiEvent);
63
64#[derive(Debug, Clone)]
65pub enum KeyboardAction {
66 Left,
67 Right,
68 Up,
69 Down,
70 Backspace,
71 Return,
72}
73#[derive(Debug, Clone)]
74pub enum EventType {
75 Generic,
76 Unknown,
77 Tap(Point),
78 Scroll(i32, i32),
79 Keyboard(u8),
80 KeyboardAction(KeyboardAction),
81 Action(),
82}
83#[derive(Debug)]
84pub struct GuiEvent<'a> {
85 pub scene: &'a mut Scene,
86 pub target: &'a ViewId,
87 pub event_type: EventType,
88 pub action: Option<Action>,
89}
90
91#[derive(Debug)]
92pub struct LayoutEvent<'a> {
93 pub scene: &'a mut Scene,
94 pub target: &'a ViewId,
95 pub space: Size,
96 pub theme: &'a Theme,
97}
98
99impl<'a> LayoutEvent<'a> {
100 pub(crate) fn layout_all_children(&mut self, name: &ViewId, space: Size) {
101 let fixed_kids = self.scene.get_children_ids(name);
102 for kid in &fixed_kids {
103 self.layout_child(kid, space);
104 }
105 }
106 pub(crate) fn layout_child(&mut self, kid: &ViewId, available_space: Size) {
107 if let Some(view) = self.scene.get_view_mut(kid) {
108 if let Some(layout) = view.layout {
109 let mut pass = LayoutEvent {
110 target: kid,
111 space: available_space,
112 scene: self.scene,
113 theme: self.theme,
114 };
115 layout(&mut pass);
116 }
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::button::make_button;
125 use crate::gfx::TextStyle;
126 use crate::scene::{click_at, draw_scene, event_at_focused, pick_at};
127 use crate::test::MockDrawingContext;
128 use crate::view::Align;
129 use alloc::boxed::Box;
130 use alloc::string::ToString;
131 use alloc::vec;
132 use alloc::vec::Vec;
133 use log::{LevelFilter, info};
134 use std::sync::Once;
135 use test_log::test;
136
137 extern crate std;
138
139 pub fn make_simple_view(name: &ViewId) -> View {
140 View {
141 name: name.clone(),
142 title: name.to_string(),
143 bounds: Bounds::new(0, 0, 10, 10),
144 visible: true,
145 draw: Some(|e| e.ctx.fill_rect(&e.view.bounds, &e.theme.bg)),
146 input: None,
147 state: None,
148 layout: None,
149 ..Default::default()
150 }
151 }
152 fn layout_vbox(evt: &mut LayoutEvent) {
153 if let Some(parent) = evt.scene.get_view_mut(evt.target) {
154 let mut y = 0;
155 let bounds = parent.bounds;
156 let kids = evt.scene.get_children_ids(evt.target);
157 for kid in kids {
158 if let Some(ch) = evt.scene.get_view_mut(&kid) {
159 ch.bounds.position.x = 0;
160 ch.bounds.position.y = y;
161 ch.bounds.size.w = bounds.w();
162 y += ch.bounds.h();
163 }
164 }
165 }
166 }
167 fn make_vbox(name: &ViewId, bounds: Bounds) -> View {
168 View {
169 name: name.clone(),
170 title: name.to_string(),
171 bounds,
172 visible: true,
173 draw: Some(|e| {
174 e.ctx.fill_rect(&e.view.bounds, &e.theme.panel_bg);
175 }),
176 input: None,
177 state: None,
178 layout: Some(layout_vbox),
179 ..Default::default()
180 }
181 }
182 struct TestButtonState {
183 drawn: bool,
184 got_input: bool,
185 }
186 fn make_test_button(name: &ViewId) -> View {
187 View {
188 name: name.clone(),
189 title: name.to_string(),
190 bounds: Bounds::new(0, 0, 20, 20),
191 visible: true,
192 draw: Some(|e| {
193 if let Some(state) = &mut e.view.state {
194 if let Some(state) = state.downcast_mut::<TestButtonState>() {
195 state.drawn = true;
196 }
197 }
198 }),
199 input: Some(|e| {
200 if let Some(view) = e.scene.get_view_mut(e.target) {
201 if let Some(state) = &mut view.state {
202 if let Some(state) = state.downcast_mut::<TestButtonState>() {
203 state.got_input = true;
204 }
205 }
206 }
207 None
208 }),
209 state: Some(Box::new(TestButtonState {
210 drawn: false,
211 got_input: false,
212 })),
213 layout: None,
214 ..Default::default()
215 }
216 }
217 fn make_text_box(name: &ViewId, title: &str) -> View {
218 View {
219 name: name.clone(),
220 title: title.into(),
221 bounds: Bounds::new(0, 0, 100, 30),
222 visible: true,
223 state: None,
224 draw: None,
225 layout: None,
226 input: Some(|e| {
227 match e.event_type {
228 EventType::Keyboard(key) => {
229 info!("got a keyboard event {}", key);
230 if let Some(view) = e.scene.get_view_mut(e.target) {
231 view.title.push(key as char)
232 }
233 }
234 _ => info!("ignoring other event"),
235 };
236 None
237 }),
238 ..Default::default()
239 }
240 }
241 fn draw_label_view(e: &mut DrawEvent) {
242 e.ctx.fill_text(
243 &e.view.bounds,
244 &e.view.title,
245 &TextStyle::new(&e.theme.font, &e.theme.fg),
246 );
247 }
248 fn make_label(name: &ViewId) -> View {
249 View {
250 name: name.clone(),
251 title: name.to_string(),
252 bounds: Bounds::new(0, 0, 30, 20),
253 visible: true,
254 draw: Some(draw_label_view),
255 input: None,
256 state: None,
257 layout: None,
258 ..Default::default()
259 }
260 }
261 fn was_button_clicked(scene: &mut Scene, name: &ViewId) -> bool {
262 scene
263 .get_view(name)
264 .unwrap()
265 .state
266 .as_ref()
267 .unwrap()
268 .downcast_ref::<TestButtonState>()
269 .unwrap()
270 .got_input
271 }
272 fn was_button_drawn(scene: &mut Scene, name: &ViewId) -> bool {
273 scene
274 .get_view(name)
275 .unwrap()
276 .state
277 .as_ref()
278 .unwrap()
279 .downcast_ref::<TestButtonState>()
280 .unwrap()
281 .drawn
282 }
283
284 fn repaint(scene: &mut Scene) {
285 let theme = MockDrawingContext::make_mock_theme();
286 let mut ctx = MockDrawingContext::new(scene);
287 draw_scene(scene, &mut ctx, &theme);
288 scene.dirty_rect = Bounds::new_empty();
289 }
290
291 #[test]
292 fn test_pick_at() {
293 let mut scene: Scene = Scene::new();
294 let vbox = make_vbox(&"parent".into(), Bounds::new(10, 10, 100, 100));
295
296 let mut button = make_test_button(&ViewId::new("child"));
297 button.bounds = Bounds::new(10, 10, 10, 10);
298
299 scene.add_view_to_parent(button, &vbox.name);
300 scene.add_view_to_root(vbox);
301 assert_eq!(pick_at(&mut scene, &Point { x: 5, y: 5 }).len(), 1);
302 assert_eq!(pick_at(&mut scene, &Point { x: 15, y: 15 }).len(), 2);
303 assert_eq!(pick_at(&mut scene, &Point { x: 25, y: 25 }).len(), 3);
304 }
305 #[test]
306 fn test_layout() {
307 let parent: ViewId = "parent".into();
308 let theme = MockDrawingContext::make_mock_theme();
309 let mut scene: Scene = Scene::new();
310 scene.add_view(make_vbox(&parent, Bounds::new(10, 10, 100, 100)));
312 scene.add_view_to_parent(make_test_button(&ViewId::new("button1")), &parent);
314 scene.add_view_to_parent(make_label(&"button2".into()), &parent);
316 let space = scene.bounds.size.clone();
318 layout_vbox(&mut LayoutEvent {
319 scene: &mut scene,
320 target: &"parent".into(),
321 theme: &theme,
322 space: space,
323 });
324 assert_eq!(
325 scene.get_view_bounds(&"parent".into()),
326 Some(Bounds::new(10, 10, 100, 100))
327 );
328 assert_eq!(
329 scene.get_view_bounds(&"button1".into()),
330 Some(Bounds::new(0, 0, 100, 20)),
331 );
332 assert_eq!(
333 scene.get_view_bounds(&"button2".into()),
334 Some(Bounds::new(0, 20, 100, 20))
335 );
336 }
337 #[test]
338 fn test_repaint() {
339 let mut scene = Scene::new();
340 scene.add_view(make_vbox(&"parent".into(), Bounds::new(10, 10, 100, 100)));
342 scene.add_view(make_test_button(&ViewId::new("button1")));
344 scene.add_view(make_test_button(&ViewId::new("button2")));
346
347 assert_eq!(scene.dirty, true);
348 repaint(&mut scene);
349 assert_eq!(scene.dirty, false);
350 }
351 #[test]
352 fn test_events() {
353 let mut scene: Scene = Scene::new();
354 let mut handlers: Vec<Callback> = vec![];
355 handlers.push(|event| {
356 info!("got an event {:?}", event);
357 if let Some(view) = event.scene.get_view_mut(event.target) {
358 view.visible = false;
359 }
360 event.scene.dirty = true;
361 });
362 handlers.push(|event| {
363 info!("got another event {:?}", event);
364 if let Some(view) = event.scene.get_view_mut(event.target) {
365 view.visible = false;
366 }
367 event.scene.dirty = true;
368 info!("the action is {:?}", event.action);
369 });
370 assert_eq!(scene.get_view(&"root".into()).unwrap().visible, true);
371 click_at(&mut scene, &handlers, Point::new(5, 5));
372 assert_eq!(scene.get_view(&"root".into()).unwrap().visible, false);
373 }
374 fn handle_toggle_button_input(event: &mut GuiEvent) -> Option<Action> {
375 if let Some(view) = event.scene.get_view_mut(event.target) {
377 view.state.insert(Box::new(String::from("enabled")));
378 }
379 None
380 }
381 #[test]
382 fn test_toggle_button() {
383 let mut scene = Scene::new();
384 let button = View {
386 name: ViewId::new("toggle"),
387 title: String::from("Off"),
388 visible: true,
389 bounds: Bounds::new(10, 10, 20, 20),
390 draw: Some(|e| {
391 if let Some(state) = &e.view.state {
392 if let Some(state) = state.downcast_ref::<String>() {
393 if state == "enabled" {
394 e.ctx.fill_rect(&e.view.bounds, &e.theme.fg);
395 e.ctx.stroke_rect(&e.view.bounds, &e.theme.bg);
396 let style = TextStyle::new(&e.theme.font, &e.theme.bg)
397 .with_halign(Align::Center);
398 e.ctx.fill_text(&e.view.bounds, &e.view.title, &style);
399 } else {
400 e.ctx.fill_rect(&e.view.bounds, &e.theme.bg);
401 e.ctx.stroke_rect(&e.view.bounds, &e.theme.fg);
402 let style = TextStyle::new(&e.theme.font, &e.theme.fg)
403 .with_halign(Align::Center);
404 e.ctx.fill_text(&e.view.bounds, &e.view.title, &style);
405 }
406 }
407 }
408 }),
409 input: Some(handle_toggle_button_input),
410 state: Some(Box::new(String::from("disabled"))),
411 layout: None,
412 ..Default::default()
413 };
414 scene.add_view_to_root(button);
415 repaint(&mut scene);
417 assert_eq!(scene.get_view(&"toggle".into()).unwrap().visible, true);
418 assert_eq!(
419 &scene
420 .get_view(&"toggle".into())
421 .as_ref()
422 .unwrap()
423 .state
424 .as_ref()
425 .unwrap()
426 .downcast_ref::<String>()
427 .unwrap(),
428 &"disabled"
429 );
430 let handlers = vec![];
432 click_at(&mut scene, &handlers, Point::new(15, 15));
433 assert_eq!(
435 &scene
436 .get_view(&"toggle".into())
437 .as_ref()
438 .unwrap()
439 .state
440 .as_ref()
441 .unwrap()
442 .downcast_ref::<String>()
443 .unwrap(),
444 &"enabled"
445 );
446 }
447 #[test]
448 fn test_make_visible() {
449 let mut scene = Scene::new();
451
452 let mut button1 = make_test_button(&ViewId::new("button1"));
454 button1.visible = true;
455 scene.add_view_to_root(button1);
456
457 let mut button2 = make_test_button(&ViewId::new("button2"));
459 button2.bounds.position.x = 100;
460 button2.visible = false;
462 scene.add_view_to_root(button2);
463
464 assert_eq!(was_button_clicked(&mut scene, &"button1".into()), false);
465 assert_eq!(was_button_drawn(&mut scene, &"button1".into()), false);
466 assert_eq!(was_button_drawn(&mut scene, &"button2".into()), false);
467
468 repaint(&mut scene);
470 assert_eq!(scene.dirty, false);
471 assert_eq!(was_button_drawn(&mut scene, &"button1".into()), true);
472 assert_eq!(was_button_drawn(&mut scene, &"button2".into()), false);
473
474 let mut handlers: Vec<Callback> = vec![];
475 handlers.push(|e| {
476 info!("clicked on {}", e.target);
477 if let Some(view) = e.scene.get_view_mut(&"button2".into()) {
478 view.visible = true;
479 e.scene.dirty = true;
480 }
481 });
482
483 assert_eq!(scene.dirty, false);
485 click_at(&mut scene, &handlers, Point::new(15, 15));
486 assert_eq!(was_button_clicked(&mut scene, &"button1".into()), true);
487 assert_eq!(scene.dirty, true);
489
490 repaint(&mut scene);
492 assert_eq!(scene.dirty, false);
493 assert_eq!(was_button_drawn(&mut scene, &"button1".into()), true);
494 assert_eq!(was_button_drawn(&mut scene, &"button2".into()), true);
495 }
496 #[test]
497 fn test_keyboard_events() {
498 let mut scene: Scene = Scene::new();
500
501 let text_box = make_text_box(&ViewId::new("textbox1"), "foo");
503 scene.add_view_to_root(text_box);
504 assert_eq!(get_view_title(&scene, ViewId::new("textbox1")), "foo");
506 scene.focused = Some("textbox1".into());
508
509 event_at_focused(&mut scene, &EventType::Keyboard(b'X'));
511 assert_eq!(get_view_title(&scene, ViewId::new("textbox1")), "fooX");
513 }
514
515 #[test]
516 fn test_draw2() {
517 let mut scene = Scene::new();
518 let view = View {
519 name: "view".into(),
520 title: "view".into(),
521 bounds: Bounds::new(0, 0, 10, 10),
522 visible: true,
523 draw: Some(|e| {
524 let mut color = &e.theme.fg;
525 if e.focused.is_some() && e.view.name.eq(e.focused.as_ref().unwrap()) {
526 color = &e.theme.bg;
527 }
528 e.ctx.fill_rect(&e.view.bounds, color);
529 }),
530 state: None,
531 input: None,
532 layout: None,
533 ..Default::default()
534 };
535
536 scene.add_view_to_root(view);
537 repaint(&mut scene);
538 }
539
540 #[test]
541 fn test_cliprect() {
542 let mut scene = Scene::new();
544 let button = make_button(&"button".into(), "Button").position_at(20, 20);
546 scene.add_view_to_root(button);
547 assert_eq!(scene.dirty, true);
548 assert_eq!(scene.dirty_rect, scene.bounds);
550 assert_eq!(scene.dirty_rect.is_empty(), false);
551 repaint(&mut scene);
553 assert_eq!(scene.dirty, false);
555 assert_eq!(scene.dirty_rect.is_empty(), true);
556 click_at(&mut scene, &vec![], Point::new(30, 30));
558 assert_eq!(scene.dirty, true);
560 assert_eq!(
561 scene.dirty_rect,
562 scene.get_view(&"button".into()).unwrap().bounds
563 );
564 repaint(&mut scene);
566 assert_eq!(scene.dirty, false);
567 assert_eq!(scene.dirty_rect.is_empty(), true);
568 }
570 #[test]
571 fn test_cliprect_nested() {
572 let mut scene = Scene::new();
573 let panel1 = View {
574 name: "panel1".into(),
575 bounds: Bounds::new(10, 10, 100, 100),
576 ..Default::default()
577 };
578 scene.add_view_to_root(panel1);
579 let panel2 = View {
580 name: "panel2".into(),
581 bounds: Bounds::new(10, 10, 100, 100),
582 ..Default::default()
583 };
584 scene.add_view_to_parent(panel2, &("panel1".into()));
585 let button_id = ViewId::new("button");
586 let button = make_button(&button_id, "Button").position_at(20, 20);
587 scene.add_view_to_parent(button, &("panel2".into()));
588
589 repaint(&mut scene);
591 assert_eq!(scene.dirty, false);
593 assert_eq!(scene.dirty_rect.is_empty(), true);
594 assert!(scene.focused.is_none());
596
597 click_at(&mut scene, &vec![], Point::new(45, 45));
598 scene.dump();
599 assert!(scene.focused.is_some());
601 assert!(scene.focused.is_some_and(|id| id == button_id));
602 assert_eq!(scene.dirty, true);
603 assert_eq!(scene.dirty_rect, Bounds::new(40, 40, 100, 100));
604 }
605
606 fn get_view_title(scene: &Scene, name: ViewId) -> String {
607 scene.get_view(&name).unwrap().title.clone()
608 }
609}