iris_ui/
toggle_group.rs

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