Skip to main content

repose_ui/
windowing.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::rc::Rc;
4
5use repose_core::{
6    AlignItems, Color, CursorIcon, Dp, DpOffset, DpSize, JustifyContent, Modifier,
7    MutableInteractionSource, PaddingValues, PointerButton, PointerEvent, PointerEventKind, Px,
8    Rect, Size, Sp, StateColors, Vec2, View, request_frame,
9};
10
11use crate::{Box, Column, Row, Spacer, Text, TextStyle, ViewExt, ZStack};
12
13const TITLE_BAR_HEIGHT: Dp = Dp(32.0);
14const WINDOW_PADDING: Dp = Dp(8.0);
15const RESIZE_HANDLE: Dp = Dp(10.0);
16const WINDOW_Z_BASE: f32 = 10_000.0;
17const WINDOW_Z_STEP: f32 = 10.0;
18const KEEP_VISIBLE_DP: f32 = 24.0;
19
20#[derive(Clone)]
21pub struct WindowAction {
22    pub label: String,
23    pub on_click: Rc<dyn Fn()>,
24}
25
26#[derive(Clone)]
27pub struct FloatingWindow {
28    pub id: u64,
29    pub title: String,
30    pub content: Rc<dyn Fn() -> View>,
31    pub on_close: Option<Rc<dyn Fn()>>,
32    /// Position in [`Dp`] from the host's top-left corner.
33    pub position: DpOffset,
34    /// Size in [`Dp`].
35    pub size: DpSize,
36    /// Minimum size in [`Dp`].
37    pub min_size: DpSize,
38    /// Maximum size in [`Dp`] (optional).
39    pub max_size: Option<DpSize>,
40    pub resizable: bool,
41    pub closable: bool,
42    pub draggable: bool,
43    pub actions: Vec<WindowAction>,
44}
45
46impl FloatingWindow {
47    pub fn new(id: u64, title: impl Into<String>, content: Rc<dyn Fn() -> View>) -> Self {
48        Self {
49            id,
50            title: title.into(),
51            content,
52            on_close: None,
53            position: DpOffset::new(Dp(40.0), Dp(40.0)),
54            size: DpSize::new(Dp(420.0), Dp(300.0)),
55            min_size: DpSize::new(Dp(220.0), Dp(160.0)),
56            max_size: None,
57            resizable: true,
58            closable: true,
59            draggable: true,
60            actions: Vec::new(),
61        }
62    }
63
64    pub fn position(mut self, x: Dp, y: Dp) -> Self {
65        self.position = DpOffset::new(x, y);
66        self
67    }
68
69    pub fn size(mut self, width: Dp, height: Dp) -> Self {
70        self.size = DpSize::new(width, height);
71        self
72    }
73
74    pub fn min_size(mut self, width: Dp, height: Dp) -> Self {
75        self.min_size = DpSize::new(width, height);
76        self
77    }
78
79    pub fn max_size(mut self, width: Dp, height: Dp) -> Self {
80        self.max_size = Some(DpSize::new(width, height));
81        self
82    }
83
84    pub fn resizable(mut self, resizable: bool) -> Self {
85        self.resizable = resizable;
86        self
87    }
88
89    pub fn closable(mut self, closable: bool) -> Self {
90        self.closable = closable;
91        self
92    }
93
94    pub fn draggable(mut self, draggable: bool) -> Self {
95        self.draggable = draggable;
96        self
97    }
98
99    pub fn actions(mut self, actions: Vec<WindowAction>) -> Self {
100        self.actions = actions;
101        self
102    }
103
104    pub fn on_close(mut self, on_close: Rc<dyn Fn()>) -> Self {
105        self.on_close = Some(on_close);
106        self
107    }
108}
109
110#[derive(Clone, Default)]
111pub struct WindowManagerState {
112    pub windows: Vec<FloatingWindow>,
113    next_id: u64,
114    pub active: Option<u64>,
115    window_index: HashMap<u64, usize>,
116}
117
118impl WindowManagerState {
119    pub fn new() -> Self {
120        Self {
121            windows: Vec::new(),
122            next_id: 1,
123            active: None,
124            window_index: HashMap::new(),
125        }
126    }
127
128    pub fn alloc_id(&mut self) -> u64 {
129        let id = self.next_id;
130        self.next_id += 1;
131        id
132    }
133
134    fn reindex_windows(&mut self) {
135        self.window_index.clear();
136        self.window_index.extend(
137            self.windows
138                .iter()
139                .enumerate()
140                .map(|(index, window)| (window.id, index)),
141        );
142    }
143
144    fn window_index(&mut self, id: u64) -> Option<usize> {
145        if let Some(&index) = self.window_index.get(&id)
146            && self
147                .windows
148                .get(index)
149                .is_some_and(|window| window.id == id)
150        {
151            return Some(index);
152        }
153        let index = self.windows.iter().position(|window| window.id == id)?;
154        self.reindex_windows();
155        Some(index)
156    }
157
158    pub fn open(&mut self, window: FloatingWindow) {
159        let window_id = window.id;
160        if let Some(pos) = self.window_index(window_id) {
161            self.windows[pos] = window;
162        } else {
163            self.windows.push(window);
164            self.reindex_windows();
165        }
166        self.bring_to_front(window_id);
167    }
168
169    pub fn close(&mut self, id: u64) -> bool {
170        let Some(idx) = self.window_index(id) else {
171            return false;
172        };
173        self.windows.remove(idx);
174        self.reindex_windows();
175        if self.active == Some(id) {
176            self.active = self.windows.last().map(|w| w.id);
177        }
178        true
179    }
180
181    pub fn bring_to_front(&mut self, id: u64) -> bool {
182        let Some(idx) = self.window_index(id) else {
183            return false;
184        };
185        let window = self.windows.remove(idx);
186        self.windows.push(window);
187        self.active = Some(id);
188        self.reindex_windows();
189        true
190    }
191
192    pub fn set_position(&mut self, id: u64, position: DpOffset) -> bool {
193        let Some(index) = self.window_index(id) else {
194            return false;
195        };
196        self.windows[index].position = position;
197        true
198    }
199
200    pub fn set_size(&mut self, id: u64, size: DpSize) -> bool {
201        let Some(index) = self.window_index(id) else {
202            return false;
203        };
204        self.windows[index].size = size;
205        true
206    }
207}
208
209#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210pub enum ResizeHandle {
211    Left,
212    Right,
213    Top,
214    Bottom,
215    TopLeft,
216    TopRight,
217    BottomLeft,
218    BottomRight,
219}
220
221#[derive(Clone, Copy, Debug, PartialEq, Eq)]
222enum DragKind {
223    Move,
224    Resize(ResizeHandle),
225}
226
227#[derive(Clone, Copy, Debug)]
228pub(crate) struct DragState {
229    window_id: u64,
230    kind: DragKind,
231    start_pointer: Vec2,
232    start_pos: Vec2,
233    start_size: Size,
234    min_size: Size,
235    max_size: Option<Size>,
236}
237
238/// Ephemeral, reusable window behavior handle. Created inside [`WindowHost`] and
239/// passed to the [`WindowModifierExt`] helpers so custom chrome can reuse the
240/// exact same window behavior.
241#[derive(Clone)]
242pub struct WindowHostHandle {
243    pub(crate) state: Rc<RefCell<WindowManagerState>>,
244    pub(crate) bounds: Rc<RefCell<Rect>>,
245    pub(crate) drag_state: Rc<RefCell<Option<DragState>>>,
246}
247
248fn cursor_for_resize_handle(handle: ResizeHandle) -> CursorIcon {
249    use ResizeHandle::*;
250    match handle {
251        Top | Bottom => CursorIcon::NsResize,
252        Left | Right => CursorIcon::EwResize,
253        TopLeft | BottomRight => CursorIcon::NwseResize,
254        TopRight | BottomLeft => CursorIcon::NeswResize,
255    }
256}
257
258/// Modular window behavior modifiers. Lets custom chrome reuse window behavior:
259///
260/// ```ignore
261/// use repose_ui::windowing::{WindowModifierExt, WindowHostHandle};
262/// Row(Modifier::new().window_drag_region(&host, window_id)).child(...)
263/// ```
264pub trait WindowModifierExt: Sized {
265    /// Bring this window to the front when the region is pressed.
266    fn window_focus_region(self, host: &WindowHostHandle, window_id: u64) -> Modifier;
267
268    /// Make this node a drag region that moves the window.
269    fn window_drag_region(self, host: &WindowHostHandle, window_id: u64) -> Modifier;
270
271    /// Make this node a resize handle for the given edge/corner.
272    fn window_resize_handle(
273        self,
274        host: &WindowHostHandle,
275        window_id: u64,
276        handle: ResizeHandle,
277    ) -> Modifier;
278
279    /// Shared "while dragging" continuation: move + end. Used by both the drag
280    /// region and resize handles.
281    fn window_drag_continuation(self, host: &WindowHostHandle, window_id: u64) -> Modifier;
282}
283
284impl WindowModifierExt for Modifier {
285    fn window_focus_region(self, host: &WindowHostHandle, window_id: u64) -> Modifier {
286        let state = host.state.clone();
287
288        self.on_pointer_down(move |pe: PointerEvent| {
289            if matches!(pe.event, PointerEventKind::Down(PointerButton::Primary)) {
290                state.borrow_mut().bring_to_front(window_id);
291                request_frame();
292            }
293        })
294    }
295
296    fn window_drag_region(self, host: &WindowHostHandle, window_id: u64) -> Modifier {
297        let drag_state_down = host.drag_state.clone();
298        let state_down = host.state.clone();
299
300        self.cursor(CursorIcon::Grab)
301            .on_pointer_down(move |pe: PointerEvent| {
302                if !matches!(pe.event, PointerEventKind::Down(PointerButton::Primary)) {
303                    return;
304                }
305
306                let (pos, size, min_size, max_size) = {
307                    let st = state_down.borrow();
308                    let Some(w) = st.windows.iter().find(|w| w.id == window_id) else {
309                        return;
310                    };
311                    (w.position, w.size, w.min_size, w.max_size)
312                };
313
314                if drag_state_down.borrow().is_some() {
315                    return;
316                }
317                *drag_state_down.borrow_mut() = Some(DragState {
318                    window_id,
319                    kind: DragKind::Move,
320                    start_pointer: px_vec_to_dp(pe.position_in_window()),
321                    start_pos: Vec2 {
322                        x: pos.x.0,
323                        y: pos.y.0,
324                    },
325                    start_size: Size {
326                        width: size.width.0,
327                        height: size.height.0,
328                    },
329                    min_size: Size {
330                        width: min_size.width.0,
331                        height: min_size.height.0,
332                    },
333                    max_size: max_size.map(|s| Size {
334                        width: s.width.0,
335                        height: s.height.0,
336                    }),
337                });
338
339                state_down.borrow_mut().bring_to_front(window_id);
340                pe.consume();
341                request_frame();
342            })
343            .window_drag_continuation(host, window_id)
344    }
345
346    fn window_resize_handle(
347        self,
348        host: &WindowHostHandle,
349        window_id: u64,
350        handle: ResizeHandle,
351    ) -> Modifier {
352        let drag_state_down = host.drag_state.clone();
353        let state_down = host.state.clone();
354
355        self.cursor(cursor_for_resize_handle(handle))
356            .on_pointer_down(move |pe: PointerEvent| {
357                if !matches!(pe.event, PointerEventKind::Down(PointerButton::Primary)) {
358                    return;
359                }
360
361                let (pos, size, min_size, max_size) = {
362                    let st = state_down.borrow();
363                    let Some(w) = st.windows.iter().find(|w| w.id == window_id) else {
364                        return;
365                    };
366                    (w.position, w.size, w.min_size, w.max_size)
367                };
368
369                if drag_state_down.borrow().is_some() {
370                    return;
371                }
372                *drag_state_down.borrow_mut() = Some(DragState {
373                    window_id,
374                    kind: DragKind::Resize(handle),
375                    start_pointer: px_vec_to_dp(pe.position_in_window()),
376                    start_pos: Vec2 {
377                        x: pos.x.0,
378                        y: pos.y.0,
379                    },
380                    start_size: Size {
381                        width: size.width.0,
382                        height: size.height.0,
383                    },
384                    min_size: Size {
385                        width: min_size.width.0,
386                        height: min_size.height.0,
387                    },
388                    max_size: max_size.map(|s| Size {
389                        width: s.width.0,
390                        height: s.height.0,
391                    }),
392                });
393
394                state_down.borrow_mut().bring_to_front(window_id);
395                pe.consume();
396                request_frame();
397            })
398            .window_drag_continuation(host, window_id)
399    }
400
401    fn window_drag_continuation(self, host: &WindowHostHandle, window_id: u64) -> Modifier {
402        let drag_state_move = host.drag_state.clone();
403        let state_move = host.state.clone();
404        let bounds_move = host.bounds.clone();
405
406        let drag_state_up = host.drag_state.clone();
407        let drag_state_cancel = host.drag_state.clone();
408
409        self.on_pointer_move(move |pe: PointerEvent| {
410            let Some(ds) = *drag_state_move.borrow() else {
411                return;
412            };
413            if ds.window_id != window_id {
414                return;
415            }
416
417            let cur = px_vec_to_dp(pe.position_in_window());
418            let delta = Vec2 {
419                x: cur.x - ds.start_pointer.x,
420                y: cur.y - ds.start_pointer.y,
421            };
422
423            let bounds = *bounds_move.borrow();
424            let (pos, size) = apply_drag(ds, delta, bounds);
425
426            let mut st = state_move.borrow_mut();
427            st.set_position(window_id, DpOffset::new(Dp(pos.x), Dp(pos.y)));
428            st.set_size(window_id, DpSize::new(Dp(size.width), Dp(size.height)));
429            pe.consume();
430            request_frame();
431        })
432        .on_pointer_up(move |pe: PointerEvent| {
433            let clearing = drag_state_up
434                .borrow()
435                .as_ref()
436                .is_some_and(|d| d.window_id == window_id);
437            if clearing {
438                *drag_state_up.borrow_mut() = None;
439                pe.consume();
440                request_frame();
441            }
442        })
443        .on_pointer_cancel(move |_pe: PointerEvent| {
444            let clearing = drag_state_cancel
445                .borrow()
446                .as_ref()
447                .is_some_and(|d| d.window_id == window_id);
448            if clearing {
449                *drag_state_cancel.borrow_mut() = None;
450                request_frame();
451            }
452        })
453    }
454}
455
456pub fn WindowHost(
457    key: impl Into<String>,
458    modifier: Modifier,
459    state: Rc<RefCell<WindowManagerState>>,
460    content: View,
461) -> View {
462    let key = key.into();
463    let bounds = repose_core::remember_with_key(format!("window:bounds:{key}"), || {
464        RefCell::new(Rect::default())
465    });
466    let drag_state = repose_core::remember_with_key(format!("window:drag:{key}"), || {
467        RefCell::new(None::<DragState>)
468    });
469
470    let host = WindowHostHandle {
471        state: state.clone(),
472        bounds: bounds.clone(),
473        drag_state: drag_state.clone(),
474    };
475
476    let bounds_capture = bounds.clone();
477    let host_mod = modifier.painter(move |_scene, rect_px, _alpha| {
478        let mut bounds_dp = rect_px_to_dp(rect_px);
479        bounds_dp.x = 0.0;
480        bounds_dp.y = 0.0;
481        *bounds_capture.borrow_mut() = bounds_dp;
482    });
483
484    let active_id = state.borrow().active;
485    let windows = state.borrow().windows.clone();
486
487    let window_views = windows
488        .into_iter()
489        .enumerate()
490        .map(|(idx, window)| {
491            let z_base = WINDOW_Z_BASE + (idx as f32 * WINDOW_Z_STEP);
492            let chrome_z = 2.0;
493            let content_z = 1.0;
494
495            let window_id = window.id;
496            let window_actions = window.actions.clone();
497            let window_closable = window.closable;
498            let window_on_close = window.on_close.clone();
499            let window_content = window.content.clone();
500            let window_pos = window.position;
501            let window_size = window.size;
502            let window_title = window.title.clone();
503            let window_draggable = window.draggable;
504            let window_resizable = window.resizable;
505
506            let is_active = active_id == Some(window_id);
507            let th = repose_core::locals::theme();
508            let border_color = if is_active {
509                th.focus
510            } else {
511                th.outline_variant
512            };
513            let title_fg = if is_active {
514                th.on_surface
515            } else {
516                th.on_surface_variant
517            };
518            let title_bg = if is_active {
519                th.surface_variant
520            } else {
521                th.surface
522            };
523
524            let bring_to_front = {
525                let state = state.clone();
526                move || {
527                    state.borrow_mut().bring_to_front(window_id);
528                    request_frame();
529                }
530            };
531
532            let is_dragging = host
533                .drag_state
534                .borrow()
535                .as_ref()
536                .is_some_and(|d| d.window_id == window_id && d.kind == DragKind::Move);
537            let title_cursor = if is_dragging {
538                CursorIcon::Grabbing
539            } else {
540                CursorIcon::Grab
541            };
542
543            let title_bar = {
544                let actions = window_actions.clone();
545                let close_enabled = window_closable;
546                let close_state = state.clone();
547                let close_handler = window_on_close.clone();
548                let focus_state = state.clone();
549                let mut action_views = Vec::new();
550
551                for (idx, action) in actions.into_iter().enumerate() {
552                    let label = action.label.clone();
553                    let on_click = action.on_click.clone();
554                    let focus_state = focus_state.clone();
555                    let action_id = window_id;
556                    let action_src: Rc<MutableInteractionSource> = repose_core::remember_with_key(
557                        format!("window:action_src:{window_id}:{idx}"),
558                        MutableInteractionSource::new,
559                    );
560                    action_views.push(
561                        Row(Modifier::new()
562                            .padding_values(PaddingValues {
563                                left: Dp(6.0),
564                                right: Dp(6.0),
565                                top: Dp::ZERO,
566                                bottom: Dp::ZERO,
567                            })
568                            .height(Dp(20.0))
569                            .clip_rounded(Dp(10.0))
570                            .justify_content(JustifyContent::CENTER)
571                            .align_items(AlignItems::CENTER)
572                            .interaction_source(&action_src)
573                            .state_colors(StateColors {
574                                default: th.surface_variant,
575                                hovered: th.on_surface.with_alpha(16),
576                                focused: th.on_surface.with_alpha(16),
577                                pressed: th.on_surface.with_alpha(24),
578                                dragged: th.on_surface.with_alpha(24),
579                                disabled: Color::TRANSPARENT,
580                            })
581                            .clickable()
582                            .on_pointer_down(move |_| {
583                                focus_state.borrow_mut().bring_to_front(action_id);
584                                (on_click)();
585                                request_frame();
586                            })
587                            .z_index(1.0)
588                            .key(key_for(window_id, 60 + idx as u64)))
589                        .child(
590                            Text(label)
591                                .size(th.typography.label_medium)
592                                .color(th.primary)
593                                .single_line(),
594                        ),
595                    );
596                }
597
598                if close_enabled {
599                    let close_id = window_id;
600                    let focus_state = focus_state.clone();
601                    let close_src: Rc<MutableInteractionSource> = repose_core::remember_with_key(
602                        format!("window:close_src:{window_id}"),
603                        MutableInteractionSource::new,
604                    );
605                    action_views.push(
606                        Row(Modifier::new()
607                            .width(Dp(20.0))
608                            .height(Dp(20.0))
609                            .clip_rounded(Dp(10.0))
610                            .justify_content(JustifyContent::CENTER)
611                            .align_items(AlignItems::CENTER)
612                            .interaction_source(&close_src)
613                            .state_colors(StateColors {
614                                default: th.error.with_alpha(20),
615                                hovered: th.error.with_alpha(40),
616                                focused: th.error.with_alpha(40),
617                                pressed: th.error.with_alpha(60),
618                                dragged: th.error.with_alpha(60),
619                                disabled: Color::TRANSPARENT,
620                            })
621                            .clickable()
622                            .on_pointer_down(move |_| {
623                                focus_state.borrow_mut().bring_to_front(close_id);
624                                if let Some(handler) = close_handler.as_ref() {
625                                    (handler)();
626                                } else {
627                                    close_state.borrow_mut().close(close_id);
628                                }
629                                request_frame();
630                            })
631                            .z_index(1.0)
632                            .key(key_for(window_id, 90)))
633                        .child(
634                            Text("\u{E5CD}")
635                                .font_family("Material Symbols Outlined")
636                                .size(Sp(14.0))
637                                .color(th.error),
638                        ),
639                    );
640                }
641
642                let mut bar_mod = Modifier::new()
643                    .fill_max_width()
644                    .height(TITLE_BAR_HEIGHT)
645                    .background(title_bg)
646                    .padding_values(PaddingValues {
647                        left: Dp(10.0),
648                        right: Dp(8.0),
649                        top: Dp(6.0),
650                        bottom: Dp(6.0),
651                    })
652                    .align_items(AlignItems::CENTER)
653                    .key(key_for(window_id, 10));
654
655                if window_draggable {
656                    bar_mod = bar_mod
657                        .window_drag_region(&host, window_id)
658                        .cursor(title_cursor);
659                } else {
660                    bar_mod = bar_mod.on_pointer_down(move |_| bring_to_front());
661                }
662
663                let bar = Row(bar_mod).child((
664                    Text(window_title)
665                        .size(th.typography.title_small)
666                        .color(title_fg)
667                        .single_line()
668                        .overflow_ellipsize(),
669                    Spacer(),
670                    Row(Modifier::new().align_items(AlignItems::CENTER))
671                        .with_children(action_views),
672                ));
673
674                apply_z_offset(bar, chrome_z)
675            };
676
677            let content_view = {
678                let content_builder = window_content.clone();
679                let focus_cb = {
680                    let state = state.clone();
681                    Rc::new(move || {
682                        state.borrow_mut().bring_to_front(window_id);
683                        request_frame();
684                    })
685                };
686                let inner = inject_focus_handlers((content_builder)(), focus_cb);
687                apply_z_offset(inner, content_z)
688            };
689
690            let content_shell =
691                Box(Modifier::new().fill_max_size().padding(WINDOW_PADDING)).child(content_view);
692
693            let resize_handles = if window_resizable {
694                let handles = build_resize_handles(&host, window_id);
695                apply_z_offset(handles, chrome_z + 1.0)
696            } else {
697                Box(Modifier::new())
698            };
699
700            let column = Column(Modifier::new().fill_max_size()).child((title_bar, content_shell));
701
702            let mut window_view = Box(Modifier::new()
703                .key(key_for(window_id, 1))
704                .absolute()
705                .offset(Some(window_pos.x), Some(window_pos.y), None, None)
706                .size(window_size.width, window_size.height)
707                .background(th.surface_container_high)
708                .border(Dp(1.0), border_color, th.shapes.medium)
709                .clip_rounded(th.shapes.medium)
710                .z_index(-1.0)
711                .window_focus_region(&host, window_id))
712            .child(ZStack(Modifier::new().fill_max_size()).child((column, resize_handles)));
713            window_view = apply_z_offset(window_view, z_base);
714            window_view
715        })
716        .collect::<Vec<_>>();
717
718    Column(host_mod).child((
719        content,
720        Box(Modifier::new().absolute().offset(
721            Some(Dp::ZERO),
722            Some(Dp::ZERO),
723            Some(Dp::ZERO),
724            Some(Dp::ZERO),
725        ))
726        .child(Column(Modifier::new().fill_max_size()).with_children(window_views)),
727    ))
728}
729
730fn build_resize_handles(host: &WindowHostHandle, window_id: u64) -> View {
731    let handles = [
732        (ResizeHandle::Left, handle_mod_left(), 20),
733        (ResizeHandle::Right, handle_mod_right(), 21),
734        (ResizeHandle::Top, handle_mod_top(), 22),
735        (ResizeHandle::Bottom, handle_mod_bottom(), 23),
736        (ResizeHandle::TopLeft, handle_mod_corner(true, true), 24),
737        (ResizeHandle::TopRight, handle_mod_corner(false, true), 25),
738        (ResizeHandle::BottomLeft, handle_mod_corner(true, false), 26),
739        (
740            ResizeHandle::BottomRight,
741            handle_mod_corner(false, false),
742            27,
743        ),
744    ];
745
746    Column(Modifier::new().fill_max_size()).with_children(
747        handles
748            .into_iter()
749            .map(|(handle, modifier, key)| {
750                Box(modifier
751                    .window_resize_handle(host, window_id, handle)
752                    .key(key_for(window_id, key)))
753            })
754            .collect::<Vec<_>>(),
755    )
756}
757
758fn handle_mod_left() -> Modifier {
759    Modifier::new()
760        .absolute()
761        .offset(Some(Dp::ZERO), Some(Dp::ZERO), None, Some(Dp::ZERO))
762        .width(RESIZE_HANDLE)
763}
764
765fn handle_mod_right() -> Modifier {
766    Modifier::new()
767        .absolute()
768        .offset(None, Some(Dp::ZERO), Some(Dp::ZERO), Some(Dp::ZERO))
769        .width(RESIZE_HANDLE)
770}
771
772fn handle_mod_top() -> Modifier {
773    Modifier::new()
774        .absolute()
775        .offset(Some(Dp::ZERO), Some(Dp::ZERO), Some(Dp::ZERO), None)
776        .height(RESIZE_HANDLE)
777}
778
779fn handle_mod_bottom() -> Modifier {
780    Modifier::new()
781        .absolute()
782        .offset(Some(Dp::ZERO), None, Some(Dp::ZERO), Some(Dp::ZERO))
783        .height(RESIZE_HANDLE)
784}
785
786fn handle_mod_corner(left: bool, top: bool) -> Modifier {
787    Modifier::new()
788        .absolute()
789        .offset(
790            if left { Some(Dp::ZERO) } else { None },
791            if top { Some(Dp::ZERO) } else { None },
792            if left { None } else { Some(Dp::ZERO) },
793            if top { None } else { Some(Dp::ZERO) },
794        )
795        .size(RESIZE_HANDLE * 1.4, RESIZE_HANDLE * 1.4)
796}
797
798fn resize_from_handle(ds: DragState, handle: ResizeHandle, delta: Vec2) -> (Vec2, Size) {
799    let mut pos = ds.start_pos;
800    let mut size = ds.start_size;
801
802    match handle {
803        ResizeHandle::Left => {
804            pos.x += delta.x;
805            size.width -= delta.x;
806        }
807        ResizeHandle::Right => {
808            size.width += delta.x;
809        }
810        ResizeHandle::Top => {
811            pos.y += delta.y;
812            size.height -= delta.y;
813        }
814        ResizeHandle::Bottom => {
815            size.height += delta.y;
816        }
817        ResizeHandle::TopLeft => {
818            pos.x += delta.x;
819            size.width -= delta.x;
820            pos.y += delta.y;
821            size.height -= delta.y;
822        }
823        ResizeHandle::TopRight => {
824            size.width += delta.x;
825            pos.y += delta.y;
826            size.height -= delta.y;
827        }
828        ResizeHandle::BottomLeft => {
829            pos.x += delta.x;
830            size.width -= delta.x;
831            size.height += delta.y;
832        }
833        ResizeHandle::BottomRight => {
834            size.width += delta.x;
835            size.height += delta.y;
836        }
837    }
838
839    (pos, size)
840}
841
842fn apply_drag(ds: DragState, delta: Vec2, bounds: Rect) -> (Vec2, Size) {
843    let (mut pos, mut size) = match ds.kind {
844        DragKind::Move => (
845            Vec2 {
846                x: ds.start_pos.x + delta.x,
847                y: ds.start_pos.y + delta.y,
848            },
849            ds.start_size,
850        ),
851        DragKind::Resize(handle) => resize_from_handle(ds, handle, delta),
852    };
853
854    let min_w = ds.min_size.width.max(120.0);
855    let min_h = ds.min_size.height.max(TITLE_BAR_HEIGHT.0 + 40.0);
856
857    let mut max_w = f32::INFINITY;
858    let mut max_h = f32::INFINITY;
859    if let Some(max) = ds.max_size {
860        max_w = max.width.max(min_w);
861        max_h = max.height.max(min_h);
862    }
863    if bounds.w > 1.0 && bounds.h > 1.0 {
864        max_w = max_w.min(bounds.w.max(min_w));
865        max_h = max_h.min(bounds.h.max(min_h));
866    }
867
868    let right = pos.x + size.width;
869    let bottom = pos.y + size.height;
870
871    size.width = size.width.clamp(min_w, max_w);
872    size.height = size.height.clamp(min_h, max_h);
873
874    if let DragKind::Resize(handle) = ds.kind {
875        let affects_left = matches!(
876            handle,
877            ResizeHandle::Left | ResizeHandle::TopLeft | ResizeHandle::BottomLeft
878        );
879        let affects_top = matches!(
880            handle,
881            ResizeHandle::Top | ResizeHandle::TopLeft | ResizeHandle::TopRight
882        );
883        if affects_left {
884            pos.x = right - size.width;
885        }
886        if affects_top {
887            pos.y = bottom - size.height;
888        }
889    }
890
891    if bounds.w > 1.0 && bounds.h > 1.0 {
892        let min_x = bounds.x - size.width + KEEP_VISIBLE_DP;
893        let max_x = bounds.x + bounds.w - KEEP_VISIBLE_DP;
894        let min_y = bounds.y - size.height + KEEP_VISIBLE_DP;
895        let max_y = bounds.y + bounds.h - KEEP_VISIBLE_DP;
896        pos.x = clamp_f32(pos.x, min_x, max_x);
897        pos.y = clamp_f32(pos.y, min_y, max_y);
898    }
899
900    (pos, size)
901}
902
903fn clamp_f32(v: f32, min: f32, max: f32) -> f32 {
904    if max < min { min } else { v.clamp(min, max) }
905}
906
907fn apply_z_offset(mut view: View, z: f32) -> View {
908    view.modifier.z_index += z;
909    if let Some(rz) = view.modifier.render_z_index {
910        view.modifier.render_z_index = Some(rz + z);
911    }
912    view.children = view
913        .children
914        .into_iter()
915        .map(|child| apply_z_offset(child, z))
916        .collect();
917    view
918}
919
920fn inject_focus_handlers(mut view: View, focus: Rc<dyn Fn()>) -> View {
921    let needs_focus = modifier_handles_hit(&view.modifier)
922        || view.modifier.text_input.is_some()
923        || modifier_has_hit(&view.modifier);
924    if needs_focus {
925        let existing = view.modifier.on_pointer_down.clone();
926        let focus_cb = focus.clone();
927        view.modifier.on_pointer_down = Some(Rc::new(move |pe: PointerEvent| {
928            if matches!(pe.event, PointerEventKind::Down(PointerButton::Primary)) {
929                focus_cb();
930            }
931            if let Some(cb) = existing.as_ref() {
932                cb(pe);
933            }
934        }));
935    }
936
937    view.children = view
938        .children
939        .into_iter()
940        .map(|child| inject_focus_handlers(child, focus.clone()))
941        .collect();
942    view
943}
944
945fn modifier_handles_hit(modifier: &Modifier) -> bool {
946    modifier.scroll.is_some()
947}
948
949fn modifier_has_hit(modifier: &Modifier) -> bool {
950    modifier.click
951        || modifier.on_action.is_some()
952        || modifier.on_pointer_down.is_some()
953        || modifier.on_pointer_move.is_some()
954        || modifier.on_pointer_up.is_some()
955        || modifier.on_pointer_cancel.is_some()
956        || modifier.on_pointer_enter.is_some()
957        || modifier.on_pointer_leave.is_some()
958        || modifier.on_drag_start.is_some()
959        || modifier.on_drag_end.is_some()
960        || modifier.on_drag_enter.is_some()
961        || modifier.on_drag_over.is_some()
962        || modifier.on_drag_leave.is_some()
963        || modifier.on_drop.is_some()
964}
965
966fn key_for(window_id: u64, part: u64) -> u64 {
967    window_id ^ (part.wrapping_mul(0x9E3779B97F4A7C15))
968}
969
970/// Convert a px magnitude to a dp magnitude (both as `f32`).
971/// Prefer the typed [`Px`] / [`Dp`] API at public boundaries; these helpers
972/// serve the drag-math internals, which work in dp magnitudes.
973fn px_to_dp(px: f32) -> f32 {
974    Px(px).to_dp().0
975}
976
977fn px_vec_to_dp(v: Vec2) -> Vec2 {
978    Vec2 {
979        x: px_to_dp(v.x),
980        y: px_to_dp(v.y),
981    }
982}
983
984fn rect_px_to_dp(r: Rect) -> Rect {
985    Rect {
986        x: px_to_dp(r.x),
987        y: px_to_dp(r.y),
988        w: px_to_dp(r.w),
989        h: px_to_dp(r.h),
990    }
991}