iris_ui/
list_view.rs

1use crate::geom::Bounds;
2use crate::gfx::draw_centered_text;
3use crate::view::{View, ViewId};
4use crate::{Action, DrawEvent, EventType, GuiEvent, KeyboardAction, LayoutEvent};
5use alloc::boxed::Box;
6use alloc::string::{String, ToString};
7use alloc::vec::Vec;
8use core::any::Any;
9use core::option::Option::Some;
10use log::info;
11
12pub fn make_list_view(name: &ViewId, data: Vec<&str>, selected: usize) -> View {
13    View {
14        name: name.clone(),
15        title: name.as_str().into(),
16        state: Some(ListState::new_with(data, selected)),
17        input: Some(input_list),
18        layout: Some(layout_list),
19        draw: Some(draw_list),
20        ..Default::default()
21    }
22}
23
24pub struct ListState {
25    pub items: Vec<String>,
26    pub selected: usize,
27}
28
29impl ListState {
30    pub fn select_next(&mut self) {
31        if self.selected < self.items.len() - 1 {
32            self.selected += 1;
33        }
34    }
35    pub fn select_prev(&mut self) {
36        if self.selected > 0 {
37            self.selected -= 1;
38        }
39    }
40}
41
42impl ListState {
43    pub fn new_with(items: Vec<&str>, selected: usize) -> Box<dyn Any> {
44        Box::new(ListState {
45            items: items.iter().map(|s| s.to_string()).collect(),
46            selected,
47        })
48    }
49}
50
51fn input_list(e: &mut GuiEvent) -> Option<Action> {
52    match &e.event_type {
53        EventType::Tap(pt) => {
54            e.scene.mark_dirty_view(e.target);
55            e.scene.set_focused(e.target);
56            if let Some(view) = e.scene.get_view_mut(e.target) {
57                let bounds = view.bounds;
58                if let Some(state) = view.get_state::<ListState>() {
59                    let cell_height = bounds.h() / (state.items.len() as i32);
60                    let y = pt.y - bounds.y();
61                    let n = y / cell_height;
62                    if n >= 0 && n < state.items.len() as i32 {
63                        state.selected = n as usize;
64                        return Some(Action::Command(state.items[state.selected].clone()));
65                    }
66                }
67            }
68        }
69        EventType::Scroll(x, y) => {
70            e.scene.mark_dirty_view(e.target);
71            if let Some(state) = e.scene.get_view_state::<ListState>(e.target) {
72                if *y > 0 {
73                    state.select_next();
74                }
75                if *y < 0 {
76                    state.select_prev();
77                }
78            }
79        }
80        EventType::KeyboardAction(action) => {
81            e.scene.mark_dirty_view(e.target);
82            if let Some(state) = e.scene.get_view_state::<ListState>(e.target) {
83                match action {
84                    KeyboardAction::Up => state.select_prev(),
85                    KeyboardAction::Down => state.select_next(),
86                    KeyboardAction::Return => {
87                        info!("firmly selecting the item");
88                        return Some(Action::Command(
89                            state.items[state.selected as usize].clone(),
90                        ));
91                    }
92                    _ => {}
93                }
94            }
95        }
96        _ => {}
97    }
98    None
99}
100
101fn draw_list(e: &mut DrawEvent) {
102    let bounds = e.view.bounds;
103    e.ctx.fill_rect(&e.view.bounds, &e.theme.bg);
104    let name = e.view.name.clone();
105    if let Some(state) = e.view.get_state::<ListState>() {
106        let cell_height = bounds.h() / (state.items.len() as i32);
107        for (i, item) in state.items.iter().enumerate() {
108            let (bg, fg) = if i == state.selected {
109                (&e.theme.selected_bg, &e.theme.selected_fg)
110            } else {
111                (&e.theme.bg, &e.theme.fg)
112            };
113            let bds = Bounds::new(
114                bounds.x(),
115                bounds.y() + (i as i32) * cell_height + 1,
116                bounds.w(),
117                cell_height - 1,
118            );
119            // draw background only if selected
120            if i == state.selected {
121                e.ctx.fill_rect(&bds, bg);
122                if let Some(focused) = e.focused {
123                    if focused == &name {
124                        e.ctx.stroke_rect(&bds.contract(2), fg);
125                    }
126                }
127            }
128
129            // draw text
130            draw_centered_text(e.ctx, item, &bds, &e.theme.font, fg);
131        }
132    }
133    e.ctx.stroke_rect(&e.view.bounds, &e.theme.fg);
134}
135
136fn layout_list(e: &mut LayoutEvent) {
137    if let Some(state) = e.scene.get_view_state::<ListState>(e.target) {
138        let ch = e.theme.font.character_size;
139        let height = state.items.len() as u32 * ch.height * 2;
140        if let Some(view) = e.scene.get_view_mut(e.target) {
141            view.bounds.size.h = height as i32;
142        }
143    }
144}
145mod tests {
146    use crate::geom::{Bounds, Point};
147    use crate::list_view::{ListState, make_list_view};
148    use crate::scene::{Scene, click_at, draw_scene, layout_scene};
149    use crate::test::MockDrawingContext;
150    use crate::view::ViewId;
151    use alloc::vec;
152
153    #[test]
154    fn test_list_view() {
155        let theme = MockDrawingContext::make_mock_theme();
156        let mut scene: Scene = Scene::new_with_bounds(Bounds::new(0, 0, 320, 240));
157
158        let listview = ViewId::new("listview");
159        {
160            let list = make_list_view(&listview, vec!["A", "BB", "CCC"], 0);
161            scene.add_view_to_root(list);
162        }
163        layout_scene(&mut scene, &theme);
164
165        {
166            let mut group = scene.get_view_mut(&listview).unwrap();
167            let state = &mut group.get_state::<ListState>().unwrap();
168            assert_eq!(state.selected, 0);
169        }
170
171        click_at(&mut scene, &vec![], Point::new(50, 30));
172
173        {
174            let state = &mut scene
175                .get_view_state::<ListState>(&ViewId::new("listview"))
176                .unwrap();
177            assert_eq!(state.selected, 1);
178        }
179
180        let mut ctx = MockDrawingContext::new(&scene);
181        assert_eq!(scene.dirty, true);
182        draw_scene(&mut scene, &mut ctx, &theme);
183        assert_eq!(scene.dirty, false);
184    }
185}