embedded_menu/selection_indicator/
mod.rs1use 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 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 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 pub fn update_target(&self, pos: i32, state: &mut State<P, S>) {
167 self.controller.update_target(&mut state.position, pos);
168 }
169
170 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 pub fn update(&self, input_state: InputState, state: &mut State<P, S>) -> bool {
181 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 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 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 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 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}