Skip to main content

spitfire_gui/
interactions.rs

1use raui_core::{
2    application::Application,
3    interactive::{
4        InteractionsEngine,
5        default_interactions_engine::{
6            DefaultInteractionsEngine, DefaultInteractionsEngineResult, Interaction, PointerButton,
7        },
8    },
9    layout::CoordsMapping,
10    widget::{
11        component::interactive::navigation::{NavJump, NavScroll, NavSignal, NavTextChange},
12        utils::Vec2,
13    },
14};
15use spitfire_input::{ArrayInputCombinator, InputActionRef, InputCharactersRef, InputCombinator};
16
17const ZERO_THRESHOLD: f32 = 1.0e-6;
18
19#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum GuiCardinalDirection {
21    #[default]
22    None,
23    Up,
24    Down,
25    Left,
26    Right,
27}
28
29impl GuiCardinalDirection {
30    pub fn new(direction: [f32; 2], mut threshold: f32) -> Self {
31        threshold = threshold.max(ZERO_THRESHOLD);
32        let length = (direction[0] * direction[0] + direction[1] * direction[1]).sqrt();
33        if length < threshold {
34            GuiCardinalDirection::None
35        } else {
36            let normalized = [direction[0] / length, direction[1] / length];
37            let abs_x = normalized[0].abs();
38            let abs_y = normalized[1].abs();
39            if abs_x > abs_y {
40                if normalized[0] > 0.0 {
41                    GuiCardinalDirection::Right
42                } else {
43                    GuiCardinalDirection::Left
44                }
45            } else {
46                if normalized[1] > 0.0 {
47                    GuiCardinalDirection::Down
48                } else {
49                    GuiCardinalDirection::Up
50                }
51            }
52        }
53    }
54}
55
56pub struct GuiDefaultDirectionCombinator(pub InputCombinator<GuiCardinalDirection>);
57
58impl GuiDefaultDirectionCombinator {
59    pub fn new(
60        left: InputActionRef,
61        right: InputActionRef,
62        up: InputActionRef,
63        down: InputActionRef,
64        threshold: f32,
65    ) -> Self {
66        Self(InputCombinator::new(move || {
67            let direction = [
68                if left.get().is_pressed() {
69                    -1.0
70                } else if right.get().is_pressed() {
71                    1.0
72                } else {
73                    0.0
74                },
75                if up.get().is_pressed() {
76                    1.0
77                } else if down.get().is_pressed() {
78                    -1.0
79                } else {
80                    0.0
81                },
82            ];
83            GuiCardinalDirection::new(direction, threshold)
84        }))
85    }
86
87    pub fn get(&self) -> GuiCardinalDirection {
88        self.0.get()
89    }
90}
91
92#[derive(Default)]
93pub struct GuiInteractionsInputs {
94    pub trigger: InputActionRef,
95    pub context: InputActionRef,
96    pub cancel: InputActionRef,
97    pub direction: InputCombinator<GuiCardinalDirection>,
98    pub prev: InputActionRef,
99    pub next: InputActionRef,
100    pub text: InputCharactersRef,
101    pub text_start: InputActionRef,
102    pub text_end: InputActionRef,
103    pub text_delete_left: InputActionRef,
104    pub text_delete_right: InputActionRef,
105    pub pointer_position: ArrayInputCombinator<2>,
106    pub pointer_trigger: InputActionRef,
107    pub pointer_context: InputActionRef,
108    pub scroll: ArrayInputCombinator<2>,
109}
110
111#[derive(Default)]
112pub struct GuiInteractionsEngine {
113    pub inputs: GuiInteractionsInputs,
114    pub engine: DefaultInteractionsEngine,
115    cached_pointer_position: Vec2,
116}
117
118impl GuiInteractionsEngine {
119    pub fn maintain(&mut self, mapping: &CoordsMapping) {
120        if self.engine.focused_text_input().is_some() {
121            if let Some(mut text) = self.inputs.text.write() {
122                for character in text.take().chars() {
123                    self.engine
124                        .interact(Interaction::Navigate(NavSignal::TextChange(
125                            NavTextChange::InsertCharacter(character),
126                        )));
127                }
128            }
129            match self.inputs.direction.get() {
130                GuiCardinalDirection::Left => {
131                    self.engine
132                        .interact(Interaction::Navigate(NavSignal::TextChange(
133                            NavTextChange::MoveCursorLeft,
134                        )));
135                }
136                GuiCardinalDirection::Right => {
137                    self.engine
138                        .interact(Interaction::Navigate(NavSignal::TextChange(
139                            NavTextChange::MoveCursorRight,
140                        )));
141                }
142                _ => {}
143            }
144            if self.inputs.text_start.get().is_pressed() {
145                self.engine
146                    .interact(Interaction::Navigate(NavSignal::TextChange(
147                        NavTextChange::MoveCursorStart,
148                    )));
149            }
150            if self.inputs.text_end.get().is_pressed() {
151                self.engine
152                    .interact(Interaction::Navigate(NavSignal::TextChange(
153                        NavTextChange::MoveCursorEnd,
154                    )));
155            }
156            if self.inputs.text_delete_left.get().is_pressed() {
157                self.engine
158                    .interact(Interaction::Navigate(NavSignal::TextChange(
159                        NavTextChange::DeleteLeft,
160                    )));
161            }
162            if self.inputs.text_delete_right.get().is_pressed() {
163                self.engine
164                    .interact(Interaction::Navigate(NavSignal::TextChange(
165                        NavTextChange::DeleteRight,
166                    )));
167            }
168            if self.inputs.trigger.get().is_pressed() {
169                self.engine
170                    .interact(Interaction::Navigate(NavSignal::TextChange(
171                        NavTextChange::NewLine,
172                    )));
173            }
174        } else {
175            match self.inputs.direction.get() {
176                GuiCardinalDirection::Up => {
177                    self.engine.interact(Interaction::Navigate(NavSignal::Up));
178                }
179                GuiCardinalDirection::Down => {
180                    self.engine.interact(Interaction::Navigate(NavSignal::Down));
181                }
182                GuiCardinalDirection::Left => {
183                    self.engine.interact(Interaction::Navigate(NavSignal::Left));
184                }
185                GuiCardinalDirection::Right => {
186                    self.engine
187                        .interact(Interaction::Navigate(NavSignal::Right));
188                }
189                _ => {}
190            }
191            if self.inputs.prev.get().is_pressed() {
192                self.engine.interact(Interaction::Navigate(NavSignal::Prev));
193            }
194            if self.inputs.next.get().is_pressed() {
195                self.engine.interact(Interaction::Navigate(NavSignal::Next));
196            }
197            let trigger = self.inputs.trigger.get();
198            if trigger.is_pressed() {
199                self.engine
200                    .interact(Interaction::Navigate(NavSignal::Accept(true)));
201            }
202            if trigger.is_released() {
203                self.engine
204                    .interact(Interaction::Navigate(NavSignal::Accept(false)));
205            }
206            let context = self.inputs.context.get();
207            if context.is_pressed() {
208                self.engine
209                    .interact(Interaction::Navigate(NavSignal::Context(true)));
210            }
211            if context.is_released() {
212                self.engine
213                    .interact(Interaction::Navigate(NavSignal::Context(false)));
214            }
215            let cancel = self.inputs.cancel.get();
216            if cancel.is_pressed() {
217                self.engine
218                    .interact(Interaction::Navigate(NavSignal::Cancel(true)));
219            }
220            if cancel.is_released() {
221                self.engine
222                    .interact(Interaction::Navigate(NavSignal::Cancel(false)));
223            }
224        }
225        let pointer_position = {
226            let [x, y] = self.inputs.pointer_position.get();
227            let position = mapping.real_to_virtual_vec2(Vec2 { x, y }, false);
228            if (position.x - self.cached_pointer_position.x).abs() > ZERO_THRESHOLD
229                || (position.y - self.cached_pointer_position.y).abs() > ZERO_THRESHOLD
230            {
231                self.cached_pointer_position = position;
232                self.engine.interact(Interaction::PointerMove(position));
233            }
234            position
235        };
236        let pointer_trigger = self.inputs.pointer_trigger.get();
237        let pointer_context = self.inputs.pointer_context.get();
238        if pointer_trigger.is_pressed() {
239            self.engine.interact(Interaction::PointerDown(
240                PointerButton::Trigger,
241                pointer_position,
242            ));
243        }
244        if pointer_trigger.is_released() {
245            self.engine.interact(Interaction::PointerUp(
246                PointerButton::Trigger,
247                pointer_position,
248            ));
249        }
250        if pointer_context.is_pressed() {
251            self.engine.interact(Interaction::PointerDown(
252                PointerButton::Context,
253                pointer_position,
254            ));
255        }
256        if pointer_context.is_released() {
257            self.engine.interact(Interaction::PointerUp(
258                PointerButton::Context,
259                pointer_position,
260            ));
261        }
262        {
263            let [x, y] = self.inputs.scroll.get();
264            if x.abs() > ZERO_THRESHOLD || y.abs() > ZERO_THRESHOLD {
265                self.engine
266                    .interact(Interaction::Navigate(NavSignal::Jump(NavJump::Scroll(
267                        NavScroll::Units(Vec2 { x: -x, y: -y }, true),
268                    ))));
269            }
270        }
271    }
272}
273
274impl InteractionsEngine<DefaultInteractionsEngineResult, ()> for GuiInteractionsEngine {
275    fn perform_interactions(
276        &mut self,
277        app: &mut Application,
278    ) -> Result<DefaultInteractionsEngineResult, ()> {
279        self.engine.perform_interactions(app)
280    }
281}