embedded_menu/selection_indicator/
mod.rs

1use crate::{
2    adapters::color_map::BinaryColorDrawTargetExt,
3    collection::MenuItemCollection,
4    interaction::{InputAdapterSource, InputState},
5    margin::Insets,
6    selection_indicator::style::IndicatorStyle,
7    theme::Theme,
8    MenuState, MenuStyle,
9};
10use embedded_graphics::{
11    prelude::{DrawTarget, DrawTargetExt, Point, Size},
12    primitives::Rectangle,
13    transform::Transform,
14};
15
16pub mod style;
17
18pub trait SelectionIndicatorController: Copy {
19    type State: Default + Copy;
20
21    fn update_target(&self, state: &mut Self::State, y: i32);
22    fn jump_to_target(&self, state: &mut Self::State);
23    fn offset(&self, state: &Self::State) -> i32;
24    fn update(&self, state: &mut Self::State);
25}
26
27#[derive(Clone, Copy, Default)]
28pub struct StaticState {
29    y_offset: i32,
30}
31
32#[derive(Clone, Copy)]
33pub struct StaticPosition;
34
35impl SelectionIndicatorController for StaticPosition {
36    type State = StaticState;
37
38    fn update_target(&self, state: &mut Self::State, y: i32) {
39        state.y_offset = y;
40    }
41
42    fn jump_to_target(&self, _state: &mut Self::State) {}
43
44    fn offset(&self, state: &Self::State) -> i32 {
45        state.y_offset
46    }
47
48    fn update(&self, _state: &mut Self::State) {}
49}
50
51#[derive(Clone, Copy)]
52pub struct AnimatedPosition {
53    frames: i32,
54}
55
56#[derive(Clone, Copy, Default)]
57pub struct AnimatedState {
58    current: i32,
59    target: i32,
60}
61
62impl AnimatedPosition {
63    pub const fn new(frames: i32) -> Self {
64        Self { frames }
65    }
66}
67
68impl SelectionIndicatorController for AnimatedPosition {
69    type State = AnimatedState;
70
71    fn update_target(&self, state: &mut Self::State, y: i32) {
72        state.target = y;
73    }
74
75    fn jump_to_target(&self, state: &mut Self::State) {
76        state.current = state.target;
77    }
78
79    fn offset(&self, state: &Self::State) -> i32 {
80        state.current
81    }
82
83    fn update(&self, state: &mut Self::State) {
84        let rounding = if state.current < state.target {
85            self.frames - 1
86        } else {
87            1 - self.frames
88        };
89
90        let distance = state.target - state.current;
91        state.current += (distance + rounding) / self.frames;
92    }
93}
94
95pub struct State<P, S>
96where
97    P: SelectionIndicatorController,
98    S: IndicatorStyle,
99{
100    position: P::State,
101    state: S::State,
102}
103
104impl<P, S> Default for State<P, S>
105where
106    P: SelectionIndicatorController,
107    S: IndicatorStyle,
108{
109    fn default() -> Self {
110        Self {
111            position: Default::default(),
112            state: Default::default(),
113        }
114    }
115}
116
117impl<P, S> Clone for State<P, S>
118where
119    P: SelectionIndicatorController,
120    S: IndicatorStyle,
121{
122    fn clone(&self) -> Self {
123        *self
124    }
125}
126
127impl<P, S> Copy for State<P, S>
128where
129    P: SelectionIndicatorController,
130    S: IndicatorStyle,
131{
132}
133
134#[derive(Clone, Copy, Debug)]
135pub(crate) struct Indicator<P, S> {
136    pub controller: P,
137    pub style: S,
138}
139
140impl<P, S> Indicator<P, S>
141where
142    P: SelectionIndicatorController,
143    S: IndicatorStyle,
144{
145    pub fn offset(&self, state: &State<P, S>) -> i32 {
146        self.controller.offset(&state.position)
147    }
148
149    pub fn change_selected_item(&self, pos: i32, state: &mut State<P, S>) {
150        self.controller.update_target(&mut state.position, pos);
151        self.style.on_target_changed(&mut state.state);
152    }
153
154    pub fn jump_to_target(&self, state: &mut State<P, S>) {
155        self.controller.jump_to_target(&mut state.position);
156    }
157
158    pub fn update(&self, input_state: InputState, state: &mut State<P, S>) {
159        self.controller.update(&mut state.position);
160        self.style.update(&mut state.state, input_state);
161    }
162
163    pub fn item_height(&self, menuitem_height: i32, state: &State<P, S>) -> i32 {
164        let indicator_insets = self.style.padding(&state.state, menuitem_height);
165        menuitem_height + indicator_insets.top + indicator_insets.bottom
166    }
167
168    pub fn draw<R, D, IT, C>(
169        &self,
170        selected_height: i32,
171        selected_offset: i32,
172        input_state: InputState,
173        mut display: D,
174        items: &impl MenuItemCollection<R>,
175        style: &MenuStyle<S, IT, P, R, C>,
176        menu_state: &MenuState<IT::InputAdapter, P, S>,
177    ) -> Result<(), D::Error>
178    where
179        D: DrawTarget<Color = C::Color>,
180        IT: InputAdapterSource<R>,
181        P: SelectionIndicatorController,
182        C: Theme,
183        S: IndicatorStyle,
184    {
185        let display_size = display.bounding_box().size;
186
187        // We treat the horizontal insets as padding, but the vertical insets only as an expansion
188        // for the selection indicator. Menu items are placed tightly, ignoring the vertical insets.
189        let Insets {
190            left: padding_left,
191            top: padding_top,
192            right: padding_right,
193            bottom: padding_bottom,
194        } = self
195            .style
196            .padding(&menu_state.indicator_state.state, selected_height);
197
198        // Draw the selection indicator
199        let selected_item_height = (selected_height + padding_top + padding_bottom) as u32;
200        let selected_item_area = Rectangle::new(
201            Point::new(0, selected_offset),
202            Size::new(display_size.width, selected_item_height),
203        );
204
205        let selection_area = self.style.draw(
206            &menu_state.indicator_state.state,
207            input_state,
208            &style.theme,
209            &mut display.cropped(&selected_item_area),
210        )?;
211
212        // Translate inverting area to its position
213        let mapping_area = selection_area.translate(selected_item_area.top_left);
214        let mut inverting = display.map_colors(
215            &mapping_area,
216            style.theme.text_color(),
217            style.theme.selected_text_color(),
218        );
219
220        // Draw the menu content
221        let content_width = (display_size.width as i32 - padding_left - padding_right) as u32;
222        let content_area = Rectangle::new(
223            Point::new(padding_left, padding_top),
224            Size::new(content_width, display_size.height),
225        );
226
227        items.draw_styled(
228            &style.text_style(),
229            &mut inverting
230                .clipped(&content_area)
231                .translated(content_area.top_left - Point::new(0, menu_state.list_offset)),
232        )
233    }
234}