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_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}