Skip to main content

repose_ui/
windowing.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use repose_core::{
5    request_frame, AlignItems, CursorIcon, Modifier, PaddingValues, PointerButton, PointerEvent,
6    PointerEventKind, Rect, Size, Vec2, View, ViewKind,
7};
8
9use crate::{Box, Column, Row, Spacer, Stack, Surface, Text, TextStyle, ViewExt};
10
11const TITLE_BAR_HEIGHT_DP: f32 = 32.0;
12const WINDOW_PADDING_DP: f32 = 8.0;
13const RESIZE_HANDLE_DP: f32 = 10.0;
14const WINDOW_Z_BASE: f32 = 10_000.0;
15const WINDOW_Z_STEP: f32 = 10.0;
16const KEEP_VISIBLE_DP: f32 = 24.0;
17
18#[derive(Clone)]
19pub struct WindowAction {
20    pub label: String,
21    pub on_click: Rc<dyn Fn()>,
22}
23
24#[derive(Clone)]
25pub struct FloatingWindow {
26    pub id: u64,
27    pub title: String,
28    pub content: Rc<dyn Fn() -> View>,
29    pub on_close: Option<Rc<dyn Fn()>>,
30    /// Position in dp from the host's top-left corner.
31    pub position: Vec2,
32    /// Size in dp.
33    pub size: Size,
34    /// Minimum size in dp.
35    pub min_size: Size,
36    /// Maximum size in dp (optional).
37    pub max_size: Option<Size>,
38    pub resizable: bool,
39    pub closable: bool,
40    pub draggable: bool,
41    pub actions: Vec<WindowAction>,
42}
43
44impl FloatingWindow {
45    pub fn new(id: u64, title: impl Into<String>, content: Rc<dyn Fn() -> View>) -> Self {
46        Self {
47            id,
48            title: title.into(),
49            content,
50            on_close: None,
51            position: Vec2 { x: 40.0, y: 40.0 },
52            size: Size {
53                width: 420.0,
54                height: 300.0,
55            },
56            min_size: Size {
57                width: 220.0,
58                height: 160.0,
59            },
60            max_size: None,
61            resizable: true,
62            closable: true,
63            draggable: true,
64            actions: Vec::new(),
65        }
66    }
67
68    pub fn position(mut self, x: f32, y: f32) -> Self {
69        self.position = Vec2 { x, y };
70        self
71    }
72
73    pub fn size(mut self, width: f32, height: f32) -> Self {
74        self.size = Size { width, height };
75        self
76    }
77
78    pub fn min_size(mut self, width: f32, height: f32) -> Self {
79        self.min_size = Size { width, height };
80        self
81    }
82
83    pub fn max_size(mut self, width: f32, height: f32) -> Self {
84        self.max_size = Some(Size { width, height });
85        self
86    }
87
88    pub fn resizable(mut self, resizable: bool) -> Self {
89        self.resizable = resizable;
90        self
91    }
92
93    pub fn closable(mut self, closable: bool) -> Self {
94        self.closable = closable;
95        self
96    }
97
98    pub fn draggable(mut self, draggable: bool) -> Self {
99        self.draggable = draggable;
100        self
101    }
102
103    pub fn actions(mut self, actions: Vec<WindowAction>) -> Self {
104        self.actions = actions;
105        self
106    }
107
108    pub fn on_close(mut self, on_close: Rc<dyn Fn()>) -> Self {
109        self.on_close = Some(on_close);
110        self
111    }
112}
113
114#[derive(Clone, Default)]
115pub struct WindowManagerState {
116    pub windows: Vec<FloatingWindow>,
117    next_id: u64,
118    pub active: Option<u64>,
119}
120
121impl WindowManagerState {
122    pub fn new() -> Self {
123        Self {
124            windows: Vec::new(),
125            next_id: 1,
126            active: None,
127        }
128    }
129
130    pub fn alloc_id(&mut self) -> u64 {
131        let id = self.next_id;
132        self.next_id += 1;
133        id
134    }
135
136    pub fn open(&mut self, window: FloatingWindow) {
137        let window_id = window.id;
138        if let Some(pos) = self.windows.iter().position(|w| w.id == window_id) {
139            self.windows[pos] = window;
140        } else {
141            self.windows.push(window);
142        }
143        self.bring_to_front(window_id);
144    }
145
146    pub fn close(&mut self, id: u64) -> bool {
147        if let Some(idx) = self.windows.iter().position(|w| w.id == id) {
148            self.windows.remove(idx);
149            if self.active == Some(id) {
150                self.active = self.windows.last().map(|w| w.id);
151            }
152            true
153        } else {
154            false
155        }
156    }
157
158    pub fn bring_to_front(&mut self, id: u64) -> bool {
159        if let Some(idx) = self.windows.iter().position(|w| w.id == id) {
160            let window = self.windows.remove(idx);
161            self.windows.push(window);
162            self.active = Some(id);
163            true
164        } else {
165            false
166        }
167    }
168
169    pub fn set_position(&mut self, id: u64, position: Vec2) -> bool {
170        if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
171            w.position = position;
172            true
173        } else {
174            false
175        }
176    }
177
178    pub fn set_size(&mut self, id: u64, size: Size) -> bool {
179        if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
180            w.size = size;
181            true
182        } else {
183            false
184        }
185    }
186}
187
188#[derive(Clone, Copy, Debug, PartialEq, Eq)]
189enum ResizeHandle {
190    Left,
191    Right,
192    Top,
193    Bottom,
194    TopLeft,
195    TopRight,
196    BottomLeft,
197    BottomRight,
198}
199
200#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201enum DragKind {
202    Move,
203    Resize(ResizeHandle),
204}
205
206#[derive(Clone, Copy, Debug)]
207struct DragState {
208    window_id: u64,
209    kind: DragKind,
210    start_pointer: Vec2,
211    start_pos: Vec2,
212    start_size: Size,
213    min_size: Size,
214    max_size: Option<Size>,
215}
216
217pub fn WindowHost(
218    key: impl Into<String>,
219    modifier: Modifier,
220    state: Rc<RefCell<WindowManagerState>>,
221    content: View,
222) -> View {
223    let key = key.into();
224    let bounds = repose_core::remember_with_key(format!("window:bounds:{key}"), || {
225        RefCell::new(Rect::default())
226    });
227    let drag_state = repose_core::remember_with_key(format!("window:drag:{key}"), || {
228        RefCell::new(None::<DragState>)
229    });
230
231    let bounds_capture = bounds.clone();
232    let host_mod = modifier.painter(move |_scene, rect_px| {
233        let mut bounds_dp = rect_px_to_dp(rect_px);
234        bounds_dp.x = 0.0;
235        bounds_dp.y = 0.0;
236        *bounds_capture.borrow_mut() = bounds_dp;
237    });
238
239    let active_id = state.borrow().active;
240    let windows = state.borrow().windows.clone();
241
242    let window_views = windows
243        .into_iter()
244        .enumerate()
245        .map(|(idx, window)| {
246            let z_base = WINDOW_Z_BASE + (idx as f32 * WINDOW_Z_STEP);
247            let chrome_z = 2.0;
248            let content_z = 1.0;
249
250            let window_id = window.id;
251            let window_actions = window.actions.clone();
252            let window_closable = window.closable;
253            let window_on_close = window.on_close.clone();
254            let window_content = window.content.clone();
255            let window_pos = window.position;
256            let window_size = window.size;
257            let window_title = window.title.clone();
258            let window_draggable = window.draggable;
259            let window_resizable = window.resizable;
260
261            let is_active = active_id == Some(window_id);
262            let th = repose_core::locals::theme();
263            let border_color = if is_active { th.focus } else { th.outline };
264            let title_fg = if is_active {
265                th.on_surface
266            } else {
267                th.on_surface_variant
268            };
269            let title_bg = if is_active {
270                th.surface_variant
271            } else {
272                th.surface
273            };
274
275            let start_drag = {
276                let drag_state = drag_state.clone();
277                let state = state.clone();
278                move |kind: DragKind, pe: PointerEvent| {
279                    if !matches!(pe.event, PointerEventKind::Down(PointerButton::Primary)) {
280                        return;
281                    }
282
283                    let (pos, size, min_size, max_size) = {
284                        let st = state.borrow();
285                        let Some(w) = st.windows.iter().find(|w| w.id == window_id) else {
286                            return;
287                        };
288                        (w.position, w.size, w.min_size, w.max_size)
289                    };
290
291                    let start = DragState {
292                        window_id,
293                        kind,
294                        start_pointer: px_vec_to_dp(pe.position),
295                        start_pos: pos,
296                        start_size: size,
297                        min_size,
298                        max_size,
299                    };
300                    *drag_state.borrow_mut() = Some(start);
301                    state.borrow_mut().bring_to_front(window_id);
302                    request_frame();
303                }
304            };
305
306            let bring_to_front = {
307                let state = state.clone();
308                move || {
309                    state.borrow_mut().bring_to_front(window_id);
310                    request_frame();
311                }
312            };
313
314            let move_drag = {
315                let drag_state = drag_state.clone();
316                let state = state.clone();
317                let bounds = bounds.clone();
318                move |pe: PointerEvent| {
319                    let Some(ds) = *drag_state.borrow() else {
320                        return;
321                    };
322                    if ds.window_id != window_id {
323                        return;
324                    }
325
326                    let cur = px_vec_to_dp(pe.position);
327                    let delta = Vec2 {
328                        x: cur.x - ds.start_pointer.x,
329                        y: cur.y - ds.start_pointer.y,
330                    };
331                    let bounds = *bounds.borrow();
332
333                    let (mut pos, mut size) = match ds.kind {
334                        DragKind::Move => (
335                            Vec2 {
336                                x: ds.start_pos.x + delta.x,
337                                y: ds.start_pos.y + delta.y,
338                            },
339                            ds.start_size,
340                        ),
341                        DragKind::Resize(handle) => resize_from_handle(ds, handle, delta),
342                    };
343
344                    let (clamped_pos, clamped_size) =
345                        clamp_rect(pos, size, ds.min_size, ds.max_size, bounds);
346                    pos = clamped_pos;
347                    size = clamped_size;
348
349                    let mut st = state.borrow_mut();
350                    st.set_position(window_id, pos);
351                    st.set_size(window_id, size);
352                    request_frame();
353                }
354            };
355
356            let end_drag = {
357                let drag_state = drag_state.clone();
358                move |_pe: PointerEvent| {
359                    *drag_state.borrow_mut() = None;
360                }
361            };
362
363            let is_dragging = drag_state
364                .borrow()
365                .as_ref()
366                .is_some_and(|d| d.window_id == window_id && d.kind == DragKind::Move);
367            let title_cursor = if is_dragging {
368                CursorIcon::Grabbing
369            } else {
370                CursorIcon::Grab
371            };
372
373            let title_bar = {
374                let window_id = window_id;
375                let actions = window_actions.clone();
376                let close_enabled = window_closable;
377                let close_state = state.clone();
378                let close_handler = window_on_close.clone();
379                let focus_state = state.clone();
380                let mut action_views = Vec::new();
381
382                for (idx, action) in actions.into_iter().enumerate() {
383                    let label = action.label.clone();
384                    let on_click = action.on_click.clone();
385                    let focus_state = focus_state.clone();
386                    let action_id = window_id;
387                    action_views.push(
388                        Box(Modifier::new()
389                            .padding_values(PaddingValues {
390                                left: 6.0,
391                                right: 6.0,
392                                top: 4.0,
393                                bottom: 4.0,
394                            })
395                            .clip_rounded(6.0)
396                            .background(th.surface_variant)
397                            .clickable()
398                            .on_pointer_down(move |_| {
399                                focus_state.borrow_mut().bring_to_front(action_id);
400                                (on_click)();
401                                request_frame();
402                            })
403                            .z_index(1.0)
404                            .key(key_for(window_id, 60 + idx as u64)))
405                        .child(Text(label).size(11.0).color(th.primary).single_line()),
406                    );
407                }
408
409                if close_enabled {
410                    let close_id = window_id;
411                    let focus_state = focus_state.clone();
412                    action_views.push(
413                        Box(Modifier::new()
414                            .padding_values(PaddingValues {
415                                left: 6.0,
416                                right: 6.0,
417                                top: 4.0,
418                                bottom: 4.0,
419                            })
420                            .clip_rounded(6.0)
421                            .background(th.error.with_alpha(20))
422                            .clickable()
423                            .on_pointer_down(move |_| {
424                                focus_state.borrow_mut().bring_to_front(close_id);
425                                if let Some(handler) = close_handler.as_ref() {
426                                    (handler)();
427                                } else {
428                                    close_state.borrow_mut().close(close_id);
429                                }
430                                request_frame();
431                            })
432                            .z_index(1.0)
433                            .key(key_for(window_id, 90)))
434                        .child(Text("x").size(12.0).color(th.error)),
435                    );
436                }
437
438                let mut bar_mod = Modifier::new()
439                    .fill_max_width()
440                    .height(TITLE_BAR_HEIGHT_DP)
441                    .background(title_bg)
442                    .padding_values(PaddingValues {
443                        left: 10.0,
444                        right: 8.0,
445                        top: 6.0,
446                        bottom: 6.0,
447                    })
448                    .align_items(AlignItems::Center)
449                    .key(key_for(window_id, 10));
450
451                if window_draggable {
452                    bar_mod = bar_mod
453                        .cursor(title_cursor)
454                        .on_pointer_down({
455                            let start_drag = start_drag.clone();
456                            move |pe| start_drag(DragKind::Move, pe)
457                        })
458                        .on_pointer_move(move_drag.clone())
459                        .on_pointer_up(end_drag.clone());
460                } else {
461                    bar_mod = bar_mod.on_pointer_down(move |_| bring_to_front());
462                }
463
464                let bar = Row(bar_mod).child((
465                    Text(window_title)
466                        .size(13.0)
467                        .color(title_fg)
468                        .single_line()
469                        .overflow_ellipsize(),
470                    Spacer(),
471                    Row(Modifier::new().align_items(AlignItems::Center))
472                        .with_children(action_views),
473                ));
474
475                apply_z_offset(bar, chrome_z)
476            };
477
478            let content_view = {
479                let content_builder = window_content.clone();
480                let focus_cb = {
481                    let state = state.clone();
482                    let window_id = window_id;
483                    Rc::new(move || {
484                        state.borrow_mut().bring_to_front(window_id);
485                        request_frame();
486                    })
487                };
488                let inner = inject_focus_handlers((content_builder)(), focus_cb);
489                apply_z_offset(inner, content_z)
490            };
491
492            let content_shell =
493                Box(Modifier::new().fill_max_size().padding(WINDOW_PADDING_DP)).child(content_view);
494
495            let resize_handles = if window_resizable {
496                let handles = build_resize_handles(
497                    window_id,
498                    start_drag.clone(),
499                    move_drag.clone(),
500                    end_drag.clone(),
501                );
502                apply_z_offset(handles, chrome_z + 1.0)
503            } else {
504                Box(Modifier::new())
505            };
506
507            let column = Column(Modifier::new().fill_max_size()).child((title_bar, content_shell));
508
509            let focus_on_pointer_down = {
510                let state = state.clone();
511                move |pe: PointerEvent| {
512                    if matches!(pe.event, PointerEventKind::Down(PointerButton::Primary)) {
513                        state.borrow_mut().bring_to_front(window_id);
514                        request_frame();
515                    }
516                }
517            };
518
519            let mut window_view = Surface(
520                Modifier::new()
521                    .key(key_for(window_id, 1))
522                    .absolute()
523                    .offset(Some(window_pos.x), Some(window_pos.y), None, None)
524                    .size(window_size.width, window_size.height)
525                    .background(th.surface)
526                    .border(1.0, border_color, 10.0)
527                    .clip_rounded(10.0)
528                    .z_index(-1.0)
529                    .on_pointer_down(focus_on_pointer_down),
530                Stack(Modifier::new().fill_max_size()).child((column, resize_handles)),
531            );
532            window_view = apply_z_offset(window_view, z_base);
533            window_view
534        })
535        .collect::<Vec<_>>();
536
537    Stack(host_mod).child((
538        content,
539        Box(Modifier::new()
540            .absolute()
541            .offset(Some(0.0), Some(0.0), Some(0.0), Some(0.0)))
542        .child(Stack(Modifier::new().fill_max_size()).with_children(window_views)),
543    ))
544}
545
546fn build_resize_handles(
547    window_id: u64,
548    start_drag: impl Fn(DragKind, PointerEvent) + Clone + 'static,
549    move_drag: impl Fn(PointerEvent) + Clone + 'static,
550    end_drag: impl Fn(PointerEvent) + Clone + 'static,
551) -> View {
552    let handles = [
553        (
554            ResizeHandle::Left,
555            handle_mod_left(),
556            CursorIcon::EwResize,
557            20,
558        ),
559        (
560            ResizeHandle::Right,
561            handle_mod_right(),
562            CursorIcon::EwResize,
563            21,
564        ),
565        (
566            ResizeHandle::Top,
567            handle_mod_top(),
568            CursorIcon::NsResize,
569            22,
570        ),
571        (
572            ResizeHandle::Bottom,
573            handle_mod_bottom(),
574            CursorIcon::NsResize,
575            23,
576        ),
577        (
578            ResizeHandle::TopLeft,
579            handle_mod_corner(true, true),
580            CursorIcon::EwResize,
581            24,
582        ),
583        (
584            ResizeHandle::TopRight,
585            handle_mod_corner(false, true),
586            CursorIcon::EwResize,
587            25,
588        ),
589        (
590            ResizeHandle::BottomLeft,
591            handle_mod_corner(true, false),
592            CursorIcon::EwResize,
593            26,
594        ),
595        (
596            ResizeHandle::BottomRight,
597            handle_mod_corner(false, false),
598            CursorIcon::EwResize,
599            27,
600        ),
601    ];
602
603    Stack(Modifier::new().fill_max_size()).with_children(
604        handles
605            .into_iter()
606            .map(|(handle, modifier, cursor, key)| {
607                Box(modifier
608                    .cursor(cursor)
609                    .on_pointer_down({
610                        let start_drag = start_drag.clone();
611                        move |pe| start_drag(DragKind::Resize(handle), pe)
612                    })
613                    .on_pointer_move(move_drag.clone())
614                    .on_pointer_up(end_drag.clone())
615                    .key(key_for(window_id, key)))
616            })
617            .collect::<Vec<_>>(),
618    )
619}
620
621fn handle_mod_left() -> Modifier {
622    Modifier::new()
623        .absolute()
624        .offset(Some(0.0), Some(0.0), None, Some(0.0))
625        .width(RESIZE_HANDLE_DP)
626}
627
628fn handle_mod_right() -> Modifier {
629    Modifier::new()
630        .absolute()
631        .offset(None, Some(0.0), Some(0.0), Some(0.0))
632        .width(RESIZE_HANDLE_DP)
633}
634
635fn handle_mod_top() -> Modifier {
636    Modifier::new()
637        .absolute()
638        .offset(Some(0.0), Some(0.0), Some(0.0), None)
639        .height(RESIZE_HANDLE_DP)
640}
641
642fn handle_mod_bottom() -> Modifier {
643    Modifier::new()
644        .absolute()
645        .offset(Some(0.0), None, Some(0.0), Some(0.0))
646        .height(RESIZE_HANDLE_DP)
647}
648
649fn handle_mod_corner(left: bool, top: bool) -> Modifier {
650    Modifier::new()
651        .absolute()
652        .offset(
653            if left { Some(0.0) } else { None },
654            if top { Some(0.0) } else { None },
655            if left { None } else { Some(0.0) },
656            if top { None } else { Some(0.0) },
657        )
658        .size(RESIZE_HANDLE_DP * 1.4, RESIZE_HANDLE_DP * 1.4)
659}
660
661fn resize_from_handle(ds: DragState, handle: ResizeHandle, delta: Vec2) -> (Vec2, Size) {
662    let mut pos = ds.start_pos;
663    let mut size = ds.start_size;
664
665    match handle {
666        ResizeHandle::Left => {
667            pos.x += delta.x;
668            size.width -= delta.x;
669        }
670        ResizeHandle::Right => {
671            size.width += delta.x;
672        }
673        ResizeHandle::Top => {
674            pos.y += delta.y;
675            size.height -= delta.y;
676        }
677        ResizeHandle::Bottom => {
678            size.height += delta.y;
679        }
680        ResizeHandle::TopLeft => {
681            pos.x += delta.x;
682            size.width -= delta.x;
683            pos.y += delta.y;
684            size.height -= delta.y;
685        }
686        ResizeHandle::TopRight => {
687            size.width += delta.x;
688            pos.y += delta.y;
689            size.height -= delta.y;
690        }
691        ResizeHandle::BottomLeft => {
692            pos.x += delta.x;
693            size.width -= delta.x;
694            size.height += delta.y;
695        }
696        ResizeHandle::BottomRight => {
697            size.width += delta.x;
698            size.height += delta.y;
699        }
700    }
701
702    (pos, size)
703}
704
705fn clamp_rect(
706    mut pos: Vec2,
707    mut size: Size,
708    min_size: Size,
709    max_size: Option<Size>,
710    bounds: Rect,
711) -> (Vec2, Size) {
712    let min_w = min_size.width.max(120.0);
713    let min_h = min_size.height.max(TITLE_BAR_HEIGHT_DP + 40.0);
714    size.width = size.width.max(min_w);
715    size.height = size.height.max(min_h);
716
717    if let Some(max) = max_size {
718        size.width = size.width.min(max.width.max(min_w));
719        size.height = size.height.min(max.height.max(min_h));
720    }
721
722    if bounds.w > 1.0 && bounds.h > 1.0 {
723        let max_w = bounds.w.max(min_w);
724        let max_h = bounds.h.max(min_h);
725        size.width = size.width.min(max_w);
726        size.height = size.height.min(max_h);
727
728        let min_x = bounds.x - size.width + KEEP_VISIBLE_DP;
729        let max_x = bounds.x + bounds.w - KEEP_VISIBLE_DP;
730        let min_y = bounds.y - size.height + KEEP_VISIBLE_DP;
731        let max_y = bounds.y + bounds.h - KEEP_VISIBLE_DP;
732
733        pos.x = clamp_f32(pos.x, min_x, max_x);
734        pos.y = clamp_f32(pos.y, min_y, max_y);
735    }
736
737    (pos, size)
738}
739
740fn clamp_f32(v: f32, min: f32, max: f32) -> f32 {
741    if max < min {
742        min
743    } else {
744        v.clamp(min, max)
745    }
746}
747
748fn apply_z_offset(mut view: View, z: f32) -> View {
749    view.modifier.z_index += z;
750    if let Some(rz) = view.modifier.render_z_index {
751        view.modifier.render_z_index = Some(rz + z);
752    }
753    view.children = view
754        .children
755        .into_iter()
756        .map(|child| apply_z_offset(child, z))
757        .collect();
758    view
759}
760
761fn inject_focus_handlers(mut view: View, focus: Rc<dyn Fn()>) -> View {
762    let needs_focus = kind_handles_hit(&view.kind) || modifier_has_hit(&view.modifier);
763    if needs_focus {
764        let existing = view.modifier.on_pointer_down.clone();
765        let focus_cb = focus.clone();
766        view.modifier.on_pointer_down = Some(Rc::new(move |pe: PointerEvent| {
767            if matches!(pe.event, PointerEventKind::Down(PointerButton::Primary)) {
768                focus_cb();
769            }
770            if let Some(cb) = existing.as_ref() {
771                cb(pe);
772            }
773        }));
774    }
775
776    view.children = view
777        .children
778        .into_iter()
779        .map(|child| inject_focus_handlers(child, focus.clone()))
780        .collect();
781    view
782}
783
784fn kind_handles_hit(kind: &ViewKind) -> bool {
785    matches!(
786        kind,
787        ViewKind::Button { .. }
788            | ViewKind::TextField { .. }
789            | ViewKind::Checkbox { .. }
790            | ViewKind::RadioButton { .. }
791            | ViewKind::Switch { .. }
792            | ViewKind::Slider { .. }
793            | ViewKind::RangeSlider { .. }
794            | ViewKind::ScrollV { .. }
795            | ViewKind::ScrollXY { .. }
796    )
797}
798
799fn modifier_has_hit(modifier: &Modifier) -> bool {
800    modifier.click
801        || modifier.on_action.is_some()
802        || modifier.on_pointer_down.is_some()
803        || modifier.on_pointer_move.is_some()
804        || modifier.on_pointer_up.is_some()
805        || modifier.on_pointer_enter.is_some()
806        || modifier.on_pointer_leave.is_some()
807        || modifier.on_drag_start.is_some()
808        || modifier.on_drag_end.is_some()
809        || modifier.on_drag_enter.is_some()
810        || modifier.on_drag_over.is_some()
811        || modifier.on_drag_leave.is_some()
812        || modifier.on_drop.is_some()
813}
814
815fn key_for(window_id: u64, part: u64) -> u64 {
816    window_id ^ (part.wrapping_mul(0x9E3779B97F4A7C15))
817}
818
819fn px_to_dp(px: f32) -> f32 {
820    let scale = repose_core::locals::density().scale * repose_core::locals::ui_scale().0;
821    if scale > 0.0001 {
822        px / scale
823    } else {
824        px
825    }
826}
827
828fn px_vec_to_dp(v: Vec2) -> Vec2 {
829    Vec2 {
830        x: px_to_dp(v.x),
831        y: px_to_dp(v.y),
832    }
833}
834
835fn rect_px_to_dp(r: Rect) -> Rect {
836    Rect {
837        x: px_to_dp(r.x),
838        y: px_to_dp(r.y),
839        w: px_to_dp(r.w),
840        h: px_to_dp(r.h),
841    }
842}