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}
75
76pub fn handle_touch_raw(
77    rt: &mut ReposeRuntime,
78    touch_gestures: &mut TouchGestureState,
79    t: &Touch,
80    scale: f32,
81) -> TouchResult {
82    let pos_px = (t.location.x as f32, t.location.y as f32);
83    let tid = t.id;
84    match t.phase {
85        winit::event::TouchPhase::Started => {
86            touch_gestures.touch_started(rt, tid, pos_px);
87            TouchResult {
88                dirty: true,
89                pinch: None,
90                pan: None,
91                rotation: None,
92                swipe_right: None,
93            }
94        }
95        winit::event::TouchPhase::Moved => {
96            let (dirty, pinch, pan, rotation) = touch_gestures.touch_moved(rt, tid, pos_px, scale);
97            TouchResult {
98                dirty,
99                pinch,
100                pan,
101                rotation,
102                swipe_right: None,
103            }
104        }
105        winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
106            let cancelled = t.phase == winit::event::TouchPhase::Cancelled;
107            let swipe_right = touch_gestures.touch_ended(rt, tid, pos_px, cancelled);
108            TouchResult {
109                dirty: false,
110                pinch: None,
111                pan: None,
112                rotation: None,
113                swipe_right,
114            }
115        }
116    }
117}
118
119/// HACK: Legacy wrapper that dispatches gestures inline (use `handle_touch_raw` when caller
120/// needs to avoid double-borrow of `self` containing `rt`).
121pub fn on_touch(
122    rt: &mut ReposeRuntime,
123    touch_gestures: &mut TouchGestureState,
124    t: &Touch,
125    scale: f32,
126    mut dispatch: impl FnMut(Action) -> bool,
127) -> bool {
128    let r = handle_touch_raw(rt, touch_gestures, t, scale);
129    let mut dirty = r.dirty;
130    if let Some((delta_scale, center)) = r.pinch
131        && dispatch(Action::Gesture(Gesture::PinchWithCenter {
132            delta_scale,
133            center,
134        }))
135    {
136        dirty = true;
137    }
138    if let Some((delta, center)) = r.pan
139        && dispatch(Action::Gesture(Gesture::Pan { delta, center }))
140    {
141        dirty = true;
142    }
143    if let Some((delta_rotation, center)) = r.rotation
144        && dispatch(Action::Gesture(Gesture::Rotate {
145            delta_rotation,
146            center,
147        }))
148    {
149        dirty = true;
150    }
151    if let Some(right) = r.swipe_right {
152        let g = if right {
153            Gesture::SwipeRight
154        } else {
155            Gesture::SwipeLeft
156        };
157        if dispatch(Action::Gesture(g)) {
158            dirty = true;
159        }
160    }
161    dirty
162}
163
164/// Touch handler that also syncs IME for focused textfields (web/android).
165/// Returns whether a redraw is needed. Probably shared for desktop once winit unifies touch.
166pub fn on_touch_with_ime(
167    rt: &mut ReposeRuntime,
168    touch_gestures: &mut TouchGestureState,
169    t: &Touch,
170    scale: f32,
171    window: &winit::window::Window,
172    dispatch: impl FnMut(Action) -> bool,
173) -> bool {
174    let pos_px = (t.location.x as f32, t.location.y as f32);
175    let tid = t.id;
176    if t.phase == winit::event::TouchPhase::Started {
177        let focused = touch_gestures.touch_started(rt, tid, pos_px);
178        if let Some(fid) = focused {
179            if rt.is_textfield(fid) {
180                let (purpose, ac, cap) = rt.focused_keyboard_hints();
181                crate::common::set_ime_for_textfield_ex(window, true, purpose, ac, cap);
182            } else {
183                crate::common::set_ime_for_textfield(window, false);
184            }
185        } else {
186            crate::common::set_ime_for_textfield(window, false);
187        }
188        return true;
189    }
190    on_touch(rt, touch_gestures, t, scale, dispatch)
191}
192
193/// Shared inspector toggle + runtime dispatch.
194/// Returns true if event consumed / needs redraw.
195pub fn on_keyboard_input(
196    rt: &mut ReposeRuntime,
197    key_event: &winit::event::KeyEvent,
198    inspector: &mut Option<repose_devtools::Inspector>,
199) -> bool {
200    if key_event.state == ElementState::Pressed
201        && !key_event.repeat
202        && rt.modifiers.ctrl
203        && rt.modifiers.shift
204        && key_event.physical_key == PhysicalKey::Code(KeyCode::KeyI)
205        && let Some(inspector) = inspector
206    {
207        inspector.hud.toggle_inspector();
208        return true;
209    }
210    let mapped = map_key(key_event.physical_key, &rt.modifiers);
211    let ke = winit_key_to_repose(key_event, &mapped, &rt.modifiers);
212    rt.handle_key_with_text(&ke, key_event.text.as_deref())
213}
214
215/// Ime dispatch helper.
216pub fn on_ime(rt: &mut ReposeRuntime, ime: &winit::event::Ime) {
217    use winit::event::Ime;
218    let ev = match ime {
219        Ime::Enabled => repose_core::input::ImeEvent::Start,
220        Ime::Preedit(text, cursor) => repose_core::input::ImeEvent::Update {
221            text: text.clone(),
222            cursor: cursor.map(|(a, b)| (a, b)),
223        },
224        Ime::Commit(text) => repose_core::input::ImeEvent::Commit(text.clone()),
225        Ime::Disabled => repose_core::input::ImeEvent::Cancel,
226    };
227    rt.handle_ime(&ev);
228}