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