Skip to main content

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
25    /// Advances the animation by a single frame.
26    ///
27    /// Returns whether the indicator's position changed, i.e. whether the menu needs to be
28    /// redrawn.
29    fn update(&self, state: &mut Self::State) -> bool;
30}
31
32#[derive(Clone, Copy, Default)]
33pub struct StaticState {
34    y_offset: i32,
35}
36
37#[derive(Clone, Copy)]
38pub struct StaticPosition;
39
40impl SelectionIndicatorController for StaticPosition {
41    type State = StaticState;
42
43    fn update_target(&self, state: &mut Self::State, y: i32) {
44        state.y_offset = y;
45    }
46
47    fn jump_to_target(&self, _state: &mut Self::State) {}
48
49    fn offset(&self, state: &Self::State) -> i32 {
50        state.y_offset
51    }
52
53    fn update(&self, _state: &mut Self::State) -> bool {
54        false
55    }
56}
57
58#[derive(Clone, Copy)]
59pub struct AnimatedPosition {
60    frames: i32,
61}
62
63#[derive(Clone, Copy, Default)]
64pub struct AnimatedState {
65    current: i32,
66    target: i32,
67}
68
69impl AnimatedPosition {
70    /// Creates an indicator that takes the given number of frames to reach its target.
71    ///
72    /// # Panics
73    ///
74    /// The animation must take at least one frame.
75    pub const fn new(frames: i32) -> Self {
76        assert!(frames >= 1, "the animation must take at least one frame");
77        Self { frames }
78    }
79}
80
81impl SelectionIndicatorController for AnimatedPosition {
82    type State = AnimatedState;
83
84    fn update_target(&self, state: &mut Self::State, y: i32) {
85        state.target = y;
86    }
87
88    fn jump_to_target(&self, state: &mut Self::State) {
89        state.current = state.target;
90    }
91
92    fn offset(&self, state: &Self::State) -> i32 {
93        state.current
94    }
95
96    fn update(&self, state: &mut Self::State) -> bool {
97        let rounding = if state.current < state.target {
98            self.frames - 1
99        } else {
100            1 - self.frames
101        };
102
103        let distance = state.target - state.current;
104        let step = (distance + rounding) / self.frames;
105        state.current += step;
106
107        step != 0
108    }
109}
110
111pub struct State<P, S>
112where
113    P: SelectionIndicatorController,
114    S: IndicatorStyle,
115{
116    position: P::State,
117    state: S::State,
118}
119
120impl<P, S> Default for State<P, S>
121where
122    P: SelectionIndicatorController,
123    S: IndicatorStyle,
124{
125    fn default() -> Self {
126        Self {
127            position: Default::default(),
128            state: Default::default(),
129        }
130    }
131}
132
133impl<P, S> Clone for State<P, S>
134where
135    P: SelectionIndicatorController,
136    S: IndicatorStyle,
137{
138    fn clone(&self) -> Self {
139        *self
140    }
141}
142
143impl<P, S> Copy for State<P, S>
144where
145    P: SelectionIndicatorController,
146    S: IndicatorStyle,
147{
148}
149
150#[derive(Clone, Copy, Debug)]
151pub(crate) struct Indicator<P, S> {
152    pub controller: P,
153    pub style: S,
154}
155
156impl<P, S> Indicator<P, S>
157where
158    P: SelectionIndicatorController,
159    S: IndicatorStyle,
160{
161    pub fn offset(&self, state: &State<P, S>) -> i32 {
162        self.controller.offset(&state.position)
163    }
164
165    /// Moves the indicator to a new position, without restarting the style's animation.
166    pub fn update_target(&self, pos: i32, state: &mut State<P, S>) {
167        self.controller.update_target(&mut state.position, pos);
168    }
169
170    /// Restarts the style's animation, because the indicator points at a different item now.
171    pub fn on_target_changed(&self, state: &mut State<P, S>) {
172        self.style.on_target_changed(&mut state.state);
173    }
174
175    pub fn jump_to_target(&self, state: &mut State<P, S>) {
176        self.controller.jump_to_target(&mut state.position);
177    }
178
179    /// Advances the animations by a single frame, returning whether anything moved.
180    pub fn update(&self, input_state: InputState, state: &mut State<P, S>) -> bool {
181        // Both updates must run, so avoid short-circuiting.
182        let position_changed = self.controller.update(&mut state.position);
183        let style_changed = self.style.update(&mut state.state, input_state);
184
185        position_changed || style_changed
186    }
187
188    pub fn item_height(&self, menuitem_height: i32, state: &State<P, S>) -> i32 {
189        let indicator_insets = self.style.padding(&state.state, menuitem_height);
190        menuitem_height + indicator_insets.top + indicator_insets.bottom
191    }
192
193    pub fn draw<R, D, IT, C>(
194        &self,
195        selected_height: i32,
196        selected_offset: i32,
197        input_state: InputState,
198        mut display: D,
199        items: &impl MenuItemCollection<R>,
200        style: &MenuStyle<S, IT, P, R, C>,
201        menu_state: &MenuState<IT::InputAdapter, P, S>,
202    ) -> Result<(), D::Error>
203    where
204        D: DrawTarget<Color = C::Color>,
205        IT: InputAdapterSource<R>,
206        P: SelectionIndicatorController,
207        C: Theme,
208        S: IndicatorStyle,
209    {
210        let display_size = display.bounding_box().size;
211
212        // We treat the horizontal insets as padding, but the vertical insets only as an expansion
213        // for the selection indicator. Menu items are placed tightly, ignoring the vertical insets.
214        let Insets {
215            left: padding_left,
216            top: padding_top,
217            right: padding_right,
218            bottom: padding_bottom,
219        } = self
220            .style
221            .padding(&menu_state.indicator_state.state, selected_height);
222
223        // Draw the selection indicator
224        let selected_item_height = (selected_height + padding_top + padding_bottom) as u32;
225        let selected_item_area = Rectangle::new(
226            Point::new(0, selected_offset),
227            Size::new(display_size.width, selected_item_height),
228        );
229
230        let selection_area = self.style.draw(
231            &menu_state.indicator_state.state,
232            input_state,
233            &style.theme,
234            &mut display.cropped(&selected_item_area),
235        )?;
236
237        // Translate inverting area to its position
238        let mapping_area = selection_area.translate(selected_item_area.top_left);
239        let mut inverting = display.map_colors(
240            &mapping_area,
241            style.theme.text_color(),
242            style.theme.selected_text_color(),
243        );
244
245        // Draw the menu content
246        let content_width = (display_size.width as i32 - padding_left - padding_right) as u32;
247        let content_area = Rectangle::new(
248            Point::new(padding_left, padding_top),
249            Size::new(content_width, display_size.height),
250        );
251
252        items.draw_styled(
253            &style.text_style(),
254            &mut inverting
255                .clipped(&content_area)
256                .translated(content_area.top_left - Point::new(0, menu_state.list_offset)),
257        )
258    }
259}