embedded_menu/selection_indicator/style/
animated_triangle.rs1use embedded_graphics::{
2 prelude::{DrawTarget, Point},
3 primitives::Rectangle,
4 transform::Transform,
5};
6
7use crate::{
8 interaction::InputState,
9 selection_indicator::{
10 style::{interpolate, triangle::Arrow, IndicatorStyle},
11 Insets,
12 },
13 theme::Theme,
14};
15
16#[derive(Clone, Copy)]
17pub struct AnimatedTriangle {
18 period: i32,
19}
20
21impl AnimatedTriangle {
22 pub const fn new(period: i32) -> Self {
28 assert!(period >= 5, "the animation period must be at least 5");
29 Self { period }
30 }
31
32 const fn offset(&self, current: i32) -> i32 {
36 let half_move = self.period / 5;
37 let rest = 3 * half_move;
38
39 if current < rest {
40 0
41 } else if current < rest + half_move {
42 current - rest
43 } else {
44 self.period - current
45 }
46 }
47}
48
49#[derive(Default, Clone, Copy)]
50pub struct State {
51 current: i32,
52}
53
54impl IndicatorStyle for AnimatedTriangle {
55 type Shape = Arrow;
56 type State = State;
57
58 fn on_target_changed(&self, state: &mut Self::State) {
59 state.current = 0;
60 }
61
62 fn update(&self, state: &mut Self::State, input_state: InputState) -> bool {
63 let previous = state.current;
64
65 state.current = if input_state == InputState::Idle {
66 (state.current + 1) % self.period
67 } else {
68 0
69 };
70
71 self.offset(previous) != self.offset(state.current)
72 }
73
74 fn padding(&self, _state: &Self::State, height: i32) -> Insets {
75 Insets {
76 left: height / 2 + 1,
77 top: 0,
78 right: 0,
79 bottom: 0,
80 }
81 }
82
83 fn shape(&self, state: &Self::State, bounds: Rectangle, fill_width: u32) -> Self::Shape {
84 let max_offset = Self::Shape::tip_width(bounds);
85
86 let half_move = self.period / 5;
87 let offset = self.offset(state.current) * max_offset / half_move;
88
89 Arrow::new(bounds, fill_width).translate(Point::new(-offset, 0))
90 }
91
92 fn draw<T, D>(
93 &self,
94 state: &Self::State,
95 input_state: InputState,
96 theme: &T,
97 display: &mut D,
98 ) -> Result<Self::Shape, D::Error>
99 where
100 T: Theme,
101 D: DrawTarget<Color = T::Color>,
102 {
103 let display_area = display.bounding_box();
104
105 let fill_width = if let InputState::InProgress(progress) = input_state {
106 interpolate(progress as u32, 0, 255, 0, display_area.size.width)
107 } else {
108 0
109 };
110
111 let shape = self.shape(state, display_area, fill_width);
112
113 shape.draw(theme.selection_color(), display)?;
114
115 Ok(shape)
116 }
117}