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