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            let hit = f.hit_regions.iter().find(|h| h.rect.contains(pos));
27            let hover_rect = hit.map(|h| h.rect);
28            let hover_info = hit.and_then(|h| {
29                f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
30                    repose_devtools::HoveredInfo {
31                        id: s.id,
32                        role: format!("{:?}", s.role),
33                        label: s.label.clone(),
34                    }
35                })
36            });
37            inspector.hud.set_hovered(hover_rect, hover_info);
38        }
39    result.cursor
40}
41
42/// MouseWheel helper - converts delta to px and dispatches.
43pub fn on_mouse_wheel(rt: &mut ReposeRuntime, delta: MouseScrollDelta, scale: f32) -> bool {
44    let (dx_px, dy_px) = match delta {
45        MouseScrollDelta::LineDelta(x, y) => {
46            let _ = scale;
47            let unit_px = repose_core::Dp(60.0).to_px().0;
48            (-(x * unit_px), -(y * unit_px))
49        }
50        MouseScrollDelta::PixelDelta(p) => (-(p.x as f32), -(p.y as f32)),
51    };
52    rt.handle_scroll(Vec2 { x: dx_px, y: dy_px })
53}
54
55/// Map winit MouseButton -> PointerButton.
56pub fn map_mouse_button(btn: winit::event::MouseButton) -> Option<PointerButton> {
57    match btn {
58        winit::event::MouseButton::Left => Some(PointerButton::Primary),
59        winit::event::MouseButton::Right => Some(PointerButton::Secondary),
60        winit::event::MouseButton::Middle => Some(PointerButton::Tertiary),
61        _ => None,
62    }
63}
64
65/// Raw touch result without dispatch - caller handles `dispatch_action` to avoid double-borrow.
66pub struct TouchResult {
67    pub dirty: bool,
68    pub pinch: Option<(f32, Vec2)>,
69    pub pan: Option<Vec2>,
70    pub rotation: Option<(f32, Vec2)>,
71    pub swipe_right: Option<bool>,
72}
73
74pub fn handle_touch_raw(
75    rt: &mut ReposeRuntime,
76    touch_gestures: &mut TouchGestureState,
77    t: &Touch,
78    scale: f32,
79) -> TouchResult {
80    let pos_px = (t.location.x as f32, t.location.y as f32);
81    let tid = t.id;
82    match t.phase {
83        winit::event::TouchPhase::Started => {
84            touch_gestures.touch_started(rt, tid, pos_px);
85            TouchResult {
86                dirty: true,
87                pinch: None,
88                pan: None,
89                rotation: None,
90                swipe_right: None,
91            }
92        }
93        winit::event::TouchPhase::Moved => {
94            let (dirty, pinch, pan, rotation) = touch_gestures.touch_moved(rt, tid, pos_px, scale);
95            TouchResult {
96                dirty,
97                pinch,
98                pan,
99                rotation,
100                swipe_right: None,
101            }
102        }
103        winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
104            let cancelled = t.phase == winit::event::TouchPhase::Cancelled;
105            let swipe_right = touch_gestures.touch_ended(rt, tid, pos_px, cancelled);
106            TouchResult {
107                dirty: false,
108                pinch: None,
109                pan: None,
110                rotation: None,
111                swipe_right,
112            }
113        }
114    }
115}
116
117/// HACK: Legacy wrapper that dispatches gestures inline (use `handle_touch_raw` when caller
118/// needs to avoid double-borrow of `self` containing `rt`).
119pub fn on_touch(
120    rt: &mut ReposeRuntime,
121    touch_gestures: &mut TouchGestureState,
122    t: &Touch,
123    scale: f32,
124    mut dispatch: impl FnMut(Action) -> bool,
125) -> bool {
126    let r = handle_touch_raw(rt, touch_gestures, t, scale);
127    let mut dirty = r.dirty;
128    if let Some((delta_scale, center)) = r.pinch
129        && dispatch(Action::Gesture(Gesture::PinchWithCenter {
130            delta_scale,
131            center,
132        })) {
133            dirty = true;
134        }
135    if let Some(delta) = r.pan
136        && dispatch(Action::Gesture(Gesture::Pan { delta })) {
137            dirty = true;
138        }
139    if let Some((delta_rotation, center)) = r.rotation
140        && dispatch(Action::Gesture(Gesture::Rotate {
141            delta_rotation,
142            center,
143        })) {
144            dirty = true;
145        }
146    if let Some(right) = r.swipe_right {
147        let g = if right {
148            Gesture::SwipeRight
149        } else {
150            Gesture::SwipeLeft
151        };
152        if dispatch(Action::Gesture(g)) {
153            dirty = true;
154        }
155    }
156    dirty
157}
158
159/// Touch handler that also syncs IME for focused textfields (web/android).
160/// Returns whether a redraw is needed. Probably shared for desktop once winit unifies touch.
161pub fn on_touch_with_ime(
162    rt: &mut ReposeRuntime,
163    touch_gestures: &mut TouchGestureState,
164    t: &Touch,
165    scale: f32,
166    window: &winit::window::Window,
167    dispatch: impl FnMut(Action) -> bool,
168) -> bool {
169    let pos_px = (t.location.x as f32, t.location.y as f32);
170    let tid = t.id;
171    if t.phase == winit::event::TouchPhase::Started {
172        let focused = touch_gestures.touch_started(rt, tid, pos_px);
173        if let Some(fid) = focused {
174            if rt.is_textfield(fid) {
175                let (purpose, ac, cap) = rt.focused_keyboard_hints();
176                crate::common::set_ime_for_textfield_ex(window, true, purpose, ac, cap);
177            } else {
178                crate::common::set_ime_for_textfield(window, false);
179            }
180        } else {
181            crate::common::set_ime_for_textfield(window, false);
182        }
183        return true;
184    }
185    on_touch(rt, touch_gestures, t, scale, dispatch)
186}
187
188/// Shared inspector toggle + runtime dispatch.
189/// Returns true if event consumed / needs redraw.
190pub fn on_keyboard_input(
191    rt: &mut ReposeRuntime,
192    key_event: &winit::event::KeyEvent,
193    inspector: &mut Option<repose_devtools::Inspector>,
194) -> bool {
195    if key_event.state == ElementState::Pressed
196        && !key_event.repeat
197        && rt.modifiers.ctrl
198        && rt.modifiers.shift
199        && key_event.physical_key == PhysicalKey::Code(KeyCode::KeyI)
200        && let Some(inspector) = inspector {
201            inspector.hud.toggle_inspector();
202            return true;
203        }
204    let mapped = map_key(key_event.physical_key, &rt.modifiers);
205    let ke = winit_key_to_repose(key_event, &mapped, &rt.modifiers);
206    rt.handle_key_with_text(&ke, key_event.text.as_deref())
207}
208
209/// Ime dispatch helper.
210pub fn on_ime(rt: &mut ReposeRuntime, ime: &winit::event::Ime) {
211    use winit::event::Ime;
212    let ev = match ime {
213        Ime::Enabled => repose_core::input::ImeEvent::Start,
214        Ime::Preedit(text, cursor) => repose_core::input::ImeEvent::Update {
215            text: text.clone(),
216            cursor: cursor.map(|(a, b)| (a, b)),
217        },
218        Ime::Commit(text) => repose_core::input::ImeEvent::Commit(text.clone()),
219        Ime::Disabled => repose_core::input::ImeEvent::Cancel,
220    };
221    rt.handle_ime(&ev);
222}