Skip to main content

repose_platform/
runner_common.rs

1//! Shared runner helpers extracted from duplicated desktop/web/android `App` impls.
2
3use repose_app::{ReposeRuntime, TouchGestureState};
4use repose_core::Vec2;
5use repose_core::input::PointerButton;
6use repose_core::shortcuts::{Action, Gesture};
7use winit::event::{ElementState, MouseScrollDelta, Touch};
8use winit::keyboard::{KeyCode, PhysicalKey};
9
10use crate::common::{map_key, winit_key_to_repose};
11
12/// Update `Modifiers` from winit state - shared.
13pub fn on_modifiers_changed(rt: &mut ReposeRuntime, state: &winit::keyboard::ModifiersState) {
14    crate::common::update_modifiers(&mut rt.modifiers, state);
15}
16
17/// CursorMoved helper - returns cursor + whether inspector hover updated.
18pub fn on_cursor_moved(
19    rt: &mut ReposeRuntime,
20    pos: Vec2,
21    inspector: &mut Option<repose_devtools::Inspector>,
22) -> Option<repose_core::CursorIcon> {
23    let result = rt.handle_pointer_move(pos);
24    if let (Some(inspector), Some(f)) = (inspector, &rt.frame_cache)
25        && inspector.hud.inspector_enabled
26    {
27        let hit = f.hit_regions.iter().find(|h| h.rect.contains(pos));
28        let hover_rect = hit.map(|h| h.rect);
29        let hover_info = hit.and_then(|h| {
30            f.semantics_nodes
31                .iter()
32                .find(|s| s.id == h.id)
33                .map(|s| repose_devtools::HoveredInfo {
34                    id: s.id,
35                    role: format!("{:?}", s.role),
36                    label: s.label.clone(),
37                })
38        });
39        inspector.hud.set_hovered(hover_rect, hover_info);
40    }
41    result.cursor
42}
43
44/// MouseWheel helper - converts delta to px and dispatches.
45pub fn on_mouse_wheel(rt: &mut ReposeRuntime, delta: MouseScrollDelta, scale: f32) -> bool {
46    let (dx_px, dy_px) = match delta {
47        MouseScrollDelta::LineDelta(x, y) => {
48            let _ = scale;
49            let unit_px = repose_core::Dp(60.0).to_px().0;
50            (-(x * unit_px), -(y * unit_px))
51        }
52        MouseScrollDelta::PixelDelta(p) => (-(p.x as f32), -(p.y as f32)),
53    };
54    rt.handle_scroll(Vec2 { x: dx_px, y: dy_px })
55}
56
57/// Map winit MouseButton -> PointerButton.
58pub fn map_mouse_button(btn: winit::event::MouseButton) -> Option<PointerButton> {
59    match btn {
60        winit::event::MouseButton::Left => Some(PointerButton::Primary),
61        winit::event::MouseButton::Right => Some(PointerButton::Secondary),
62        winit::event::MouseButton::Middle => Some(PointerButton::Tertiary),
63        _ => None,
64    }
65}
66
67/// Raw touch result without dispatch - caller handles `dispatch_action` to avoid double-borrow.
68pub struct TouchResult {
69    pub dirty: bool,
70    pub pinch: Option<(f32, Vec2)>,
71    pub pan: Option<(Vec2, Vec2)>,
72    pub rotation: Option<(f32, Vec2)>,
73    pub swipe_right: Option<bool>,
74    /// Press result of a tap dispatched on `Ended`. `None` when no press ran
75    /// (scroll/pinch release); `Some` carries the press's focused id, with
76    /// `None` inside meaning the tap explicitly defocused.
77    pub press: Option<Option<u64>>,
78}
79
80pub fn handle_touch_raw(
81    rt: &mut ReposeRuntime,
82    touch_gestures: &mut TouchGestureState,
83    t: &Touch,
84    scale: f32,
85) -> TouchResult {
86    let pos_px = (t.location.x as f32, t.location.y as f32);
87    let tid = t.id;
88    match t.phase {
89        winit::event::TouchPhase::Started => {
90            // Sync before the gesture layer: `touch_started` reads
91            // the table for multi-finger detection (`len() >= 2`
92            // cancels the pending primary instead of dispatching a
93            // press), so the second finger must already be present.
94            touch_gestures.contact_down(tid, pos_px);
95            touch_gestures.touch_started(rt, tid, pos_px);
96            sync_touch_points(rt, touch_gestures);
97            TouchResult {
98                dirty: true,
99                pinch: None,
100                pan: None,
101                rotation: None,
102                swipe_right: None,
103                press: None,
104            }
105        }
106        winit::event::TouchPhase::Moved => {
107            touch_gestures.contact_down(tid, pos_px);
108            let (dirty, pinch, pan, rotation) = touch_gestures.touch_moved(rt, tid, pos_px, scale);
109            sync_touch_points(rt, touch_gestures);
110            TouchResult {
111                dirty,
112                pinch,
113                pan,
114                rotation,
115                swipe_right: None,
116                press: None,
117            }
118        }
119        winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
120            let cancelled = t.phase == winit::event::TouchPhase::Cancelled;
121            // Sync before the gesture layer: `touch_ended` derives
122            // tap-vs-multi from the table (`was_multi`), so the lifted
123            // finger must still be present.
124            let ended = touch_gestures.touch_ended(rt, tid, pos_px, cancelled);
125            touch_gestures.contact_up(tid);
126            sync_touch_points(rt, touch_gestures);
127            TouchResult {
128                dirty: false,
129                pinch: None,
130                pan: None,
131                rotation: None,
132                swipe_right: ended.swipe_right,
133                press: ended.press,
134            }
135        }
136    }
137}
138
139/// Publish the live contact table into `sched.touch_points` (single
140/// sync point: every runner funnels raw touch through
141/// `handle_touch_raw`). Games snapshot it each frame, so held fingers
142/// stay visible across frames (unlike the press edge, which fires
143/// once and is gone). GML `device_mouse_*` parity: stable per-finger
144/// ids, physical-px positions sampled every tick while held.
145pub fn sync_touch_points(rt: &mut ReposeRuntime, touch_gestures: &TouchGestureState) {
146    rt.sched.touch_points.clear();
147    rt.sched.touch_points.extend(
148        touch_gestures
149            .active_touches()
150            .iter()
151            .map(|(id, (x, y))| (*id, *x, *y)),
152    );
153}
154
155/// HACK: Legacy wrapper that dispatches gestures inline (use `handle_touch_raw` when caller
156/// needs to avoid double-borrow of `self` containing `rt`).
157pub fn on_touch(
158    rt: &mut ReposeRuntime,
159    touch_gestures: &mut TouchGestureState,
160    t: &Touch,
161    scale: f32,
162    mut dispatch: impl FnMut(Action) -> bool,
163) -> bool {
164    let r = handle_touch_raw(rt, touch_gestures, t, scale);
165    let mut dirty = r.dirty;
166    if let Some((delta_scale, center)) = r.pinch
167        && dispatch(Action::Gesture(Gesture::PinchWithCenter {
168            delta_scale,
169            center,
170        }))
171    {
172        dirty = true;
173    }
174    if let Some((delta, center)) = r.pan
175        && dispatch(Action::Gesture(Gesture::Pan { delta, center }))
176    {
177        dirty = true;
178    }
179    if let Some((delta_rotation, center)) = r.rotation
180        && dispatch(Action::Gesture(Gesture::Rotate {
181            delta_rotation,
182            center,
183        }))
184    {
185        dirty = true;
186    }
187    if let Some(right) = r.swipe_right {
188        let g = if right {
189            Gesture::SwipeRight
190        } else {
191            Gesture::SwipeLeft
192        };
193        if dispatch(Action::Gesture(g)) {
194            dirty = true;
195        }
196    }
197    dirty
198}
199
200/// Touch handler that also syncs IME for focused textfields on native runners.
201/// The web backend has no editor bridge, so its touch path only dispatches input.
202pub fn on_touch_with_ime(
203    rt: &mut ReposeRuntime,
204    touch_gestures: &mut TouchGestureState,
205    t: &Touch,
206    scale: f32,
207    _window: &winit::window::Window,
208    dispatch: impl FnMut(Action) -> bool,
209) -> bool {
210    let pos_px = (t.location.x as f32, t.location.y as f32);
211    let tid = t.id;
212    if t.phase == winit::event::TouchPhase::Started {
213        touch_gestures.contact_down(tid, pos_px);
214        let focused = touch_gestures.touch_started(rt, tid, pos_px);
215        sync_touch_points(rt, touch_gestures);
216        #[cfg(not(target_arch = "wasm32"))]
217        if let Some(fid) = focused {
218            let editable = rt
219                .frame_cache
220                .as_ref()
221                .is_some_and(|frame| crate::common::is_editable_textfield_hit(frame, fid));
222            if rt.sched.window_focused && editable {
223                let (purpose, ac, cap) = rt.focused_keyboard_hints();
224                crate::common::set_ime_for_textfield_ex(_window, true, purpose, ac, cap);
225            } else {
226                crate::common::set_ime_for_textfield(_window, false);
227            }
228        } else {
229            crate::common::set_ime_for_textfield(_window, false);
230        }
231        #[cfg(target_arch = "wasm32")]
232        let _ = focused;
233        return true;
234    }
235    on_touch(rt, touch_gestures, t, scale, dispatch)
236}
237
238/// Shared inspector toggle + runtime dispatch.
239/// Reports the raw physical transition to the polled `held_keys` set
240/// first (games read held keys GML-style without depending on focus),
241/// then dispatches the focus-routed key event as before.
242/// Returns true if event consumed / needs redraw.
243pub fn on_keyboard_input(
244    rt: &mut ReposeRuntime,
245    key_event: &winit::event::KeyEvent,
246    inspector: &mut Option<repose_devtools::Inspector>,
247) -> bool {
248    rt.set_physical_key(
249        &physical_key_name(key_event.physical_key),
250        key_event.state == ElementState::Pressed,
251    );
252    // Mirror into the scheduler's polled snapshot too: games read
253    // `sched.held_keys` + mouse levels directly (no runtime handle),
254    // so the platform publishes hardware levels where composition can
255    // see them. Focus dispatch below is unchanged.
256    {
257        let key = map_physical_key(key_event.physical_key);
258        let sched = &mut rt.sched;
259        if key_event.state == ElementState::Pressed {
260            sched.held_keys.insert(key);
261        } else {
262            sched.held_keys.remove(&key);
263        }
264    }
265    if key_event.state == ElementState::Pressed
266        && !key_event.repeat
267        && rt.modifiers.ctrl
268        && rt.modifiers.shift
269        && key_event.physical_key == PhysicalKey::Code(KeyCode::KeyI)
270        && let Some(inspector) = inspector
271    {
272        inspector.hud.toggle_inspector();
273        return true;
274    }
275    let mapped = if crate::common::is_back_key(key_event) {
276        repose_core::input::Key::Escape
277    } else {
278        map_key(key_event.physical_key, &rt.modifiers)
279    };
280    let ke = winit_key_to_repose(key_event, &mapped, &rt.modifiers);
281    rt.handle_key_with_text(&ke, key_event.text.as_deref())
282}
283
284/// Stable debug name for a physical key (`KeyW`, `Digit1`, `Space`,
285/// ...), matching `KeyCode`'s variant names. Layout-independent:
286/// AZERTY/QWERTZ report the same physical names, so games bind by
287/// position the way GML's `ord("W")` binds the US position.
288pub fn physical_key_name(key: PhysicalKey) -> String {
289    match key {
290        PhysicalKey::Code(code) => format!("{code:?}"),
291        PhysicalKey::Unidentified(native) => format!("Unidentified({native:?})"),
292    }
293}
294
295/// Typed physical position for a winit key (`None` wraps to
296/// [`repose_core::input::PhysicalKey::Unidentified`], never `None`:
297/// every physical press has a position). Backs both the `KeyEvent`
298/// field and the `Scheduler::held_keys` snapshot.
299pub fn map_physical_key(key: PhysicalKey) -> repose_core::input::PhysicalKey {
300    repose_core::input::PhysicalKey::from_name(&physical_key_name(key))
301}
302
303/// Ime dispatch helper.
304pub fn on_ime(rt: &mut ReposeRuntime, ime: &winit::event::Ime) {
305    use winit::event::Ime;
306    let ev = match ime {
307        Ime::Enabled => repose_core::input::ImeEvent::Start,
308        Ime::Preedit(text, cursor) => repose_core::input::ImeEvent::Update {
309            text: text.clone(),
310            cursor: cursor.map(|(a, b)| (a, b)),
311        },
312        Ime::Commit(text) => repose_core::input::ImeEvent::Commit(text.clone()),
313        Ime::Disabled => repose_core::input::ImeEvent::Cancel,
314    };
315    rt.handle_ime(&ev);
316}