embedded_menu/interaction/
single_touch.rs

1use core::marker::PhantomData;
2
3use crate::{
4    interaction::{
5        Action, InputAdapter, InputAdapterSource, InputResult, InputState, Interaction, Navigation,
6    },
7    selection_indicator::style::interpolate,
8};
9
10#[derive(Default, Debug, Clone, Copy)]
11pub struct State {
12    interaction_time: u32,
13    was_released: bool,
14    repeated: bool,
15}
16
17/// Single touch navigation in hierarchical lists
18///
19/// Short press: select next item
20/// Long press: activate current item
21#[derive(Clone, Copy)]
22pub struct SingleTouch {
23    /// Does not display short presses on the selection indicator.
24    pub ignore_time: u32,
25
26    /// Ignores touches shorter than this many update periods.
27    pub debounce_time: u32,
28
29    /// Detects long presses after this many update periods.
30    pub max_time: u32,
31}
32
33impl<R> InputAdapterSource<R> for SingleTouch {
34    type InputAdapter = SingleTouchAdapter<R>;
35
36    fn adapter(&self) -> Self::InputAdapter {
37        SingleTouchAdapter {
38            ignore_time: self.ignore_time,
39            debounce_time: self.debounce_time,
40            max_time: self.max_time,
41            marker: PhantomData,
42        }
43    }
44}
45
46/// Single touch navigation in hierarchical lists
47///
48/// Short press: select next item
49/// Long press: activate current item
50pub struct SingleTouchAdapter<R> {
51    ignore_time: u32,
52    debounce_time: u32,
53    max_time: u32,
54    marker: PhantomData<R>,
55}
56
57impl<R> Clone for SingleTouchAdapter<R> {
58    fn clone(&self) -> Self {
59        *self
60    }
61}
62
63impl<R> Copy for SingleTouchAdapter<R> {}
64
65impl<R> InputAdapter for SingleTouchAdapter<R> {
66    type Input = bool;
67    type Value = R;
68    type State = State;
69
70    fn handle_input(
71        &self,
72        state: &mut Self::State,
73        action: Self::Input,
74    ) -> InputResult<Self::Value> {
75        if !state.was_released {
76            if action {
77                return InputResult::from(InputState::Idle);
78            }
79            state.was_released = true;
80        }
81
82        if action {
83            state.interaction_time = state.interaction_time.saturating_add(1);
84            if state.interaction_time <= self.ignore_time && !state.repeated {
85                InputResult::from(InputState::Idle)
86            } else if state.interaction_time < self.max_time {
87                let ignore_time = if state.repeated { 0 } else { self.ignore_time };
88                InputResult::from(InputState::InProgress(interpolate(
89                    state.interaction_time - ignore_time,
90                    0,
91                    self.max_time - ignore_time,
92                    0,
93                    255,
94                ) as u8))
95            } else {
96                state.repeated = true;
97                state.interaction_time = 0;
98                InputResult::from(Interaction::Action(Action::Select))
99            }
100        } else {
101            let time = core::mem::replace(&mut state.interaction_time, 0);
102
103            if self.debounce_time < time && time < self.max_time && !state.repeated {
104                InputResult::from(Interaction::Navigation(Navigation::Next))
105            } else {
106                // Already interacted before releasing, ignore and reset.
107                state.repeated = false;
108                InputResult::from(InputState::Idle)
109            }
110        }
111    }
112}
113
114#[cfg(test)]
115mod test {
116    use crate::interaction::{
117        single_touch::SingleTouch, Action, InputAdapter, InputAdapterSource, InputResult,
118        InputState, Interaction, Navigation,
119    };
120
121    #[test]
122    fn test_interaction() {
123        // ignore 1 long pulses, accept 2-4 as short press, 5- as long press
124        let controller = SingleTouch {
125            ignore_time: 1,
126            debounce_time: 1,
127            max_time: 5,
128        }
129        .adapter();
130
131        let expectations: [&[(bool, InputResult<()>)]; 6] = [
132            &[
133                (false, InputState::Idle.into()),
134                (false, InputState::Idle.into()),
135                (false, InputState::Idle.into()),
136                (false, InputState::Idle.into()),
137                (false, InputState::Idle.into()),
138                (false, InputState::Idle.into()),
139            ],
140            // repeated short pulse ignored
141            &[
142                (true, InputState::Idle.into()),
143                (false, InputState::Idle.into()),
144                (true, InputState::Idle.into()),
145                (false, InputState::Idle.into()),
146                (true, InputState::Idle.into()),
147                (false, InputState::Idle.into()),
148            ],
149            // longer pulse recongised as Next event on falling edge
150            &[
151                (false, InputState::Idle.into()),
152                (true, InputState::Idle.into()),
153                (true, InputState::InProgress(63).into()),
154                (false, Interaction::Navigation(Navigation::Next).into()),
155            ],
156            &[
157                (false, InputState::Idle.into()),
158                (true, InputState::Idle.into()),
159                (true, InputState::InProgress(63).into()),
160                (true, InputState::InProgress(127).into()),
161                (false, Interaction::Navigation(Navigation::Next).into()),
162            ],
163            // long pulse NOT recognised as Select event on falling edge
164            &[
165                (false, InputState::Idle.into()),
166                (true, InputState::Idle.into()),
167                (true, InputState::InProgress(63).into()),
168                (true, InputState::InProgress(127).into()),
169                (true, InputState::InProgress(191).into()),
170                (false, Interaction::Navigation(Navigation::Next).into()),
171            ],
172            // long pulse recognised as Select event immediately
173            &[
174                (false, InputState::Idle.into()),
175                (true, InputState::Idle.into()),
176                (true, InputState::InProgress(63).into()),
177                (true, InputState::InProgress(127).into()),
178                (true, InputState::InProgress(191).into()),
179                (true, Interaction::Action(Action::Select).into()),
180                (true, InputState::InProgress(51).into()),
181                (true, InputState::InProgress(102).into()),
182                (true, InputState::InProgress(153).into()),
183                (true, InputState::InProgress(204).into()),
184                (true, Interaction::Action(Action::Select).into()),
185                (true, InputState::InProgress(51).into()),
186                (true, InputState::InProgress(102).into()),
187                (true, InputState::InProgress(153).into()),
188                (false, InputState::Idle.into()),
189            ],
190        ];
191
192        for (row, &inputs) in expectations.iter().enumerate() {
193            let mut controller_state = Default::default();
194
195            for (sample, (input, expectation)) in inputs.iter().enumerate() {
196                let ret = controller.handle_input(&mut controller_state, *input);
197
198                assert_eq!(
199                    ret, *expectation,
200                    "Mismatch at row {}, sample {}",
201                    row, sample
202                );
203            }
204        }
205    }
206}