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)]
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/// 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                                focused: th.on_surface.with_alpha(16),
514                                pressed: th.on_surface.with_alpha(24),
515                                dragged: th.on_surface.with_alpha(24),
516                                disabled: Color::TRANSPARENT,
517                            })
518                            .clickable()
519                            .on_pointer_down(move |_| {
520                                focus_state.borrow_mut().bring_to_front(action_id);
521                                (on_click)();
522                                request_frame();
523                            })
524                            .z_index(1.0)
525                            .key(key_for(window_id, 60 + idx as u64)))
526                        .child(
527                            Text(label)
528                                .size(th.typography.label_medium)
529                                .color(th.primary)
530                                .single_line(),
531                        ),
532                    );
533                }
534
535                if close_enabled {
536                    let close_id = window_id;
537                    let focus_state = focus_state.clone();
538                    action_views.push(
539                        Row(Modifier::new()
540                            .width(20.0)
541                            .height(20.0)
542                            .clip_rounded(10.0)
543                            .justify_content(JustifyContent::CENTER)
544                            .align_items(AlignItems::CENTER)
545                            .state_colors(StateColors {
546                                default: th.error.with_alpha(20),
547                                hovered: th.error.with_alpha(40),
548                                focused: th.error.with_alpha(40),
549                                pressed: th.error.with_alpha(60),
550                                dragged: th.error.with_alpha(60),
551                                disabled: Color::TRANSPARENT,
552                            })
553                            .clickable()
554                            .on_pointer_down(move |_| {
555                                focus_state.borrow_mut().bring_to_front(close_id);
556                                if let Some(handler) = close_handler.as_ref() {
557                                    (handler)();
558                                } else {
559                                    close_state.borrow_mut().close(close_id);
560                                }
561                                request_frame();
562                            })
563                            .z_index(1.0)
564                            .key(key_for(window_id, 90)))
565                        .child(
566                            Text("\u{E5CD}")
567                                .font_family("Material Symbols Outlined")
568                                .size(14.0)
569                                .color(th.error),
570                        ),
571                    );
572                }
573
574                let mut bar_mod = Modifier::new()
575                    .fill_max_width()
576                    .height(TITLE_BAR_HEIGHT_DP)
577                    .background(title_bg)
578                    .padding_values(PaddingValues {
579                        left: 10.0,
580                        right: 8.0,
581                        top: 6.0,
582                        bottom: 6.0,
583                    })
584                    .align_items(AlignItems::CENTER)
585                    .key(key_for(window_id, 10));
586
587                if window_draggable {
588                    bar_mod = bar_mod
589                        .window_drag_region(&host, window_id)
590                        .cursor(title_cursor);
591                } else {
592                    bar_mod = bar_mod.on_pointer_down(move |_| bring_to_front());
593                }
594
595                let bar = Row(bar_mod).child((
596                    Text(window_title)
597                        .size(th.typography.title_small)
598                        .color(title_fg)
599                        .single_line()
600                        .overflow_ellipsize(),
601                    Spacer(),
602                    Row(Modifier::new().align_items(AlignItems::CENTER))
603                        .with_children(action_views),
604                ));
605
606                apply_z_offset(bar, chrome_z)
607            };
608
609            let content_view = {
610                let content_builder = window_content.clone();
611                let focus_cb = {
612                    let state = state.clone();
613                    let window_id = window_id;
614                    Rc::new(move || {
615                        state.borrow_mut().bring_to_front(window_id);
616                        request_frame();
617                    })
618                };
619                let inner = inject_focus_handlers((content_builder)(), focus_cb);
620                apply_z_offset(inner, content_z)
621            };
622
623            let content_shell =
624                Box(Modifier::new().fill_max_size().padding(WINDOW_PADDING_DP)).child(content_view);
625
626            let resize_handles = if window_resizable {
627                let handles = build_resize_handles(&host, window_id);
628                apply_z_offset(handles, chrome_z + 1.0)
629            } else {
630                Box(Modifier::new())
631            };
632
633            let column = Column(Modifier::new().fill_max_size()).child((title_bar, content_shell));
634
635            let mut window_view = Box(Modifier::new()
636                .key(key_for(window_id, 1))
637                .absolute()
638                .offset(Some(window_pos.x), Some(window_pos.y), None, None)
639                .size(window_size.width, window_size.height)
640                .background(th.surface_container_high)
641                .border(1.0, border_color, th.shapes.medium)
642                .clip_rounded(th.shapes.medium)
643                .z_index(-1.0)
644                .window_focus_region(&host, window_id))
645            .child(ZStack(Modifier::new().fill_max_size()).child((column, resize_handles)));
646            window_view = apply_z_offset(window_view, z_base);
647            window_view
648        })
649        .collect::<Vec<_>>();
650
651    Column(host_mod).child((
652        content,
653        Box(Modifier::new()
654            .absolute()
655            .offset(Some(0.0), Some(0.0), Some(0.0), Some(0.0)))
656        .child(Column(Modifier::new().fill_max_size()).with_children(window_views)),
657    ))
658}
659
660fn build_resize_handles(host: &WindowHostHandle, window_id: u64) -> View {
661    let handles = [
662        (ResizeHandle::Left, handle_mod_left(), 20),
663        (ResizeHandle::Right, handle_mod_right(), 21),
664        (ResizeHandle::Top, handle_mod_top(), 22),
665        (ResizeHandle::Bottom, handle_mod_bottom(), 23),
666        (ResizeHandle::TopLeft, handle_mod_corner(true, true), 24),
667        (ResizeHandle::TopRight, handle_mod_corner(false, true), 25),
668        (ResizeHandle::BottomLeft, handle_mod_corner(true, false), 26),
669        (
670            ResizeHandle::BottomRight,
671            handle_mod_corner(false, false),
672            27,
673        ),
674    ];
675
676    Column(Modifier::new().fill_max_size()).with_children(
677        handles
678            .into_iter()
679            .map(|(handle, modifier, key)| {
680                Box(modifier
681                    .window_resize_handle(host, window_id, handle)
682                    .key(key_for(window_id, key)))
683            })
684            .collect::<Vec<_>>(),
685    )
686}
687
688fn handle_mod_left() -> Modifier {
689    Modifier::new()
690        .absolute()
691        .offset(Some(0.0), Some(0.0), None, Some(0.0))
692        .width(RESIZE_HANDLE_DP)
693}
694
695fn handle_mod_right() -> Modifier {
696    Modifier::new()
697        .absolute()
698        .offset(None, Some(0.0), Some(0.0), Some(0.0))
699        .width(RESIZE_HANDLE_DP)
700}
701
702fn handle_mod_top() -> Modifier {
703    Modifier::new()
704        .absolute()
705        .offset(Some(0.0), Some(0.0), Some(0.0), None)
706        .height(RESIZE_HANDLE_DP)
707}
708
709fn handle_mod_bottom() -> Modifier {
710    Modifier::new()
711        .absolute()
712        .offset(Some(0.0), None, Some(0.0), Some(0.0))
713        .height(RESIZE_HANDLE_DP)
714}
715
716fn handle_mod_corner(left: bool, top: bool) -> Modifier {
717    Modifier::new()
718        .absolute()
719        .offset(
720            if left { Some(0.0) } else { None },
721            if top { Some(0.0) } else { None },
722            if left { None } else { Some(0.0) },
723            if top { None } else { Some(0.0) },
724        )
725        .size(RESIZE_HANDLE_DP * 1.4, RESIZE_HANDLE_DP * 1.4)
726}
727
728fn resize_from_handle(ds: DragState, handle: ResizeHandle, delta: Vec2) -> (Vec2, Size) {
729    let mut pos = ds.start_pos;
730    let mut size = ds.start_size;
731
732    match handle {
733        ResizeHandle::Left => {
734            pos.x += delta.x;
735            size.width -= delta.x;
736        }
737        ResizeHandle::Right => {
738            size.width += delta.x;
739        }
740        ResizeHandle::Top => {
741            pos.y += delta.y;
742            size.height -= delta.y;
743        }
744        ResizeHandle::Bottom => {
745            size.height += delta.y;
746        }
747        ResizeHandle::TopLeft => {
748            pos.x += delta.x;
749            size.width -= delta.x;
750            pos.y += delta.y;
751            size.height -= delta.y;
752        }
753        ResizeHandle::TopRight => {
754            size.width += delta.x;
755            pos.y += delta.y;
756            size.height -= delta.y;
757        }
758        ResizeHandle::BottomLeft => {
759            pos.x += delta.x;
760            size.width -= delta.x;
761            size.height += delta.y;
762        }
763        ResizeHandle::BottomRight => {
764            size.width += delta.x;
765            size.height += delta.y;
766        }
767    }
768
769    (pos, size)
770}
771
772fn clamp_rect(
773    mut pos: Vec2,
774    mut size: Size,
775    min_size: Size,
776    max_size: Option<Size>,
777    bounds: Rect,
778) -> (Vec2, Size) {
779    let min_w = min_size.width.max(120.0);
780    let min_h = min_size.height.max(TITLE_BAR_HEIGHT_DP + 40.0);
781    size.width = size.width.max(min_w);
782    size.height = size.height.max(min_h);
783
784    if let Some(max) = max_size {
785        size.width = size.width.min(max.width.max(min_w));
786        size.height = size.height.min(max.height.max(min_h));
787    }
788
789    if bounds.w > 1.0 && bounds.h > 1.0 {
790        let max_w = bounds.w.max(min_w);
791        let max_h = bounds.h.max(min_h);
792        size.width = size.width.min(max_w);
793        size.height = size.height.min(max_h);
794
795        let min_x = bounds.x - size.width + KEEP_VISIBLE_DP;
796        let max_x = bounds.x + bounds.w - KEEP_VISIBLE_DP;
797        let min_y = bounds.y - size.height + KEEP_VISIBLE_DP;
798        let max_y = bounds.y + bounds.h - KEEP_VISIBLE_DP;
799
800        pos.x = clamp_f32(pos.x, min_x, max_x);
801        pos.y = clamp_f32(pos.y, min_y, max_y);
802    }
803
804    (pos, size)
805}
806
807fn clamp_f32(v: f32, min: f32, max: f32) -> f32 {
808    if max < min { min } else { v.clamp(min, max) }
809}
810
811fn apply_z_offset(mut view: View, z: f32) -> View {
812    view.modifier.z_index += z;
813    if let Some(rz) = view.modifier.render_z_index {
814        view.modifier.render_z_index = Some(rz + z);
815    }
816    view.children = view
817        .children
818        .into_iter()
819        .map(|child| apply_z_offset(child, z))
820        .collect();
821    view
822}
823
824fn inject_focus_handlers(mut view: View, focus: Rc<dyn Fn()>) -> View {
825    let needs_focus = modifier_handles_hit(&view.modifier)
826        || view.modifier.text_input.is_some()
827        || modifier_has_hit(&view.modifier);
828    if needs_focus {
829        let existing = view.modifier.on_pointer_down.clone();
830        let focus_cb = focus.clone();
831        view.modifier.on_pointer_down = Some(Rc::new(move |pe: PointerEvent| {
832            if matches!(pe.event, PointerEventKind::Down(PointerButton::Primary)) {
833                focus_cb();
834            }
835            if let Some(cb) = existing.as_ref() {
836                cb(pe);
837            }
838        }));
839    }
840
841    view.children = view
842        .children
843        .into_iter()
844        .map(|child| inject_focus_handlers(child, focus.clone()))
845        .collect();
846    view
847}
848
849fn modifier_handles_hit(modifier: &Modifier) -> bool {
850    modifier.scroll.is_some()
851}
852
853fn modifier_has_hit(modifier: &Modifier) -> bool {
854    modifier.click
855        || modifier.on_action.is_some()
856        || modifier.on_pointer_down.is_some()
857        || modifier.on_pointer_move.is_some()
858        || modifier.on_pointer_up.is_some()
859        || modifier.on_pointer_enter.is_some()
860        || modifier.on_pointer_leave.is_some()
861        || modifier.on_drag_start.is_some()
862        || modifier.on_drag_end.is_some()
863        || modifier.on_drag_enter.is_some()
864        || modifier.on_drag_over.is_some()
865        || modifier.on_drag_leave.is_some()
866        || modifier.on_drop.is_some()
867}
868
869fn key_for(window_id: u64, part: u64) -> u64 {
870    window_id ^ (part.wrapping_mul(0x9E3779B97F4A7C15))
871}
872
873fn px_to_dp(px: f32) -> f32 {
874    let scale = repose_core::locals::density().scale * repose_core::locals::ui_scale().0;
875    if scale > 0.0001 { px / scale } else { px }
876}
877
878fn px_vec_to_dp(v: Vec2) -> Vec2 {
879    Vec2 {
880        x: px_to_dp(v.x),
881        y: px_to_dp(v.y),
882    }
883}
884
885fn rect_px_to_dp(r: Rect) -> Rect {
886    Rect {
887        x: px_to_dp(r.x),
888        y: px_to_dp(r.y),
889        w: px_to_dp(r.w),
890        h: px_to_dp(r.h),
891    }
892}