Skip to main content

repose_platform/
common.rs

1use crate::*;
2use repose_core::Modifiers;
3use repose_core::Vec2;
4use repose_core::input::{PointerButton, PointerEvent, PointerEventKind, PointerId, PointerKind};
5use repose_core::locals::dp_to_px;
6use repose_core::runtime::Frame;
7use repose_ui::TextFieldState;
8use repose_ui::textfield::{
9    TF_FONT_DP, TextMeasureConfig, caret_xy_for_byte, index_for_x_bytes, index_for_xy_bytes, measure_text,
10};
11
12pub(crate) fn tick_snackbar(last_redraw: web_time::Instant) {
13    let now = web_time::Instant::now();
14    let elapsed = now.saturating_duration_since(last_redraw);
15    let ms = elapsed.as_millis().min(u32::MAX as u128) as u32;
16    if ms > 0 {
17        repose_ui::overlay::SnackbarController::tick_for_frame(ms);
18    }
19}
20
21pub(crate) fn request_redraw(window: &Option<std::sync::Arc<winit::window::Window>>) {
22    if let Some(w) = window {
23        w.request_redraw();
24    }
25}
26
27pub(crate) fn tf_key_of_in_frame(frame_cache: &Option<Frame>, visual_id: u64) -> u64 {
28    if let Some(f) = frame_cache {
29        return tf_key_of(f, visual_id);
30    }
31    visual_id
32}
33
34pub(crate) fn is_textfield_in_frame(frame_cache: &Option<Frame>, id: u64) -> bool {
35    if let Some(f) = frame_cache {
36        f.semantics_nodes
37            .iter()
38            .any(|n| n.id == id && n.role == Role::TextField)
39    } else {
40        false
41    }
42}
43
44pub(crate) fn update_modifiers(modifiers: &mut Modifiers, state: &winit::keyboard::ModifiersState) {
45    modifiers.shift = state.shift_key();
46    modifiers.ctrl = state.control_key();
47    modifiers.alt = state.alt_key();
48    modifiers.meta = state.super_key();
49    modifiers.command = if cfg!(target_os = "macos") {
50        modifiers.meta
51    } else {
52        modifiers.ctrl
53    };
54}
55
56/// Like `index_for_x_bytes` but applies visual transformation if active on the state.
57/// The returned offset is in the original text's byte space.
58pub(crate) fn index_for_x_bytes_vt(state: &TextFieldState, font_px: f32, x_px: f32) -> usize {
59    if let Some(vt) = &state.visual_transformation {
60        let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
61        let tfmd = vt.filter(&annotated);
62        let display_idx = index_for_x_bytes(tfmd.text.as_str(), font_px, x_px, 400, 0);
63        tfmd.offset_mapping.transformed_to_original(display_idx)
64    } else {
65        index_for_x_bytes(&state.text, font_px, x_px, 400, 0)
66    }
67}
68
69/// Like `index_for_xy_bytes` but applies visual transformation if active on the state.
70pub(crate) fn index_for_xy_bytes_vt(
71    state: &TextFieldState,
72    font_px: f32,
73    wrap_w: f32,
74    x_px: f32,
75    y_px: f32,
76) -> usize {
77    if let Some(vt) = &state.visual_transformation {
78        let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
79        let tfmd = vt.filter(&annotated);
80        let display_idx = index_for_xy_bytes(tfmd.text.as_str(), font_px, wrap_w, x_px, y_px);
81        tfmd.offset_mapping.transformed_to_original(display_idx)
82    } else {
83        index_for_xy_bytes(&state.text, font_px, wrap_w, x_px, y_px)
84    }
85}
86
87/// Find the top-most hit region index under `pos` (reverse iteration).
88pub(crate) fn top_hit_index(frame: &Frame, pos: Vec2) -> Option<usize> {
89    frame
90        .hit_regions
91        .iter()
92        .enumerate()
93        .rev()
94        .find(|(_, h)| h.rect.contains(pos))
95        .map(|(i, _)| i)
96}
97
98pub(crate) fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
99    frame.hit_regions.iter().position(|h| h.id == id)
100}
101
102pub(crate) fn tf_key_of(frame: &Frame, visual_id: u64) -> u64 {
103    if let Some(i) = hit_index_by_id(frame, visual_id) {
104        let hr = &frame.hit_regions[i];
105        return hr.tf_state_key.unwrap_or(hr.id);
106    }
107    visual_id
108}
109
110pub(crate) fn pe_mouse(event: PointerEventKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
111    PointerEvent::new(PointerId(0), PointerKind::Mouse, event, pos, 1.0, mods)
112}
113
114pub(crate) fn pe_touch(event: PointerEventKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
115    PointerEvent::new(PointerId(0), PointerKind::Touch, event, pos, 1.0, mods)
116}
117
118pub(crate) fn pe_down_primary(kind: PointerKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
119    PointerEvent::new(
120        PointerId(0),
121        kind,
122        PointerEventKind::Down(PointerButton::Primary),
123        pos,
124        1.0,
125        mods,
126    )
127}
128
129pub(crate) fn pe_up_primary(kind: PointerKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
130    PointerEvent::new(
131        PointerId(0),
132        kind,
133        PointerEventKind::Up(PointerButton::Primary),
134        pos,
135        1.0,
136        mods,
137    )
138}
139
140pub(crate) fn map_key(key: winit::keyboard::PhysicalKey) -> repose_core::input::Key {
141    use repose_core::input::Key;
142    use winit::keyboard::{KeyCode, PhysicalKey};
143
144    match key {
145        PhysicalKey::Code(KeyCode::Enter) => Key::Enter,
146        PhysicalKey::Code(KeyCode::Tab) => Key::Tab,
147        PhysicalKey::Code(KeyCode::Backspace) => Key::Backspace,
148        PhysicalKey::Code(KeyCode::Delete) => Key::Delete,
149        PhysicalKey::Code(KeyCode::Escape) => Key::Escape,
150        PhysicalKey::Code(KeyCode::ArrowLeft) => Key::ArrowLeft,
151        PhysicalKey::Code(KeyCode::ArrowRight) => Key::ArrowRight,
152        PhysicalKey::Code(KeyCode::ArrowUp) => Key::ArrowUp,
153        PhysicalKey::Code(KeyCode::ArrowDown) => Key::ArrowDown,
154        PhysicalKey::Code(KeyCode::Home) => Key::Home,
155        PhysicalKey::Code(KeyCode::End) => Key::End,
156        PhysicalKey::Code(KeyCode::PageUp) => Key::PageUp,
157        PhysicalKey::Code(KeyCode::PageDown) => Key::PageDown,
158        PhysicalKey::Code(KeyCode::Space) => Key::Space,
159        PhysicalKey::Code(KeyCode::KeyA) => Key::Character('a'),
160        PhysicalKey::Code(KeyCode::KeyB) => Key::Character('b'),
161        PhysicalKey::Code(KeyCode::KeyC) => Key::Character('c'),
162        PhysicalKey::Code(KeyCode::KeyD) => Key::Character('d'),
163        PhysicalKey::Code(KeyCode::KeyE) => Key::Character('e'),
164        PhysicalKey::Code(KeyCode::KeyF) => Key::Character('f'),
165        PhysicalKey::Code(KeyCode::KeyG) => Key::Character('g'),
166        PhysicalKey::Code(KeyCode::KeyH) => Key::Character('h'),
167        PhysicalKey::Code(KeyCode::KeyI) => Key::Character('i'),
168        PhysicalKey::Code(KeyCode::KeyJ) => Key::Character('j'),
169        PhysicalKey::Code(KeyCode::KeyK) => Key::Character('k'),
170        PhysicalKey::Code(KeyCode::KeyL) => Key::Character('l'),
171        PhysicalKey::Code(KeyCode::KeyM) => Key::Character('m'),
172        PhysicalKey::Code(KeyCode::KeyN) => Key::Character('n'),
173        PhysicalKey::Code(KeyCode::KeyO) => Key::Character('o'),
174        PhysicalKey::Code(KeyCode::KeyP) => Key::Character('p'),
175        PhysicalKey::Code(KeyCode::KeyQ) => Key::Character('q'),
176        PhysicalKey::Code(KeyCode::KeyR) => Key::Character('r'),
177        PhysicalKey::Code(KeyCode::KeyS) => Key::Character('s'),
178        PhysicalKey::Code(KeyCode::KeyT) => Key::Character('t'),
179        PhysicalKey::Code(KeyCode::KeyU) => Key::Character('u'),
180        PhysicalKey::Code(KeyCode::KeyV) => Key::Character('v'),
181        PhysicalKey::Code(KeyCode::KeyW) => Key::Character('w'),
182        PhysicalKey::Code(KeyCode::KeyX) => Key::Character('x'),
183        PhysicalKey::Code(KeyCode::KeyY) => Key::Character('y'),
184        PhysicalKey::Code(KeyCode::KeyZ) => Key::Character('z'),
185        PhysicalKey::Code(KeyCode::Digit0) => Key::Character('0'),
186        PhysicalKey::Code(KeyCode::Digit1) => Key::Character('1'),
187        PhysicalKey::Code(KeyCode::Digit2) => Key::Character('2'),
188        PhysicalKey::Code(KeyCode::Digit3) => Key::Character('3'),
189        PhysicalKey::Code(KeyCode::Digit4) => Key::Character('4'),
190        PhysicalKey::Code(KeyCode::Digit5) => Key::Character('5'),
191        PhysicalKey::Code(KeyCode::Digit6) => Key::Character('6'),
192        PhysicalKey::Code(KeyCode::Digit7) => Key::Character('7'),
193        PhysicalKey::Code(KeyCode::Digit8) => Key::Character('8'),
194        PhysicalKey::Code(KeyCode::Digit9) => Key::Character('9'),
195        PhysicalKey::Code(KeyCode::F1) => Key::F(1),
196        PhysicalKey::Code(KeyCode::F2) => Key::F(2),
197        PhysicalKey::Code(KeyCode::F3) => Key::F(3),
198        PhysicalKey::Code(KeyCode::F4) => Key::F(4),
199        PhysicalKey::Code(KeyCode::F5) => Key::F(5),
200        PhysicalKey::Code(KeyCode::F6) => Key::F(6),
201        PhysicalKey::Code(KeyCode::F7) => Key::F(7),
202        PhysicalKey::Code(KeyCode::F8) => Key::F(8),
203        PhysicalKey::Code(KeyCode::F9) => Key::F(9),
204        PhysicalKey::Code(KeyCode::F10) => Key::F(10),
205        PhysicalKey::Code(KeyCode::F11) => Key::F(11),
206        PhysicalKey::Code(KeyCode::F12) => Key::F(12),
207        _ => Key::Unknown,
208    }
209}
210
211pub(crate) fn tf_ensure_caret_visible(state: &mut TextFieldState, is_multiline: bool) {
212    let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
213    let wrap_width = state.inner_width;
214
215    if is_multiline {
216        let (cx, cy, _) = caret_xy_for_byte(&state.text, font_px, wrap_width, state.caret_index());
217        let iw = state.inner_width;
218        let ih = state.inner_height;
219        state.ensure_caret_visible_xy(cx, cy, iw, ih, dp_to_px(2.0));
220    } else {
221        let caret_idx = state.caret_index();
222        let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
223            let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
224            let tfmd = vt.filter(&annotated);
225            let off =
226                repose_core::original_offset_to_display(&state.text, tfmd.text.as_str(), caret_idx);
227            (tfmd.text.text, off)
228        } else {
229            (state.text.clone(), caret_idx)
230        };
231        let m = measure_text(&display, font_px, TextMeasureConfig::default());
232        let caret_x_px = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
233        state.ensure_caret_visible(caret_x_px, wrap_width, dp_to_px(2.0));
234    }
235}
236
237/// Place caret in textfield at pointer position and begin drag selection.
238/// Handles both single-line and multiline textfields.
239/// `pos_px`: absolute pointer position in pixels
240/// `scale`: display scale factor
241/// `shift`: whether shift key is held (extends selection)
242pub(crate) fn tf_place_caret_at_pointer(
243    state: &mut TextFieldState,
244    hit_rect: Rect,
245    is_multiline: bool,
246    pos_px: (f32, f32),
247    scale: f32,
248    shift: bool,
249) {
250    let inner_x_px = hit_rect.x;
251    let inner_y_px = hit_rect.y;
252    let content_x_px = (pos_px.0 - inner_x_px + state.scroll_offset).max(0.0);
253    let content_y_px = (pos_px.1 - inner_y_px + state.scroll_offset_y).max(0.0);
254    let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
255
256    let idx = if is_multiline {
257        index_for_xy_bytes_vt(
258            state,
259            font_px,
260            hit_rect.w,
261            content_x_px,
262            content_y_px,
263        )
264    } else {
265        index_for_x_bytes_vt(state, font_px, content_x_px)
266    };
267    state.begin_drag(idx, shift);
268
269    // Ensure caret visible
270    let caret_idx = state.caret_index();
271    if is_multiline {
272        let (cx, cy, _) = caret_xy_for_byte(&state.text, font_px, hit_rect.w, caret_idx);
273        let iw = state.inner_width;
274        let ih = state.inner_height;
275        state.ensure_caret_visible_xy(cx, cy, iw, ih, 2.0 * scale);
276    } else {
277        let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
278            let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
279            let tfmd = vt.filter(&annotated);
280            let off =
281                repose_core::original_offset_to_display(&state.text, tfmd.text.as_str(), caret_idx);
282            (tfmd.text.text, off)
283        } else {
284            (state.text.clone(), caret_idx)
285        };
286        let m = measure_text(&display, font_px, TextMeasureConfig::default());
287        let cx = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
288        state.ensure_caret_visible(cx, hit_rect.w, 2.0 * scale);
289    }
290}
291
292/// Dispatch wheel/touch-scroll to scroll consumers under `pos`, propagating
293/// leftovers to parent hit regions.
294///
295/// Returns `(any_consumed, updated_capture)`.  Feed the new capture back on
296/// subsequent calls during the same touch gesture.
297pub(crate) fn dispatch_scroll(
298    frame: &Frame,
299    pos: Vec2,
300    mut delta: Vec2,
301    scroll_capture: Option<u64>,
302) -> (bool, Option<u64>) {
303    let mut new_capture = scroll_capture;
304
305    // If a scrollable has captured the gesture, try it first (position-independent).
306    if let Some(cid) = scroll_capture {
307        if let Some(cb) = frame
308            .hit_regions
309            .iter()
310            .find(|h| h.id == cid)
311            .and_then(|h| h.on_scroll.as_ref())
312        {
313            let before = delta;
314            let leftover = cb(before);
315            let consumed_x = (before.x - leftover.x).abs() > 0.001;
316            let consumed_y = (before.y - leftover.y).abs() > 0.001;
317            if consumed_x || consumed_y {
318                delta = leftover;
319                if delta.x.abs() <= 0.001 && delta.y.abs() <= 0.001 {
320                    return (true, Some(cid));
321                }
322            } else {
323                new_capture = None; // captured region exhausted - fall through
324            }
325        }
326    }
327
328    // Normal position-based dispatch for remaining/uncaptured delta.
329    let mut any_consumed = false;
330    for hit in frame
331        .hit_regions
332        .iter()
333        .rev()
334        .filter(|h| h.rect.contains(pos))
335    {
336        if let Some(cb) = &hit.on_scroll {
337            let before = delta;
338            let leftover = cb(before);
339            let consumed_x = (before.x - leftover.x).abs() > 0.001;
340            let consumed_y = (before.y - leftover.y).abs() > 0.001;
341            if consumed_x || consumed_y {
342                any_consumed = true;
343                if new_capture.is_none() {
344                    new_capture = Some(hit.id);
345                }
346            }
347            delta = leftover;
348            if delta.x.abs() <= 0.001 && delta.y.abs() <= 0.001 {
349                break;
350            }
351        }
352    }
353    (any_consumed, new_capture)
354}
355
356#[macro_export]
357macro_rules! handle_text_undo_redo {
358    ($app:expr, $key_event:expr) => {{
359        let mut __handled = false;
360        if $key_event.state == ElementState::Pressed && !$key_event.repeat && $app.modifiers.command
361        {
362            match $key_event.physical_key {
363                PhysicalKey::Code(KeyCode::KeyZ) if $app.modifiers.shift => {
364                    if let Some(fid) = $app.sched.focused {
365                        let key = $app.tf_key_of(fid);
366                        if let Some(state_rc) = $app.textfield_states.get(&key) {
367                            let mut st = state_rc.borrow_mut();
368                            if st.can_redo() {
369                                st.redo();
370                                $app.notify_text_change(fid, st.text.clone());
371                                __handled = true;
372                            }
373                        }
374                    }
375                }
376                PhysicalKey::Code(KeyCode::KeyZ) => {
377                    if let Some(fid) = $app.sched.focused {
378                        let key = $app.tf_key_of(fid);
379                        if let Some(state_rc) = $app.textfield_states.get(&key) {
380                            let mut st = state_rc.borrow_mut();
381                            if st.can_undo() {
382                                st.undo();
383                                $app.notify_text_change(fid, st.text.clone());
384                                __handled = true;
385                            }
386                        }
387                    }
388                }
389                _ => {}
390            }
391        }
392        __handled
393    }};
394}
395
396pub(crate) fn process_render_commands(
397    backend: &mut repose_render_wgpu::WgpuBackend,
398    cmds: Vec<RenderCommand>,
399) {
400    for cmd in cmds {
401        match cmd {
402            RenderCommand::SetImageEncoded {
403                handle,
404                bytes,
405                srgb,
406            } => {
407                let _ = backend.set_image_from_bytes(handle, &bytes, srgb);
408            }
409            RenderCommand::SetImageRgba8 {
410                handle,
411                w,
412                h,
413                rgba,
414                srgb,
415            } => {
416                let _ = backend.set_image_rgba8(handle, w, h, &rgba, srgb);
417            }
418            RenderCommand::SetImageNv12 {
419                handle,
420                w,
421                h,
422                y,
423                uv,
424                full_range,
425            } => {
426                let _ = backend.set_image_nv12(handle, w, h, &y, &uv, full_range);
427            }
428            RenderCommand::RemoveImage { handle } => {
429                backend.remove_image(handle);
430            }
431        }
432    }
433}