repose-platform 0.31.24

Platform runners (winit Desktop, Android and Web)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
//! Shared runner helpers extracted from duplicated desktop/web/android `App` impls.

use web_time::Duration;

use repose_app::{ReposeRuntime, TouchGestureState};
use repose_core::Vec2;
use repose_core::input::PointerButton;
use repose_core::shortcuts::{Action, Gesture};
use winit::event::{ElementState, MouseScrollDelta, Touch};
use winit::keyboard::{KeyCode, PhysicalKey};

use crate::common::{map_key, winit_key_to_repose};

/// Adapter creation is retried a few times because a platform may still be
/// bringing up its GPU process. A capability problem (no supported backend,
/// blocklisted driver) never resolves, so stop after this many tries.
pub const MAX_BACKEND_INIT_ATTEMPTS: u32 = 3;

const BASE_BACKEND_RETRY_DELAY: Duration = Duration::from_millis(250);

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BackendState {
    Pending,
    Ready,
    Failed,
    /// Nothing left to try: the canvas/window stays blank until reload.
    Unavailable,
}

pub enum BackendRetry {
    Ready,
    RetryAfter(Duration),
    Unavailable,
}

/// Retry policy for GPU backend construction, shared by every runner so the
/// attempt cap and backoff curve stay identical across platforms. Callers keep
/// their own clock and absolute retry deadline, since `Instant` differs on
/// wasm.
#[derive(Default)]
pub struct BackendLifecycle {
    state: Option<BackendState>,
    attempts: u32,
}

impl BackendLifecycle {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn state(&self) -> BackendState {
        self.state.unwrap_or(BackendState::Pending)
    }

    /// Mark an in-flight construction so a stale result cannot look ready.
    pub fn begin(&mut self) {
        self.state = Some(BackendState::Pending);
    }

    pub fn ready(&mut self) {
        self.state = Some(BackendState::Ready);
        self.attempts = 0;
    }

    /// Record a failure and say what to do next.
    pub fn failed(&mut self) -> BackendRetry {
        self.attempts += 1;
        if self.attempts >= MAX_BACKEND_INIT_ATTEMPTS {
            self.state = Some(BackendState::Unavailable);
            BackendRetry::Unavailable
        } else {
            self.state = Some(BackendState::Failed);
            BackendRetry::RetryAfter(BASE_BACKEND_RETRY_DELAY * (1 << (self.attempts - 1).min(4)))
        }
    }
}

/// Update `Modifiers` from winit state - shared.
pub fn on_modifiers_changed(rt: &mut ReposeRuntime, state: &winit::keyboard::ModifiersState) {
    crate::common::update_modifiers(&mut rt.modifiers, state);
}

/// CursorMoved helper - returns cursor + whether inspector hover updated.
pub fn on_cursor_moved(
    rt: &mut ReposeRuntime,
    pos: Vec2,
    inspector: &mut Option<repose_devtools::Inspector>,
) -> Option<repose_core::CursorIcon> {
    let result = rt.handle_pointer_move(pos);
    if let (Some(inspector), Some(f)) = (inspector, &rt.frame_cache)
        && inspector.hud.inspector_enabled
    {
        let hit = f.hit_regions.iter().find(|h| h.rect.contains(pos));
        let hover_rect = hit.map(|h| h.rect);
        let hover_info = hit.and_then(|h| {
            f.semantics_nodes
                .iter()
                .find(|s| s.id == h.id)
                .map(|s| repose_devtools::HoveredInfo {
                    id: s.id,
                    role: format!("{:?}", s.role),
                    label: s.label.clone(),
                })
        });
        inspector.hud.set_hovered(hover_rect, hover_info);
    }
    result.cursor
}

/// MouseWheel helper - converts delta to px and dispatches.
pub fn on_mouse_wheel(rt: &mut ReposeRuntime, delta: MouseScrollDelta, scale: f32) -> bool {
    let (dx_px, dy_px) = match delta {
        MouseScrollDelta::LineDelta(x, y) => {
            let _ = scale;
            let unit_px = repose_core::Dp(60.0).to_px().0;
            (-(x * unit_px), -(y * unit_px))
        }
        MouseScrollDelta::PixelDelta(p) => (-(p.x as f32), -(p.y as f32)),
    };
    rt.handle_scroll(Vec2 { x: dx_px, y: dy_px })
}

/// Map winit MouseButton -> PointerButton.
pub fn map_mouse_button(btn: winit::event::MouseButton) -> Option<PointerButton> {
    match btn {
        winit::event::MouseButton::Left => Some(PointerButton::Primary),
        winit::event::MouseButton::Right => Some(PointerButton::Secondary),
        winit::event::MouseButton::Middle => Some(PointerButton::Tertiary),
        _ => None,
    }
}

/// Raw touch result without dispatch - caller handles `dispatch_action` to avoid double-borrow.
pub struct TouchResult {
    pub dirty: bool,
    pub pinch: Option<(f32, Vec2)>,
    pub pan: Option<(Vec2, Vec2)>,
    pub rotation: Option<(f32, Vec2)>,
    pub swipe_right: Option<bool>,
    /// Press result of a tap dispatched on `Ended`. `None` when no press ran
    /// (scroll/pinch release); `Some` carries the press's focused id, with
    /// `None` inside meaning the tap explicitly defocused.
    pub press: Option<Option<u64>>,
}

pub fn handle_touch_raw(
    rt: &mut ReposeRuntime,
    touch_gestures: &mut TouchGestureState,
    t: &Touch,
    scale: f32,
) -> TouchResult {
    let pos_px = (t.location.x as f32, t.location.y as f32);
    let tid = t.id;
    match t.phase {
        winit::event::TouchPhase::Started => {
            // Sync before the gesture layer: `touch_started` reads
            // the table for multi-finger detection (`len() >= 2`
            // cancels the pending primary instead of dispatching a
            // press), so the second finger must already be present.
            touch_gestures.contact_down(tid, pos_px);
            touch_gestures.touch_started(rt, tid, pos_px);
            sync_touch_points(rt, touch_gestures);
            TouchResult {
                dirty: true,
                pinch: None,
                pan: None,
                rotation: None,
                swipe_right: None,
                press: None,
            }
        }
        winit::event::TouchPhase::Moved => {
            touch_gestures.contact_down(tid, pos_px);
            let (dirty, pinch, pan, rotation) = touch_gestures.touch_moved(rt, tid, pos_px, scale);
            sync_touch_points(rt, touch_gestures);
            TouchResult {
                dirty,
                pinch,
                pan,
                rotation,
                swipe_right: None,
                press: None,
            }
        }
        winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
            let cancelled = t.phase == winit::event::TouchPhase::Cancelled;
            // Sync before the gesture layer: `touch_ended` derives
            // tap-vs-multi from the table (`was_multi`), so the lifted
            // finger must still be present.
            let ended = touch_gestures.touch_ended(rt, tid, pos_px, cancelled);
            touch_gestures.contact_up(tid);
            sync_touch_points(rt, touch_gestures);
            TouchResult {
                dirty: false,
                pinch: None,
                pan: None,
                rotation: None,
                swipe_right: ended.swipe_right,
                press: ended.press,
            }
        }
    }
}

/// Publish the live contact table into `sched.touch_points` (single
/// sync point: every runner funnels raw touch through
/// `handle_touch_raw`). Games snapshot it each frame, so held fingers
/// stay visible across frames (unlike the press edge, which fires
/// once and is gone). GML `device_mouse_*` parity: stable per-finger
/// ids, physical-px positions sampled every tick while held.
pub fn sync_touch_points(rt: &mut ReposeRuntime, touch_gestures: &TouchGestureState) {
    rt.sched.touch_points.clear();
    rt.sched.touch_points.extend(
        touch_gestures
            .active_touches()
            .iter()
            .map(|(id, (x, y))| (*id, *x, *y)),
    );
}

/// HACK: Legacy wrapper that dispatches gestures inline (use `handle_touch_raw` when caller
/// needs to avoid double-borrow of `self` containing `rt`).
pub fn on_touch(
    rt: &mut ReposeRuntime,
    touch_gestures: &mut TouchGestureState,
    t: &Touch,
    scale: f32,
    mut dispatch: impl FnMut(Action) -> bool,
) -> bool {
    let r = handle_touch_raw(rt, touch_gestures, t, scale);
    let mut dirty = r.dirty;
    if let Some((delta_scale, center)) = r.pinch
        && dispatch(Action::Gesture(Gesture::PinchWithCenter {
            delta_scale,
            center,
        }))
    {
        dirty = true;
    }
    if let Some((delta, center)) = r.pan
        && dispatch(Action::Gesture(Gesture::Pan { delta, center }))
    {
        dirty = true;
    }
    if let Some((delta_rotation, center)) = r.rotation
        && dispatch(Action::Gesture(Gesture::Rotate {
            delta_rotation,
            center,
        }))
    {
        dirty = true;
    }
    if let Some(right) = r.swipe_right {
        let g = if right {
            Gesture::SwipeRight
        } else {
            Gesture::SwipeLeft
        };
        if dispatch(Action::Gesture(g)) {
            dirty = true;
        }
    }
    dirty
}

/// Touch handler that also syncs IME for focused textfields on native runners.
/// The web backend has no editor bridge, so its touch path only dispatches input.
pub fn on_touch_with_ime(
    rt: &mut ReposeRuntime,
    touch_gestures: &mut TouchGestureState,
    t: &Touch,
    scale: f32,
    _window: &winit::window::Window,
    dispatch: impl FnMut(Action) -> bool,
) -> bool {
    let pos_px = (t.location.x as f32, t.location.y as f32);
    let tid = t.id;
    if t.phase == winit::event::TouchPhase::Started {
        touch_gestures.contact_down(tid, pos_px);
        let focused = touch_gestures.touch_started(rt, tid, pos_px);
        sync_touch_points(rt, touch_gestures);
        #[cfg(not(target_arch = "wasm32"))]
        if let Some(fid) = focused {
            let editable = rt
                .frame_cache
                .as_ref()
                .is_some_and(|frame| crate::common::is_editable_textfield_hit(frame, fid));
            if rt.sched.window_focused && editable {
                let (purpose, ac, cap) = rt.focused_keyboard_hints();
                crate::common::set_ime_for_textfield_ex(_window, true, purpose, ac, cap);
            } else {
                crate::common::set_ime_for_textfield(_window, false);
            }
        } else {
            crate::common::set_ime_for_textfield(_window, false);
        }
        #[cfg(target_arch = "wasm32")]
        let _ = focused;
        return true;
    }
    on_touch(rt, touch_gestures, t, scale, dispatch)
}

/// Shared inspector toggle + runtime dispatch.
/// Reports the raw physical transition to the polled `held_keys` set
/// first (games read held keys GML-style without depending on focus),
/// then dispatches the focus-routed key event as before.
/// Returns true if event consumed / needs redraw.
pub fn on_keyboard_input(
    rt: &mut ReposeRuntime,
    key_event: &winit::event::KeyEvent,
    inspector: &mut Option<repose_devtools::Inspector>,
) -> bool {
    on_key_parts(
        rt,
        key_event.physical_key,
        key_event.state == ElementState::Pressed,
        key_event.repeat,
        key_event.text.as_deref(),
        inspector,
    )
}

/// Same dispatch as [`on_keyboard_input`], for runners that cannot hand over a
/// [`winit::event::KeyEvent`] (it has no public constructor): synthetic keys
/// and keys forwarded from a DOM element.
pub fn on_key_parts(
    rt: &mut ReposeRuntime,
    physical_key: PhysicalKey,
    pressed: bool,
    repeat: bool,
    text: Option<&str>,
    inspector: &mut Option<repose_devtools::Inspector>,
) -> bool {
    let key_event = KeyParts {
        physical_key,
        pressed,
        repeat,
    };
    rt.set_physical_key(
        &physical_key_name(key_event.physical_key),
        key_event.pressed,
    );
    // Mirror into the scheduler's polled snapshot too: games read
    // `sched.held_keys` + mouse levels directly (no runtime handle),
    // so the platform publishes hardware levels where composition can
    // see them. Focus dispatch below is unchanged.
    {
        let key = map_physical_key(key_event.physical_key);
        let sched = &mut rt.sched;
        if key_event.pressed {
            sched.held_keys.insert(key);
        } else {
            sched.held_keys.remove(&key);
        }
    }
    if key_event.pressed
        && !key_event.repeat
        && rt.modifiers.ctrl
        && rt.modifiers.shift
        && key_event.physical_key == PhysicalKey::Code(KeyCode::KeyI)
        && let Some(inspector) = inspector
    {
        inspector.hud.toggle_inspector();
        return true;
    }
    let mapped = if crate::common::is_back_key_physical(key_event.physical_key) {
        repose_core::input::Key::Escape
    } else {
        map_key(key_event.physical_key, &rt.modifiers)
    };
    let ke = winit_key_to_repose(
        key_event.physical_key,
        key_event.pressed,
        key_event.repeat,
        &mapped,
        &rt.modifiers,
    );
    rt.handle_key_with_text(&ke, text)
}

struct KeyParts {
    physical_key: PhysicalKey,
    pressed: bool,
    repeat: bool,
}

/// Stable debug name for a physical key (`KeyW`, `Digit1`, `Space`,
/// ...), matching `KeyCode`'s variant names. Layout-independent:
/// AZERTY/QWERTZ report the same physical names, so games bind by
/// position the way GML's `ord("W")` binds the US position.
pub fn physical_key_name(key: PhysicalKey) -> String {
    match key {
        PhysicalKey::Code(code) => format!("{code:?}"),
        PhysicalKey::Unidentified(native) => format!("Unidentified({native:?})"),
    }
}

/// Typed physical position for a winit key (`None` wraps to
/// [`repose_core::input::PhysicalKey::Unidentified`], never `None`:
/// every physical press has a position). Backs both the `KeyEvent`
/// field and the `Scheduler::held_keys` snapshot.
pub fn map_physical_key(key: PhysicalKey) -> repose_core::input::PhysicalKey {
    repose_core::input::PhysicalKey::from_name(&physical_key_name(key))
}

/// Ime dispatch helper.
pub fn on_ime(rt: &mut ReposeRuntime, ime: &winit::event::Ime) {
    use winit::event::Ime;
    let ev = match ime {
        Ime::Enabled => repose_core::input::ImeEvent::Start,
        Ime::Preedit(text, cursor) => repose_core::input::ImeEvent::Update {
            text: text.clone(),
            cursor: cursor.map(|(a, b)| (a, b)),
        },
        Ime::Commit(text) => repose_core::input::ImeEvent::Commit(text.clone()),
        Ime::Disabled => repose_core::input::ImeEvent::Cancel,
    };
    rt.handle_ime(&ev);
}