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 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_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}