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 pub position: Vec2,
32 pub size: Size,
34 pub min_size: Size,
36 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}