Skip to main content

repose_ui/
windowing.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use repose_core::{
5    AlignItems, Color, CursorIcon, JustifyContent, Modifier, PaddingValues, PointerButton,
6    PointerEvent, PointerEventKind, Rect, Size, StateColors, Vec2, View, request_frame,
7};
8
9use crate::{Box, Column, Row, Spacer, Text, TextStyle, ViewExt, ZStack};
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, _alpha| {
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 {
264                th.focus
265            } else {
266                th.outline_variant
267            };
268            let title_fg = if is_active {
269                th.on_surface
270            } else {
271                th.on_surface_variant
272            };
273            let title_bg = if is_active {
274                th.surface_variant
275            } else {
276                th.surface
277            };
278
279            let start_drag = {
280                let drag_state = drag_state.clone();
281                let state = state.clone();
282                move |kind: DragKind, pe: PointerEvent| {
283                    if !matches!(pe.event, PointerEventKind::Down(PointerButton::Primary)) {
284                        return;
285                    }
286
287                    let (pos, size, min_size, max_size) = {
288                        let st = state.borrow();
289                        let Some(w) = st.windows.iter().find(|w| w.id == window_id) else {
290                            return;
291                        };
292                        (w.position, w.size, w.min_size, w.max_size)
293                    };
294
295                    let start = DragState {
296                        window_id,
297                        kind,
298                        start_pointer: px_vec_to_dp(pe.position),
299                        start_pos: pos,
300                        start_size: size,
301                        min_size,
302                        max_size,
303                    };
304                    *drag_state.borrow_mut() = Some(start);
305                    state.borrow_mut().bring_to_front(window_id);
306                    request_frame();
307                }
308            };
309
310            let bring_to_front = {
311                let state = state.clone();
312                move || {
313                    state.borrow_mut().bring_to_front(window_id);
314                    request_frame();
315                }
316            };
317
318            let move_drag = {
319                let drag_state = drag_state.clone();
320                let state = state.clone();
321                let bounds = bounds.clone();
322                move |pe: PointerEvent| {
323                    let Some(ds) = *drag_state.borrow() else {
324                        return;
325                    };
326                    if ds.window_id != window_id {
327                        return;
328                    }
329
330                    let cur = px_vec_to_dp(pe.position);
331                    let delta = Vec2 {
332                        x: cur.x - ds.start_pointer.x,
333                        y: cur.y - ds.start_pointer.y,
334                    };
335                    let bounds = *bounds.borrow();
336
337                    let (mut pos, mut size) = match ds.kind {
338                        DragKind::Move => (
339                            Vec2 {
340                                x: ds.start_pos.x + delta.x,
341                                y: ds.start_pos.y + delta.y,
342                            },
343                            ds.start_size,
344                        ),
345                        DragKind::Resize(handle) => resize_from_handle(ds, handle, delta),
346                    };
347
348                    let (clamped_pos, clamped_size) =
349                        clamp_rect(pos, size, ds.min_size, ds.max_size, bounds);
350                    pos = clamped_pos;
351                    size = clamped_size;
352
353                    let mut st = state.borrow_mut();
354                    st.set_position(window_id, pos);
355                    st.set_size(window_id, size);
356                    request_frame();
357                }
358            };
359
360            let end_drag = {
361                let drag_state = drag_state.clone();
362                move |_pe: PointerEvent| {
363                    *drag_state.borrow_mut() = None;
364                }
365            };
366
367            let is_dragging = drag_state
368                .borrow()
369                .as_ref()
370                .is_some_and(|d| d.window_id == window_id && d.kind == DragKind::Move);
371            let title_cursor = if is_dragging {
372                CursorIcon::Grabbing
373            } else {
374                CursorIcon::Grab
375            };
376
377            let title_bar = {
378                let window_id = window_id;
379                let actions = window_actions.clone();
380                let close_enabled = window_closable;
381                let close_state = state.clone();
382                let close_handler = window_on_close.clone();
383                let focus_state = state.clone();
384                let mut action_views = Vec::new();
385
386                for (idx, action) in actions.into_iter().enumerate() {
387                    let label = action.label.clone();
388                    let on_click = action.on_click.clone();
389                    let focus_state = focus_state.clone();
390                    let action_id = window_id;
391                    action_views.push(
392                        Row(Modifier::new()
393                            .padding_values(PaddingValues {
394                                left: 6.0,
395                                right: 6.0,
396                                top: 0.0,
397                                bottom: 0.0,
398                            })
399                            .height(20.0)
400                            .clip_rounded(10.0)
401                            .justify_content(JustifyContent::CENTER)
402                            .align_items(AlignItems::CENTER)
403                            .state_colors(StateColors {
404                                default: th.surface_variant,
405                                hovered: th.on_surface.with_alpha(16),
406                                pressed: th.on_surface.with_alpha(24),
407                                disabled: Color::TRANSPARENT,
408                            })
409                            .clickable()
410                            .on_pointer_down(move |_| {
411                                focus_state.borrow_mut().bring_to_front(action_id);
412                                (on_click)();
413                                request_frame();
414                            })
415                            .z_index(1.0)
416                            .key(key_for(window_id, 60 + idx as u64)))
417                        .child(
418                            Text(label)
419                                .size(th.typography.label_medium)
420                                .color(th.primary)
421                                .single_line(),
422                        ),
423                    );
424                }
425
426                if close_enabled {
427                    let close_id = window_id;
428                    let focus_state = focus_state.clone();
429                    action_views.push(
430                        Row(Modifier::new()
431                            .width(20.0)
432                            .height(20.0)
433                            .clip_rounded(10.0)
434                            .justify_content(JustifyContent::CENTER)
435                            .align_items(AlignItems::CENTER)
436                            .state_colors(StateColors {
437                                default: th.error.with_alpha(20),
438                                hovered: th.error.with_alpha(40),
439                                pressed: th.error.with_alpha(60),
440                                disabled: Color::TRANSPARENT,
441                            })
442                            .clickable()
443                            .on_pointer_down(move |_| {
444                                focus_state.borrow_mut().bring_to_front(close_id);
445                                if let Some(handler) = close_handler.as_ref() {
446                                    (handler)();
447                                } else {
448                                    close_state.borrow_mut().close(close_id);
449                                }
450                                request_frame();
451                            })
452                            .z_index(1.0)
453                            .key(key_for(window_id, 90)))
454                        .child(
455                            Text("\u{E5CD}")
456                                .font_family("Material Symbols Outlined")
457                                .size(14.0)
458                                .color(th.error),
459                        ),
460                    );
461                }
462
463                let mut bar_mod = Modifier::new()
464                    .fill_max_width()
465                    .height(TITLE_BAR_HEIGHT_DP)
466                    .background(title_bg)
467                    .padding_values(PaddingValues {
468                        left: 10.0,
469                        right: 8.0,
470                        top: 6.0,
471                        bottom: 6.0,
472                    })
473                    .align_items(AlignItems::CENTER)
474                    .key(key_for(window_id, 10));
475
476                if window_draggable {
477                    bar_mod = bar_mod
478                        .cursor(title_cursor)
479                        .on_pointer_down({
480                            let start_drag = start_drag.clone();
481                            move |pe| start_drag(DragKind::Move, pe)
482                        })
483                        .on_pointer_move(move_drag.clone())
484                        .on_pointer_up(end_drag.clone());
485                } else {
486                    bar_mod = bar_mod.on_pointer_down(move |_| bring_to_front());
487                }
488
489                let bar = Row(bar_mod).child((
490                    Text(window_title)
491                        .size(th.typography.title_small)
492                        .color(title_fg)
493                        .single_line()
494                        .overflow_ellipsize(),
495                    Spacer(),
496                    Row(Modifier::new().align_items(AlignItems::CENTER))
497                        .with_children(action_views),
498                ));
499
500                apply_z_offset(bar, chrome_z)
501            };
502
503            let content_view = {
504                let content_builder = window_content.clone();
505                let focus_cb = {
506                    let state = state.clone();
507                    let window_id = window_id;
508                    Rc::new(move || {
509                        state.borrow_mut().bring_to_front(window_id);
510                        request_frame();
511                    })
512                };
513                let inner = inject_focus_handlers((content_builder)(), focus_cb);
514                apply_z_offset(inner, content_z)
515            };
516
517            let content_shell =
518                Box(Modifier::new().fill_max_size().padding(WINDOW_PADDING_DP)).child(content_view);
519
520            let resize_handles = if window_resizable {
521                let handles = build_resize_handles(
522                    window_id,
523                    start_drag.clone(),
524                    move_drag.clone(),
525                    end_drag.clone(),
526                );
527                apply_z_offset(handles, chrome_z + 1.0)
528            } else {
529                Box(Modifier::new())
530            };
531
532            let column = Column(Modifier::new().fill_max_size()).child((title_bar, content_shell));
533
534            let focus_on_pointer_down = {
535                let state = state.clone();
536                move |pe: PointerEvent| {
537                    if matches!(pe.event, PointerEventKind::Down(PointerButton::Primary)) {
538                        state.borrow_mut().bring_to_front(window_id);
539                        request_frame();
540                    }
541                }
542            };
543
544            let mut window_view = Box(Modifier::new()
545                .key(key_for(window_id, 1))
546                .absolute()
547                .offset(Some(window_pos.x), Some(window_pos.y), None, None)
548                .size(window_size.width, window_size.height)
549                .background(th.surface_container_high)
550                .border(1.0, border_color, th.shapes.medium)
551                .clip_rounded(th.shapes.medium)
552                .z_index(-1.0)
553                .on_pointer_down(focus_on_pointer_down))
554            .child(ZStack(Modifier::new().fill_max_size()).child((column, resize_handles)));
555            window_view = apply_z_offset(window_view, z_base);
556            window_view
557        })
558        .collect::<Vec<_>>();
559
560    Column(host_mod).child((
561        content,
562        Box(Modifier::new()
563            .absolute()
564            .offset(Some(0.0), Some(0.0), Some(0.0), Some(0.0)))
565        .child(Column(Modifier::new().fill_max_size()).with_children(window_views)),
566    ))
567}
568
569fn build_resize_handles(
570    window_id: u64,
571    start_drag: impl Fn(DragKind, PointerEvent) + Clone + 'static,
572    move_drag: impl Fn(PointerEvent) + Clone + 'static,
573    end_drag: impl Fn(PointerEvent) + Clone + 'static,
574) -> View {
575    let handles = [
576        (
577            ResizeHandle::Left,
578            handle_mod_left(),
579            CursorIcon::EwResize,
580            20,
581        ),
582        (
583            ResizeHandle::Right,
584            handle_mod_right(),
585            CursorIcon::EwResize,
586            21,
587        ),
588        (
589            ResizeHandle::Top,
590            handle_mod_top(),
591            CursorIcon::NsResize,
592            22,
593        ),
594        (
595            ResizeHandle::Bottom,
596            handle_mod_bottom(),
597            CursorIcon::NsResize,
598            23,
599        ),
600        (
601            ResizeHandle::TopLeft,
602            handle_mod_corner(true, true),
603            CursorIcon::EwResize,
604            24,
605        ),
606        (
607            ResizeHandle::TopRight,
608            handle_mod_corner(false, true),
609            CursorIcon::EwResize,
610            25,
611        ),
612        (
613            ResizeHandle::BottomLeft,
614            handle_mod_corner(true, false),
615            CursorIcon::EwResize,
616            26,
617        ),
618        (
619            ResizeHandle::BottomRight,
620            handle_mod_corner(false, false),
621            CursorIcon::EwResize,
622            27,
623        ),
624    ];
625
626    Column(Modifier::new().fill_max_size()).with_children(
627        handles
628            .into_iter()
629            .map(|(handle, modifier, cursor, key)| {
630                Box(modifier
631                    .cursor(cursor)
632                    .on_pointer_down({
633                        let start_drag = start_drag.clone();
634                        move |pe| start_drag(DragKind::Resize(handle), pe)
635                    })
636                    .on_pointer_move(move_drag.clone())
637                    .on_pointer_up(end_drag.clone())
638                    .key(key_for(window_id, key)))
639            })
640            .collect::<Vec<_>>(),
641    )
642}
643
644fn handle_mod_left() -> Modifier {
645    Modifier::new()
646        .absolute()
647        .offset(Some(0.0), Some(0.0), None, Some(0.0))
648        .width(RESIZE_HANDLE_DP)
649}
650
651fn handle_mod_right() -> Modifier {
652    Modifier::new()
653        .absolute()
654        .offset(None, Some(0.0), Some(0.0), Some(0.0))
655        .width(RESIZE_HANDLE_DP)
656}
657
658fn handle_mod_top() -> Modifier {
659    Modifier::new()
660        .absolute()
661        .offset(Some(0.0), Some(0.0), Some(0.0), None)
662        .height(RESIZE_HANDLE_DP)
663}
664
665fn handle_mod_bottom() -> Modifier {
666    Modifier::new()
667        .absolute()
668        .offset(Some(0.0), None, Some(0.0), Some(0.0))
669        .height(RESIZE_HANDLE_DP)
670}
671
672fn handle_mod_corner(left: bool, top: bool) -> Modifier {
673    Modifier::new()
674        .absolute()
675        .offset(
676            if left { Some(0.0) } else { None },
677            if top { Some(0.0) } else { None },
678            if left { None } else { Some(0.0) },
679            if top { None } else { Some(0.0) },
680        )
681        .size(RESIZE_HANDLE_DP * 1.4, RESIZE_HANDLE_DP * 1.4)
682}
683
684fn resize_from_handle(ds: DragState, handle: ResizeHandle, delta: Vec2) -> (Vec2, Size) {
685    let mut pos = ds.start_pos;
686    let mut size = ds.start_size;
687
688    match handle {
689        ResizeHandle::Left => {
690            pos.x += delta.x;
691            size.width -= delta.x;
692        }
693        ResizeHandle::Right => {
694            size.width += delta.x;
695        }
696        ResizeHandle::Top => {
697            pos.y += delta.y;
698            size.height -= delta.y;
699        }
700        ResizeHandle::Bottom => {
701            size.height += delta.y;
702        }
703        ResizeHandle::TopLeft => {
704            pos.x += delta.x;
705            size.width -= delta.x;
706            pos.y += delta.y;
707            size.height -= delta.y;
708        }
709        ResizeHandle::TopRight => {
710            size.width += delta.x;
711            pos.y += delta.y;
712            size.height -= delta.y;
713        }
714        ResizeHandle::BottomLeft => {
715            pos.x += delta.x;
716            size.width -= delta.x;
717            size.height += delta.y;
718        }
719        ResizeHandle::BottomRight => {
720            size.width += delta.x;
721            size.height += delta.y;
722        }
723    }
724
725    (pos, size)
726}
727
728fn clamp_rect(
729    mut pos: Vec2,
730    mut size: Size,
731    min_size: Size,
732    max_size: Option<Size>,
733    bounds: Rect,
734) -> (Vec2, Size) {
735    let min_w = min_size.width.max(120.0);
736    let min_h = min_size.height.max(TITLE_BAR_HEIGHT_DP + 40.0);
737    size.width = size.width.max(min_w);
738    size.height = size.height.max(min_h);
739
740    if let Some(max) = max_size {
741        size.width = size.width.min(max.width.max(min_w));
742        size.height = size.height.min(max.height.max(min_h));
743    }
744
745    if bounds.w > 1.0 && bounds.h > 1.0 {
746        let max_w = bounds.w.max(min_w);
747        let max_h = bounds.h.max(min_h);
748        size.width = size.width.min(max_w);
749        size.height = size.height.min(max_h);
750
751        let min_x = bounds.x - size.width + KEEP_VISIBLE_DP;
752        let max_x = bounds.x + bounds.w - KEEP_VISIBLE_DP;
753        let min_y = bounds.y - size.height + KEEP_VISIBLE_DP;
754        let max_y = bounds.y + bounds.h - KEEP_VISIBLE_DP;
755
756        pos.x = clamp_f32(pos.x, min_x, max_x);
757        pos.y = clamp_f32(pos.y, min_y, max_y);
758    }
759
760    (pos, size)
761}
762
763fn clamp_f32(v: f32, min: f32, max: f32) -> f32 {
764    if max < min { min } else { v.clamp(min, max) }
765}
766
767fn apply_z_offset(mut view: View, z: f32) -> View {
768    view.modifier.z_index += z;
769    if let Some(rz) = view.modifier.render_z_index {
770        view.modifier.render_z_index = Some(rz + z);
771    }
772    view.children = view
773        .children
774        .into_iter()
775        .map(|child| apply_z_offset(child, z))
776        .collect();
777    view
778}
779
780fn inject_focus_handlers(mut view: View, focus: Rc<dyn Fn()>) -> View {
781    let needs_focus = modifier_handles_hit(&view.modifier)
782        || view.modifier.text_input.is_some()
783        || modifier_has_hit(&view.modifier);
784    if needs_focus {
785        let existing = view.modifier.on_pointer_down.clone();
786        let focus_cb = focus.clone();
787        view.modifier.on_pointer_down = Some(Rc::new(move |pe: PointerEvent| {
788            if matches!(pe.event, PointerEventKind::Down(PointerButton::Primary)) {
789                focus_cb();
790            }
791            if let Some(cb) = existing.as_ref() {
792                cb(pe);
793            }
794        }));
795    }
796
797    view.children = view
798        .children
799        .into_iter()
800        .map(|child| inject_focus_handlers(child, focus.clone()))
801        .collect();
802    view
803}
804
805fn modifier_handles_hit(modifier: &Modifier) -> bool {
806    modifier.scroll.is_some()
807}
808
809fn modifier_has_hit(modifier: &Modifier) -> bool {
810    modifier.click
811        || modifier.on_action.is_some()
812        || modifier.on_pointer_down.is_some()
813        || modifier.on_pointer_move.is_some()
814        || modifier.on_pointer_up.is_some()
815        || modifier.on_pointer_enter.is_some()
816        || modifier.on_pointer_leave.is_some()
817        || modifier.on_drag_start.is_some()
818        || modifier.on_drag_end.is_some()
819        || modifier.on_drag_enter.is_some()
820        || modifier.on_drag_over.is_some()
821        || modifier.on_drag_leave.is_some()
822        || modifier.on_drop.is_some()
823}
824
825fn key_for(window_id: u64, part: u64) -> u64 {
826    window_id ^ (part.wrapping_mul(0x9E3779B97F4A7C15))
827}
828
829fn px_to_dp(px: f32) -> f32 {
830    let scale = repose_core::locals::density().scale * repose_core::locals::ui_scale().0;
831    if scale > 0.0001 { px / scale } else { px }
832}
833
834fn px_vec_to_dp(v: Vec2) -> Vec2 {
835    Vec2 {
836        x: px_to_dp(v.x),
837        y: px_to_dp(v.y),
838    }
839}
840
841fn rect_px_to_dp(r: Rect) -> Rect {
842    Rect {
843        x: px_to_dp(r.x),
844        y: px_to_dp(r.y),
845        w: px_to_dp(r.w),
846        h: px_to_dp(r.h),
847    }
848}