iris_ui/
lib.rs

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        // add panel
311        scene.add_view(make_vbox(&parent, Bounds::new(10, 10, 100, 100)));
312        // add button 1
313        scene.add_view_to_parent(make_test_button(&ViewId::new("button1")), &parent);
314        // add button 2
315        scene.add_view_to_parent(make_label(&"button2".into()), &parent);
316        // layout
317        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        // add panel
341        scene.add_view(make_vbox(&"parent".into(), Bounds::new(10, 10, 100, 100)));
342        // add button 1
343        scene.add_view(make_test_button(&ViewId::new("button1")));
344        // add button 2
345        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        // info!("view clicked {:?}", event.event_type);
376        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        // add toggle button
385        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
416        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        // click at
431        let handlers = vec![];
432        click_at(&mut scene, &handlers, Point::new(15, 15));
433        // confirm toggle button state has changed to enabled
434        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        // create scene
450        let mut scene = Scene::new();
451
452        // create button 1
453        let mut button1 = make_test_button(&ViewId::new("button1"));
454        button1.visible = true;
455        scene.add_view_to_root(button1);
456
457        // create button 2
458        let mut button2 = make_test_button(&ViewId::new("button2"));
459        button2.bounds.position.x = 100;
460        // make button 2 invisible
461        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. only button 1 should get drawn
469        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        // tap button 1
484        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        // confirm dirty
488        assert_eq!(scene.dirty, true);
489
490        // this time both buttons should be drawn
491        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        // make scene
499        let mut scene: Scene = Scene::new();
500
501        // make text box
502        let text_box = make_text_box(&ViewId::new("textbox1"), "foo");
503        scene.add_view_to_root(text_box);
504        // confirm text is correct
505        assert_eq!(get_view_title(&scene, ViewId::new("textbox1")), "foo");
506        // set text box as focused
507        scene.focused = Some("textbox1".into());
508
509        // send keyboard event
510        event_at_focused(&mut scene, &EventType::Keyboard(b'X'));
511        // confirm text is updated
512        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        // make scene
543        let mut scene = Scene::new();
544        // add button
545        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        // check that dirty area is same as bounds
549        assert_eq!(scene.dirty_rect, scene.bounds);
550        assert_eq!(scene.dirty_rect.is_empty(), false);
551        // draw
552        repaint(&mut scene);
553        // check that dirty area is empty
554        assert_eq!(scene.dirty, false);
555        assert_eq!(scene.dirty_rect.is_empty(), true);
556        // send tap to button
557        click_at(&mut scene, &vec![], Point::new(30, 30));
558        // check that dirty area is just for the button
559        assert_eq!(scene.dirty, true);
560        assert_eq!(
561            scene.dirty_rect,
562            scene.get_view(&"button".into()).unwrap().bounds
563        );
564        // draw
565        repaint(&mut scene);
566        assert_eq!(scene.dirty, false);
567        assert_eq!(scene.dirty_rect.is_empty(), true);
568        // check that button was redrawn
569    }
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        // draw
590        repaint(&mut scene);
591        // check that dirty area is empty
592        assert_eq!(scene.dirty, false);
593        assert_eq!(scene.dirty_rect.is_empty(), true);
594        // nothing should be focused yet
595        assert!(scene.focused.is_none());
596
597        click_at(&mut scene, &vec![], Point::new(45, 45));
598        scene.dump();
599        // now the button should be focused
600        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}