Skip to main content

embedded_menu/selection_indicator/style/
animated_triangle.rs

1use 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    /// Creates an animation with the given period, in frames.
23    ///
24    /// # Panics
25    ///
26    /// The period must be at least 5 frames, as the animation is divided into fifths.
27    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    /// The arrow's horizontal offset, before scaling to the shape's width.
33    ///
34    /// The arrow rests at 0 for the majority of the period.
35    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}