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