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 pub position: DpOffset,
34 pub size: DpSize,
36 pub min_size: DpSize,
38 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#[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
258pub trait WindowModifierExt: Sized {
265 fn window_focus_region(self, host: &WindowHostHandle, window_id: u64) -> Modifier;
267
268 fn window_drag_region(self, host: &WindowHostHandle, window_id: u64) -> Modifier;
270
271 fn window_resize_handle(
273 self,
274 host: &WindowHostHandle,
275 window_id: u64,
276 handle: ResizeHandle,
277 ) -> Modifier;
278
279 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
970fn 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}