Skip to main content

repose_platform/
common.rs

1use crate::*;
2use repose_core::HitRegion;
3use repose_core::Modifiers;
4use repose_core::Vec2;
5use repose_core::dnd::{DragOver, DragPayload, DropEvent};
6use repose_core::input::{PointerButton, PointerEvent, PointerEventKind, PointerId, PointerKind};
7use repose_core::locals::dp_to_px;
8use repose_core::runtime::Frame;
9use repose_ui::TextFieldState;
10use repose_ui::textfield::{
11    TF_FONT_DP, TF_PADDING_X_DP, caret_xy_for_byte, index_for_x_bytes, index_for_xy_bytes,
12    measure_text,
13};
14
15pub(crate) fn tick_snackbar(last_redraw: web_time::Instant) {
16    let now = web_time::Instant::now();
17    let elapsed = now.saturating_duration_since(last_redraw);
18    let ms = elapsed.as_millis().min(u32::MAX as u128) as u32;
19    if ms > 0 {
20        repose_ui::overlay::SnackbarController::tick_for_frame(ms);
21    }
22}
23
24pub(crate) fn request_redraw(window: &Option<std::sync::Arc<winit::window::Window>>) {
25    if let Some(w) = window {
26        w.request_redraw();
27    }
28}
29
30pub(crate) fn tf_key_of_in_frame(frame_cache: &Option<Frame>, visual_id: u64) -> u64 {
31    if let Some(f) = frame_cache {
32        return tf_key_of(f, visual_id);
33    }
34    visual_id
35}
36
37pub(crate) fn is_textfield_in_frame(frame_cache: &Option<Frame>, id: u64) -> bool {
38    if let Some(f) = frame_cache {
39        f.semantics_nodes
40            .iter()
41            .any(|n| n.id == id && n.role == Role::TextField)
42    } else {
43        false
44    }
45}
46
47pub(crate) fn dnd_update_over_in_frame(
48    frame_cache: &Option<Frame>,
49    drag: &mut Option<DragSession>,
50    modifiers: Modifiers,
51    pos: Vec2,
52) {
53    let Some(f) = frame_cache else {
54        return;
55    };
56    let Some(session) = drag.as_mut() else {
57        return;
58    };
59    dnd_update_over(f, session, modifiers, pos);
60}
61
62pub(crate) fn update_modifiers(modifiers: &mut Modifiers, state: &winit::keyboard::ModifiersState) {
63    modifiers.shift = state.shift_key();
64    modifiers.ctrl = state.control_key();
65    modifiers.alt = state.alt_key();
66    modifiers.meta = state.super_key();
67    modifiers.command = if cfg!(target_os = "macos") {
68        modifiers.meta
69    } else {
70        modifiers.ctrl
71    };
72}
73
74/// Like `index_for_x_bytes` but applies visual transformation if active on the state.
75/// The returned offset is in the original text's byte space.
76pub(crate) fn index_for_x_bytes_vt(state: &TextFieldState, font_px: f32, x_px: f32) -> usize {
77    if let Some(vt) = &state.visual_transformation {
78        let tfmd = vt.filter(&state.text);
79        let display_idx = index_for_x_bytes(&tfmd.text, font_px, x_px);
80        (tfmd.offset_map)(display_idx)
81    } else {
82        index_for_x_bytes(&state.text, font_px, x_px)
83    }
84}
85
86/// Like `index_for_xy_bytes` but applies visual transformation if active on the state.
87pub(crate) fn index_for_xy_bytes_vt(
88    state: &TextFieldState,
89    font_px: f32,
90    wrap_w: f32,
91    x_px: f32,
92    y_px: f32,
93) -> usize {
94    if let Some(vt) = &state.visual_transformation {
95        let tfmd = vt.filter(&state.text);
96        let display_idx = index_for_xy_bytes(&tfmd.text, font_px, wrap_w, x_px, y_px);
97        (tfmd.offset_map)(display_idx)
98    } else {
99        index_for_xy_bytes(&state.text, font_px, wrap_w, x_px, y_px)
100    }
101}
102
103/// Find the top-most hit region index under `pos` (reverse iteration).
104pub(crate) fn top_hit_index(frame: &Frame, pos: Vec2) -> Option<usize> {
105    frame
106        .hit_regions
107        .iter()
108        .enumerate()
109        .rev()
110        .find(|(_, h)| h.rect.contains(pos))
111        .map(|(i, _)| i)
112}
113
114pub(crate) fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
115    frame.hit_regions.iter().position(|h| h.id == id)
116}
117
118pub(crate) fn tf_key_of(frame: &Frame, visual_id: u64) -> u64 {
119    if let Some(i) = hit_index_by_id(frame, visual_id) {
120        let hr = &frame.hit_regions[i];
121        return hr.tf_state_key.unwrap_or(hr.id);
122    }
123    visual_id
124}
125
126pub(crate) fn pe_mouse(event: PointerEventKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
127    PointerEvent {
128        id: PointerId(0),
129        kind: PointerKind::Mouse,
130        event,
131        position: pos,
132        pressure: 1.0,
133        modifiers: mods,
134    }
135}
136
137pub(crate) fn pe_touch(event: PointerEventKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
138    PointerEvent {
139        id: PointerId(0),
140        kind: PointerKind::Touch,
141        event,
142        position: pos,
143        pressure: 1.0,
144        modifiers: mods,
145    }
146}
147
148pub(crate) fn pe_down_primary(kind: PointerKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
149    PointerEvent {
150        id: PointerId(0),
151        kind,
152        event: PointerEventKind::Down(PointerButton::Primary),
153        position: pos,
154        pressure: 1.0,
155        modifiers: mods,
156    }
157}
158
159pub(crate) fn pe_up_primary(kind: PointerKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
160    PointerEvent {
161        id: PointerId(0),
162        kind,
163        event: PointerEventKind::Up(PointerButton::Primary),
164        position: pos,
165        pressure: 1.0,
166        modifiers: mods,
167    }
168}
169
170pub(crate) fn map_key(key: winit::keyboard::PhysicalKey) -> repose_core::input::Key {
171    use repose_core::input::Key;
172    use winit::keyboard::{KeyCode, PhysicalKey};
173
174    match key {
175        PhysicalKey::Code(KeyCode::Enter) => Key::Enter,
176        PhysicalKey::Code(KeyCode::Tab) => Key::Tab,
177        PhysicalKey::Code(KeyCode::Backspace) => Key::Backspace,
178        PhysicalKey::Code(KeyCode::Delete) => Key::Delete,
179        PhysicalKey::Code(KeyCode::Escape) => Key::Escape,
180        PhysicalKey::Code(KeyCode::ArrowLeft) => Key::ArrowLeft,
181        PhysicalKey::Code(KeyCode::ArrowRight) => Key::ArrowRight,
182        PhysicalKey::Code(KeyCode::ArrowUp) => Key::ArrowUp,
183        PhysicalKey::Code(KeyCode::ArrowDown) => Key::ArrowDown,
184        PhysicalKey::Code(KeyCode::Home) => Key::Home,
185        PhysicalKey::Code(KeyCode::End) => Key::End,
186        PhysicalKey::Code(KeyCode::PageUp) => Key::PageUp,
187        PhysicalKey::Code(KeyCode::PageDown) => Key::PageDown,
188        PhysicalKey::Code(KeyCode::Space) => Key::Space,
189        PhysicalKey::Code(KeyCode::KeyA) => Key::Character('a'),
190        PhysicalKey::Code(KeyCode::KeyB) => Key::Character('b'),
191        PhysicalKey::Code(KeyCode::KeyC) => Key::Character('c'),
192        PhysicalKey::Code(KeyCode::KeyD) => Key::Character('d'),
193        PhysicalKey::Code(KeyCode::KeyE) => Key::Character('e'),
194        PhysicalKey::Code(KeyCode::KeyF) => Key::Character('f'),
195        PhysicalKey::Code(KeyCode::KeyG) => Key::Character('g'),
196        PhysicalKey::Code(KeyCode::KeyH) => Key::Character('h'),
197        PhysicalKey::Code(KeyCode::KeyI) => Key::Character('i'),
198        PhysicalKey::Code(KeyCode::KeyJ) => Key::Character('j'),
199        PhysicalKey::Code(KeyCode::KeyK) => Key::Character('k'),
200        PhysicalKey::Code(KeyCode::KeyL) => Key::Character('l'),
201        PhysicalKey::Code(KeyCode::KeyM) => Key::Character('m'),
202        PhysicalKey::Code(KeyCode::KeyN) => Key::Character('n'),
203        PhysicalKey::Code(KeyCode::KeyO) => Key::Character('o'),
204        PhysicalKey::Code(KeyCode::KeyP) => Key::Character('p'),
205        PhysicalKey::Code(KeyCode::KeyQ) => Key::Character('q'),
206        PhysicalKey::Code(KeyCode::KeyR) => Key::Character('r'),
207        PhysicalKey::Code(KeyCode::KeyS) => Key::Character('s'),
208        PhysicalKey::Code(KeyCode::KeyT) => Key::Character('t'),
209        PhysicalKey::Code(KeyCode::KeyU) => Key::Character('u'),
210        PhysicalKey::Code(KeyCode::KeyV) => Key::Character('v'),
211        PhysicalKey::Code(KeyCode::KeyW) => Key::Character('w'),
212        PhysicalKey::Code(KeyCode::KeyX) => Key::Character('x'),
213        PhysicalKey::Code(KeyCode::KeyY) => Key::Character('y'),
214        PhysicalKey::Code(KeyCode::KeyZ) => Key::Character('z'),
215        PhysicalKey::Code(KeyCode::Digit0) => Key::Character('0'),
216        PhysicalKey::Code(KeyCode::Digit1) => Key::Character('1'),
217        PhysicalKey::Code(KeyCode::Digit2) => Key::Character('2'),
218        PhysicalKey::Code(KeyCode::Digit3) => Key::Character('3'),
219        PhysicalKey::Code(KeyCode::Digit4) => Key::Character('4'),
220        PhysicalKey::Code(KeyCode::Digit5) => Key::Character('5'),
221        PhysicalKey::Code(KeyCode::Digit6) => Key::Character('6'),
222        PhysicalKey::Code(KeyCode::Digit7) => Key::Character('7'),
223        PhysicalKey::Code(KeyCode::Digit8) => Key::Character('8'),
224        PhysicalKey::Code(KeyCode::Digit9) => Key::Character('9'),
225        PhysicalKey::Code(KeyCode::F1) => Key::F(1),
226        PhysicalKey::Code(KeyCode::F2) => Key::F(2),
227        PhysicalKey::Code(KeyCode::F3) => Key::F(3),
228        PhysicalKey::Code(KeyCode::F4) => Key::F(4),
229        PhysicalKey::Code(KeyCode::F5) => Key::F(5),
230        PhysicalKey::Code(KeyCode::F6) => Key::F(6),
231        PhysicalKey::Code(KeyCode::F7) => Key::F(7),
232        PhysicalKey::Code(KeyCode::F8) => Key::F(8),
233        PhysicalKey::Code(KeyCode::F9) => Key::F(9),
234        PhysicalKey::Code(KeyCode::F10) => Key::F(10),
235        PhysicalKey::Code(KeyCode::F11) => Key::F(11),
236        PhysicalKey::Code(KeyCode::F12) => Key::F(12),
237        _ => Key::Unknown,
238    }
239}
240
241pub(crate) fn tf_ensure_caret_visible(state: &mut TextFieldState, is_multiline: bool) {
242    let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
243    let wrap_width = state.inner_width;
244
245    if is_multiline {
246        let (cx, cy, _) = caret_xy_for_byte(&state.text, font_px, wrap_width, state.caret_index());
247        let iw = state.inner_width;
248        let ih = state.inner_height;
249        state.ensure_caret_visible_xy(cx, cy, iw, ih, dp_to_px(2.0));
250    } else {
251        let caret_idx = state.caret_index();
252        let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
253            let tfmd = vt.filter(&state.text);
254            let off = repose_core::original_offset_to_display(&state.text, &tfmd.text, caret_idx);
255            (tfmd.text, off)
256        } else {
257            (state.text.clone(), caret_idx)
258        };
259        let m = measure_text(&display, font_px, None);
260        let caret_x_px = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
261        state.ensure_caret_visible(caret_x_px, wrap_width, dp_to_px(2.0));
262    }
263}
264
265/// Place caret in textfield at pointer position and begin drag selection.
266/// Handles both single-line and multiline textfields.
267/// `pos_px`: absolute pointer position in pixels
268/// `scale`: display scale factor
269/// `shift`: whether shift key is held (extends selection)
270pub(crate) fn tf_place_caret_at_pointer(
271    state: &mut TextFieldState,
272    hit_rect: Rect,
273    is_multiline: bool,
274    pos_px: (f32, f32),
275    scale: f32,
276    shift: bool,
277) {
278    let padding_px = TF_PADDING_X_DP * scale;
279    let inner_x_px = hit_rect.x + padding_px;
280    let inner_y_px = hit_rect.y + 8.0 * scale;
281    let content_x_px = (pos_px.0 - inner_x_px + state.scroll_offset).max(0.0);
282    let content_y_px = (pos_px.1 - inner_y_px + state.scroll_offset_y).max(0.0);
283    let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
284
285    let idx = if is_multiline {
286        index_for_xy_bytes_vt(
287            state,
288            font_px,
289            hit_rect.w - 2.0 * padding_px,
290            content_x_px,
291            content_y_px,
292        )
293    } else {
294        index_for_x_bytes_vt(state, font_px, content_x_px)
295    };
296    state.begin_drag(idx, shift);
297
298    // Ensure caret visible
299    let caret_idx = state.caret_index();
300    let wrap_w = hit_rect.w - 2.0 * padding_px;
301    if is_multiline {
302        let (cx, cy, _) = caret_xy_for_byte(&state.text, font_px, wrap_w, caret_idx);
303        let iw = state.inner_width;
304        let ih = state.inner_height;
305        state.ensure_caret_visible_xy(cx, cy, iw, ih, 2.0 * scale);
306    } else {
307        let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
308            let tfmd = vt.filter(&state.text);
309            let off = repose_core::original_offset_to_display(&state.text, &tfmd.text, caret_idx);
310            (tfmd.text, off)
311        } else {
312            (state.text.clone(), caret_idx)
313        };
314        let m = measure_text(&display, font_px, None);
315        let cx = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
316        state.ensure_caret_visible(cx, wrap_w, 2.0 * scale);
317    }
318}
319
320pub(crate) fn touch_slop_px(scale: f32) -> f32 {
321    6.0 * scale
322}
323
324/// Delegate focus movement to the core spatial focus algorithm.
325/// Now lives in `repose_core::spatial_focus_next`.
326pub(crate) fn focus_in_direction(
327    chain: &[u64],
328    hit_regions: &[HitRegion],
329    current: Option<u64>,
330    dir: FocusDirection,
331) -> Option<u64> {
332    repose_core::spatial_focus_next(chain, hit_regions, current, dir)
333}
334
335pub(crate) fn is_dnd_target(hit: &HitRegion) -> bool {
336    hit.on_drop.is_some()
337        || hit.on_drag_enter.is_some()
338        || hit.on_drag_over.is_some()
339        || hit.on_drag_leave.is_some()
340}
341
342pub(crate) fn dnd_target_id_at(frame: &Frame, pos: Vec2) -> Option<u64> {
343    frame
344        .hit_regions
345        .iter()
346        .rev()
347        .filter(|h| h.rect.contains(pos))
348        .find(|h| is_dnd_target(h))
349        .map(|h| h.id)
350}
351/// Dispatch wheel/touch-scroll to scroll consumers under `pos`, propagating
352/// leftovers to parent hit regions.
353///
354/// Returns `(any_consumed, updated_capture)`.  Feed the new capture back on
355/// subsequent calls during the same touch gesture.
356pub(crate) fn dispatch_scroll(
357    frame: &Frame,
358    pos: Vec2,
359    mut delta: Vec2,
360    scroll_capture: Option<u64>,
361) -> (bool, Option<u64>) {
362    let mut new_capture = scroll_capture;
363
364    // If a scrollable has captured the gesture, try it first (position-independent).
365    if let Some(cid) = scroll_capture {
366        if let Some(cb) = frame
367            .hit_regions
368            .iter()
369            .find(|h| h.id == cid)
370            .and_then(|h| h.on_scroll.as_ref())
371        {
372            let before = delta;
373            let leftover = cb(before);
374            let consumed_x = (before.x - leftover.x).abs() > 0.001;
375            let consumed_y = (before.y - leftover.y).abs() > 0.001;
376            if consumed_x || consumed_y {
377                delta = leftover;
378                if delta.x.abs() <= 0.001 && delta.y.abs() <= 0.001 {
379                    return (true, Some(cid));
380                }
381            } else {
382                new_capture = None; // captured region exhausted — fall through
383            }
384        }
385    }
386
387    // Normal position-based dispatch for remaining/uncaptured delta.
388    let mut any_consumed = false;
389    for hit in frame
390        .hit_regions
391        .iter()
392        .rev()
393        .filter(|h| h.rect.contains(pos))
394    {
395        if let Some(cb) = &hit.on_scroll {
396            let before = delta;
397            let leftover = cb(before);
398            let consumed_x = (before.x - leftover.x).abs() > 0.001;
399            let consumed_y = (before.y - leftover.y).abs() > 0.001;
400            if consumed_x || consumed_y {
401                any_consumed = true;
402                if new_capture.is_none() {
403                    new_capture = Some(hit.id);
404                }
405            }
406            delta = leftover;
407            if delta.x.abs() <= 0.001 && delta.y.abs() <= 0.001 {
408                break;
409            }
410        }
411    }
412    (any_consumed, new_capture)
413}
414
415#[macro_export]
416macro_rules! handle_text_undo_redo {
417    ($app:expr, $key_event:expr) => {{
418        let mut __handled = false;
419        if $key_event.state == ElementState::Pressed && !$key_event.repeat && $app.modifiers.command
420        {
421            match $key_event.physical_key {
422                PhysicalKey::Code(KeyCode::KeyZ) if $app.modifiers.shift => {
423                    if let Some(fid) = $app.sched.focused {
424                        let key = $app.tf_key_of(fid);
425                        if let Some(state_rc) = $app.textfield_states.get(&key) {
426                            let mut st = state_rc.borrow_mut();
427                            if st.can_redo() {
428                                st.redo();
429                                $app.notify_text_change(fid, st.text.clone());
430                                __handled = true;
431                            }
432                        }
433                    }
434                }
435                PhysicalKey::Code(KeyCode::KeyZ) => {
436                    if let Some(fid) = $app.sched.focused {
437                        let key = $app.tf_key_of(fid);
438                        if let Some(state_rc) = $app.textfield_states.get(&key) {
439                            let mut st = state_rc.borrow_mut();
440                            if st.can_undo() {
441                                st.undo();
442                                $app.notify_text_change(fid, st.text.clone());
443                                __handled = true;
444                            }
445                        }
446                    }
447                }
448                _ => {}
449            }
450        }
451        __handled
452    }};
453}
454
455/// Handle arrow key spatial focus navigation.
456///
457/// Skips when the focused element is a TextField (those handle arrows for cursor movement)
458#[macro_export]
459macro_rules! handle_arrow_key_spatial_nav {
460    ($app:expr, $key_event:expr, $f:ident, $next:ident, $on_focus:expr) => {
461        if $key_event.state == ElementState::Pressed && !$key_event.repeat {
462            let nav_dir = match $key_event.physical_key {
463                PhysicalKey::Code(KeyCode::ArrowLeft) => Some(FocusDirection::Left),
464                PhysicalKey::Code(KeyCode::ArrowRight) => Some(FocusDirection::Right),
465                PhysicalKey::Code(KeyCode::ArrowUp) => Some(FocusDirection::Up),
466                PhysicalKey::Code(KeyCode::ArrowDown) => Some(FocusDirection::Down),
467                _ => None,
468            };
469            if let Some(dir) = nav_dir
470                && let Some($f) = &$app.frame_cache
471                && !$f
472                    .semantics_nodes
473                    .iter()
474                    .any(|n| $app.sched.focused == Some(n.id) && n.role == Role::TextField)
475            {
476                if let Some($next) = $crate::common::focus_in_direction(
477                    &$f.focus_chain,
478                    &$f.hit_regions,
479                    $app.sched.focused,
480                    dir,
481                ) {
482                    $app.sched.focused = Some($next);
483                    $on_focus;
484                    $app.request_redraw();
485                }
486                return; // swallow arrow key
487            }
488        }
489    };
490}
491
492pub(crate) fn process_render_commands(
493    backend: &mut repose_render_wgpu::WgpuBackend,
494    cmds: Vec<RenderCommand>,
495) {
496    for cmd in cmds {
497        match cmd {
498            RenderCommand::SetImageEncoded {
499                handle,
500                bytes,
501                srgb,
502            } => {
503                let _ = backend.set_image_from_bytes(handle, &bytes, srgb);
504            }
505            RenderCommand::SetImageRgba8 {
506                handle,
507                w,
508                h,
509                rgba,
510                srgb,
511            } => {
512                let _ = backend.set_image_rgba8(handle, w, h, &rgba, srgb);
513            }
514            RenderCommand::SetImageNv12 {
515                handle,
516                w,
517                h,
518                y,
519                uv,
520                full_range,
521            } => {
522                let _ = backend.set_image_nv12(handle, w, h, &y, &uv, full_range);
523            }
524            RenderCommand::RemoveImage { handle } => {
525                backend.remove_image(handle);
526            }
527        }
528    }
529}
530
531/// Update drag-over state: dispatch enter/leave/over events as the cursor moves.
532pub(crate) fn dnd_update_over(
533    frame: &Frame,
534    session: &mut DragSession,
535    modifiers: Modifiers,
536    pos: Vec2,
537) {
538    let new_over = dnd_target_id_at(frame, pos);
539
540    if new_over != session.over_id {
541        if let Some(prev) = session.over_id {
542            if let Some(i) = hit_index_by_id(frame, prev) {
543                if let Some(cb) = &frame.hit_regions[i].on_drag_leave {
544                    cb(DragOver {
545                        source_id: session.source_id,
546                        target_id: prev,
547                        position: pos,
548                        modifiers,
549                        payload: session.payload.clone(),
550                    });
551                }
552            }
553        }
554
555        if let Some(now) = new_over {
556            if let Some(i) = hit_index_by_id(frame, now) {
557                if let Some(cb) = &frame.hit_regions[i].on_drag_enter {
558                    cb(DragOver {
559                        source_id: session.source_id,
560                        target_id: now,
561                        position: pos,
562                        modifiers,
563                        payload: session.payload.clone(),
564                    });
565                }
566            }
567        }
568
569        session.over_id = new_over;
570    }
571
572    if let Some(over) = session.over_id {
573        if let Some(i) = hit_index_by_id(frame, over) {
574            if let Some(cb) = &frame.hit_regions[i].on_drag_over {
575                cb(DragOver {
576                    source_id: session.source_id,
577                    target_id: over,
578                    position: pos,
579                    modifiers,
580                    payload: session.payload.clone(),
581                });
582            }
583        }
584    }
585}
586
587/// Finish a drag-and-drop session: deliver the drop event and the drag-end
588/// callback, then clean up the session.
589pub(crate) fn dnd_finish(
590    frame: &Frame,
591    session: DragSession,
592    modifiers: Modifiers,
593    pos: Vec2,
594    accept_if_possible: bool,
595) -> bool {
596    let mut accepted = false;
597    if accept_if_possible {
598        let drop_target = dnd_target_id_at(frame, pos);
599        if let Some(tid) = drop_target {
600            if let Some(i) = hit_index_by_id(frame, tid) {
601                if let Some(cb) = &frame.hit_regions[i].on_drop {
602                    accepted = cb(DropEvent {
603                        source_id: session.source_id,
604                        target_id: tid,
605                        position: pos,
606                        modifiers,
607                        payload: session.payload.clone(),
608                    });
609                }
610            }
611        }
612    }
613
614    if let Some(i) = hit_index_by_id(frame, session.source_id) {
615        if let Some(cb) = &frame.hit_regions[i].on_drag_end {
616            cb(repose_core::dnd::DragEnd { accepted });
617        }
618    }
619
620    accepted
621}
622
623#[derive(Clone)]
624pub(crate) struct DragSession {
625    pub source_id: u64,
626    pub payload: DragPayload,
627    pub start_px: (f32, f32),
628    pub over_id: Option<u64>,
629}