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};
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,
Unavailable,
}
pub enum BackendRetry {
Ready,
RetryAfter(Duration),
Unavailable,
}
#[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)
}
pub fn begin(&mut self) {
self.state = Some(BackendState::Pending);
}
pub fn ready(&mut self) {
self.state = Some(BackendState::Ready);
self.attempts = 0;
}
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)))
}
}
}
pub fn on_modifiers_changed(rt: &mut ReposeRuntime, state: &winit::keyboard::ModifiersState) {
crate::common::update_modifiers(&mut rt.modifiers, state);
}
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
}
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 })
}
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,
}
}
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>,
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 => {
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;
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,
}
}
}
}
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)),
);
}
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
}
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)
}
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,
)
}
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,
);
{
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,
}
pub fn physical_key_name(key: PhysicalKey) -> String {
match key {
PhysicalKey::Code(code) => format!("{code:?}"),
PhysicalKey::Unidentified(native) => format!("Unidentified({native:?})"),
}
}
pub fn map_physical_key(key: PhysicalKey) -> repose_core::input::PhysicalKey {
repose_core::input::PhysicalKey::from_name(&physical_key_name(key))
}
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);
}