1use cranpose_core::MutableState;
4use cranpose_ui::{composable, Modifier, Point, PointerEventKind, PointerInputScope, Size};
5#[cfg(all(
6 feature = "desktop",
7 feature = "renderer-wgpu",
8 not(target_arch = "wasm32")
9))]
10use std::cell::Cell;
11use std::cell::RefCell;
12#[cfg(all(
13 feature = "desktop",
14 feature = "renderer-wgpu",
15 not(target_arch = "wasm32")
16))]
17use std::collections::HashMap;
18use std::hash::{Hash, Hasher};
19use std::rc::Rc;
20#[cfg(all(
21 feature = "desktop",
22 feature = "renderer-wgpu",
23 not(target_arch = "wasm32")
24))]
25use std::rc::Weak;
26
27#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
29pub struct WindowId(u64);
30
31impl WindowId {
32 pub fn from_static(id: &'static str) -> Self {
34 Self(hash_id(id))
35 }
36}
37
38#[cfg(all(
39 feature = "desktop",
40 feature = "renderer-wgpu",
41 not(target_arch = "wasm32")
42))]
43pub(crate) type NativeWindowKey = WindowId;
44
45#[cfg(all(
46 feature = "desktop",
47 feature = "renderer-wgpu",
48 not(target_arch = "wasm32")
49))]
50#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
51pub(crate) struct WindowGroupId(u64);
52
53#[cfg(all(
54 feature = "desktop",
55 feature = "renderer-wgpu",
56 not(target_arch = "wasm32")
57))]
58impl WindowGroupId {
59 fn from_static(id: &'static str) -> Self {
60 Self(hash_id(id))
61 }
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub enum NativeWindowPositionOrigin {
67 Screen,
69 HostWindow,
71}
72
73#[derive(Clone, Debug, PartialEq)]
75pub struct NativeWindowOptions {
76 pub title: String,
78 pub width: f32,
80 pub height: f32,
82 pub x: Option<f32>,
84 pub y: Option<f32>,
86 pub position_origin: NativeWindowPositionOrigin,
88 pub decorations: bool,
90 pub transparent: bool,
92 pub resizable: bool,
94 pub visible: bool,
96 pub always_on_top: bool,
98 pub min_width: Option<f32>,
100 pub min_height: Option<f32>,
102 pub max_width: Option<f32>,
104 pub max_height: Option<f32>,
106}
107
108#[derive(Clone, Debug, PartialEq, Eq)]
110pub enum WindowMoveMode {
111 AllAttached,
113 DragLeaderOnly(Vec<WindowId>),
115}
116
117impl WindowMoveMode {
118 #[cfg(all(
119 feature = "desktop",
120 feature = "renderer-wgpu",
121 not(target_arch = "wasm32")
122 ))]
123 fn moves_attached_component(&self, window_id: WindowId) -> bool {
124 match self {
125 Self::AllAttached => true,
126 Self::DragLeaderOnly(leaders) => leaders.contains(&window_id),
127 }
128 }
129}
130
131#[derive(Clone, Debug, PartialEq)]
133pub struct WindowAttachPolicy {
134 pub snap_distance: f32,
136 pub attach_epsilon: f32,
138 pub move_mode: WindowMoveMode,
140}
141
142impl WindowAttachPolicy {
143 pub fn new(snap_distance: f32, attach_epsilon: f32, move_mode: WindowMoveMode) -> Self {
145 Self {
146 snap_distance,
147 attach_epsilon,
148 move_mode,
149 }
150 }
151}
152
153impl Default for WindowAttachPolicy {
154 fn default() -> Self {
155 Self {
156 snap_distance: 8.0,
157 attach_epsilon: 3.0,
158 move_mode: WindowMoveMode::AllAttached,
159 }
160 }
161}
162
163#[cfg(all(
164 feature = "desktop",
165 feature = "renderer-wgpu",
166 not(target_arch = "wasm32")
167))]
168#[derive(Clone, Debug, PartialEq)]
169pub(crate) struct NativeWindowGroupMembership {
170 pub(crate) id: WindowGroupId,
171 pub(crate) policy: WindowAttachPolicy,
172}
173
174impl NativeWindowOptions {
175 pub fn new(title: impl Into<String>, width: f32, height: f32) -> Self {
177 Self {
178 title: title.into(),
179 width,
180 height,
181 x: None,
182 y: None,
183 position_origin: NativeWindowPositionOrigin::Screen,
184 decorations: true,
185 transparent: false,
186 resizable: true,
187 visible: true,
188 always_on_top: false,
189 min_width: None,
190 min_height: None,
191 max_width: None,
192 max_height: None,
193 }
194 }
195
196 pub fn borderless(title: impl Into<String>, width: f32, height: f32) -> Self {
198 Self {
199 decorations: false,
200 resizable: false,
201 ..Self::new(title, width, height)
202 }
203 }
204
205 pub fn with_position(mut self, x: f32, y: f32) -> Self {
207 self.x = Some(x);
208 self.y = Some(y);
209 self.position_origin = NativeWindowPositionOrigin::Screen;
210 self
211 }
212
213 pub fn with_host_window_position(mut self, x: f32, y: f32) -> Self {
215 self.x = Some(x);
216 self.y = Some(y);
217 self.position_origin = NativeWindowPositionOrigin::HostWindow;
218 self
219 }
220
221 pub fn with_transparent(mut self, transparent: bool) -> Self {
223 self.transparent = transparent;
224 self
225 }
226
227 pub fn with_resizable(mut self, resizable: bool) -> Self {
229 self.resizable = resizable;
230 self
231 }
232
233 pub fn with_visible(mut self, visible: bool) -> Self {
235 self.visible = visible;
236 self
237 }
238
239 pub fn with_always_on_top(mut self, always_on_top: bool) -> Self {
241 self.always_on_top = always_on_top;
242 self
243 }
244
245 pub fn with_min_size(mut self, width: f32, height: f32) -> Self {
247 self.min_width = Some(width);
248 self.min_height = Some(height);
249 self
250 }
251
252 pub fn with_max_size(mut self, width: f32, height: f32) -> Self {
254 self.max_width = Some(width);
255 self.max_height = Some(height);
256 self
257 }
258}
259
260#[derive(Clone, Default)]
262pub(crate) struct NativeWindowEvents {
263 pub(crate) on_moved: Option<Rc<dyn Fn(f32, f32)>>,
264 pub(crate) on_resized: Option<Rc<dyn Fn(f32, f32)>>,
265 pub(crate) on_close_requested: Option<Rc<dyn Fn()>>,
266}
267
268impl NativeWindowEvents {
269 fn new() -> Self {
270 Self::default()
271 }
272
273 fn with_on_moved(mut self, callback: impl Fn(f32, f32) + 'static) -> Self {
274 let next = Rc::new(callback);
275 self.on_moved = Some(match self.on_moved.take() {
276 Some(previous) => Rc::new(move |x, y| {
277 previous(x, y);
278 next(x, y);
279 }),
280 None => next,
281 });
282 self
283 }
284
285 fn with_on_resized(mut self, callback: impl Fn(f32, f32) + 'static) -> Self {
286 let next = Rc::new(callback);
287 self.on_resized = Some(match self.on_resized.take() {
288 Some(previous) => Rc::new(move |width, height| {
289 previous(width, height);
290 next(width, height);
291 }),
292 None => next,
293 });
294 self
295 }
296
297 fn with_on_close_requested(mut self, callback: impl Fn() + 'static) -> Self {
298 let next = Rc::new(callback);
299 self.on_close_requested = Some(match self.on_close_requested.take() {
300 Some(previous) => Rc::new(move || {
301 previous();
302 next();
303 }),
304 None => next,
305 });
306 self
307 }
308}
309
310#[derive(Clone, Copy, Eq, PartialEq)]
312pub struct WindowState {
313 position: MutableState<Option<Point>>,
314 size: MutableState<Size>,
315}
316
317impl WindowState {
318 pub fn position(self) -> Option<Point> {
320 self.position.get()
321 }
322
323 pub fn position_non_reactive(self) -> Option<Point> {
325 self.position.get_non_reactive()
326 }
327
328 pub fn set_position(self, position: Option<Point>) {
330 if self.position.get_non_reactive() != position {
331 self.position.set(position);
332 }
333 }
334
335 pub fn translate(self, dx: f32, dy: f32) {
337 if let Some(position) = self.position_non_reactive() {
338 self.set_position(Some(Point::new(position.x + dx, position.y + dy)));
339 }
340 }
341
342 pub fn size(self) -> Size {
344 self.size.get()
345 }
346
347 pub fn size_non_reactive(self) -> Size {
349 self.size.get_non_reactive()
350 }
351
352 pub fn set_size(self, size: Size) {
354 if self.size.get_non_reactive() != size {
355 self.size.set(size);
356 }
357 }
358}
359
360#[allow(non_snake_case)]
362#[composable]
363pub fn rememberWindowState(width: f32, height: f32) -> WindowState {
364 WindowState {
365 position: cranpose_core::useState(|| None::<Point>),
366 size: cranpose_core::useState(move || Size::new(width, height)),
367 }
368}
369
370#[derive(Clone)]
376pub struct WindowConfig {
377 options: NativeWindowOptions,
378 callbacks: NativeWindowEvents,
379 state: Option<WindowState>,
380}
381
382impl WindowConfig {
383 pub fn new(title: impl Into<String>, width: f32, height: f32) -> Self {
385 Self {
386 options: NativeWindowOptions::new(title, width, height),
387 callbacks: NativeWindowEvents::new(),
388 state: None,
389 }
390 }
391
392 pub fn new_for_state(title: impl Into<String>, state: WindowState) -> Self {
394 let size = state.size();
395 Self::new(title, size.width, size.height).with_state(state)
396 }
397
398 pub fn borderless(title: impl Into<String>, width: f32, height: f32) -> Self {
400 Self {
401 options: NativeWindowOptions::borderless(title, width, height),
402 callbacks: NativeWindowEvents::new(),
403 state: None,
404 }
405 }
406
407 pub fn borderless_for_state(title: impl Into<String>, state: WindowState) -> Self {
409 let size = state.size();
410 Self::borderless(title, size.width, size.height).with_state(state)
411 }
412
413 pub fn with_position(mut self, x: f32, y: f32) -> Self {
415 self.options = self.options.with_position(x, y);
416 self
417 }
418
419 pub fn with_host_window_position(mut self, x: f32, y: f32) -> Self {
421 self.options = self.options.with_host_window_position(x, y);
422 self
423 }
424
425 pub fn with_transparent(mut self, transparent: bool) -> Self {
427 self.options = self.options.with_transparent(transparent);
428 self
429 }
430
431 pub fn with_resizable(mut self, resizable: bool) -> Self {
433 self.options = self.options.with_resizable(resizable);
434 self
435 }
436
437 pub fn with_visible(mut self, visible: bool) -> Self {
439 self.options = self.options.with_visible(visible);
440 self
441 }
442
443 pub fn with_always_on_top(mut self, always_on_top: bool) -> Self {
445 self.options = self.options.with_always_on_top(always_on_top);
446 self
447 }
448
449 pub fn with_min_size(mut self, width: f32, height: f32) -> Self {
451 self.options = self.options.with_min_size(width, height);
452 self
453 }
454
455 pub fn with_max_size(mut self, width: f32, height: f32) -> Self {
457 self.options = self.options.with_max_size(width, height);
458 self
459 }
460
461 pub fn on_moved(mut self, callback: impl Fn(f32, f32) + 'static) -> Self {
466 self.callbacks = self.callbacks.with_on_moved(callback);
467 self
468 }
469
470 pub fn on_resized(mut self, callback: impl Fn(f32, f32) + 'static) -> Self {
472 self.callbacks = self.callbacks.with_on_resized(callback);
473 self
474 }
475
476 pub fn on_close_requested(mut self, callback: impl Fn() + 'static) -> Self {
478 self.callbacks = self.callbacks.with_on_close_requested(callback);
479 self
480 }
481
482 pub fn with_state(mut self, state: WindowState) -> Self {
488 let size = state.size();
489 self.options.width = size.width;
490 self.options.height = size.height;
491 if let Some(position) = state.position() {
492 self.options.x = Some(position.x);
493 self.options.y = Some(position.y);
494 self.options.position_origin = NativeWindowPositionOrigin::Screen;
495 }
496 self.state = Some(state);
497 self
498 }
499
500 pub(crate) fn into_parts(
501 self,
502 ) -> (NativeWindowOptions, NativeWindowEvents, Option<WindowState>) {
503 (self.options, self.callbacks, self.state)
504 }
505}
506
507#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
509pub enum WindowResizeDirection {
510 East,
512 North,
514 NorthEast,
516 NorthWest,
518 South,
520 SouthEast,
522 SouthWest,
524 West,
526}
527
528pub trait WindowModifierExt {
530 fn window_drag_area(self) -> Modifier;
535
536 fn window_drag_area_with_callbacks(
541 self,
542 on_started: impl Fn() + 'static,
543 on_finished: impl Fn() + 'static,
544 ) -> Modifier;
545
546 fn window_resize_area(self, direction: WindowResizeDirection) -> Modifier;
551}
552
553impl WindowModifierExt for Modifier {
554 fn window_drag_area(self) -> Modifier {
555 self.window_drag_area_with_callbacks(|| {}, || {})
556 }
557
558 fn window_drag_area_with_callbacks(
559 self,
560 on_started: impl Fn() + 'static,
561 on_finished: impl Fn() + 'static,
562 ) -> Modifier {
563 let on_started: Rc<dyn Fn()> = Rc::new(on_started);
564 let on_finished: Rc<dyn Fn()> = Rc::new(on_finished);
565 self.pointer_input((), move |scope: PointerInputScope| {
566 let on_started = on_started.clone();
567 let on_finished = on_finished.clone();
568 async move {
569 scope
570 .await_pointer_event_scope(|await_scope| async move {
571 let mut dragging = false;
572 loop {
573 let event = await_scope.await_pointer_event().await;
574 match event.kind {
575 PointerEventKind::Down => {
576 if request_native_window_drag() {
577 dragging = true;
578 event.consume();
579 on_started();
580 }
581 }
582 PointerEventKind::Move => {
583 if dragging && event.buttons == Default::default() {
584 dragging = false;
585 on_finished();
586 }
587 }
588 PointerEventKind::Up | PointerEventKind::Cancel => {
589 if dragging {
590 dragging = false;
591 on_finished();
592 }
593 }
594 PointerEventKind::Scroll
595 | PointerEventKind::Enter
596 | PointerEventKind::Exit => {}
597 }
598 }
599 })
600 .await;
601 }
602 })
603 }
604
605 fn window_resize_area(self, direction: WindowResizeDirection) -> Modifier {
606 self.pointer_input(direction, move |scope: PointerInputScope| async move {
607 scope
608 .await_pointer_event_scope(|await_scope| async move {
609 loop {
610 let event = await_scope.await_pointer_event().await;
611 if event.kind == PointerEventKind::Down
612 && request_native_window_resize(direction)
613 {
614 event.consume();
615 }
616 }
617 })
618 .await;
619 })
620 }
621}
622
623#[cfg(all(
624 feature = "desktop",
625 feature = "renderer-wgpu",
626 not(target_arch = "wasm32")
627))]
628pub(crate) type NativeWindowContent = Rc<RefCell<Box<dyn FnMut()>>>;
629
630#[cfg(all(
631 feature = "desktop",
632 feature = "renderer-wgpu",
633 not(target_arch = "wasm32")
634))]
635type NativeWindowOwner = Rc<()>;
636
637type NativeWindowDragHandler = Rc<dyn Fn() -> bool>;
638type NativeWindowResizeHandler = Rc<dyn Fn(WindowResizeDirection)>;
639
640#[derive(Clone, Default)]
641struct NativeWindowDispatchContext {
642 drag_handler: Option<NativeWindowDragHandler>,
643 resize_handler: Option<NativeWindowResizeHandler>,
644 surface_origin: Option<Point>,
645}
646
647#[cfg(all(
648 feature = "desktop",
649 feature = "renderer-wgpu",
650 not(target_arch = "wasm32")
651))]
652#[derive(Clone)]
653pub(crate) struct NativeWindowRequest {
654 pub(crate) key: NativeWindowKey,
655 pub(crate) options: NativeWindowOptions,
656 pub(crate) events: NativeWindowEvents,
657 pub(crate) state: Option<WindowState>,
658 pub(crate) group: Option<NativeWindowGroupMembership>,
659 pub(crate) content: NativeWindowContent,
660 pub(crate) revision: u64,
661 owner: NativeWindowOwner,
662}
663
664#[cfg(all(
665 feature = "desktop",
666 feature = "renderer-wgpu",
667 not(target_arch = "wasm32")
668))]
669struct NativeWindowRegistration {
670 key: NativeWindowKey,
671 options: NativeWindowOptions,
672 events: NativeWindowEvents,
673 state: Option<WindowState>,
674 group: Option<NativeWindowGroupMembership>,
675 content: NativeWindowContent,
676 owner: NativeWindowOwner,
677}
678
679#[cfg(all(
680 feature = "desktop",
681 feature = "renderer-wgpu",
682 not(target_arch = "wasm32")
683))]
684#[derive(Default)]
685pub(crate) struct NativeWindowRegistry {
686 windows: RefCell<HashMap<NativeWindowKey, NativeWindowRequest>>,
687 next_revision: Cell<u64>,
688}
689
690#[cfg(all(
691 feature = "desktop",
692 feature = "renderer-wgpu",
693 not(target_arch = "wasm32")
694))]
695impl NativeWindowRegistry {
696 fn requests(&self) -> Vec<NativeWindowRequest> {
697 self.windows.borrow().values().cloned().collect()
698 }
699
700 fn has_requests(&self) -> bool {
701 !self.windows.borrow().is_empty()
702 }
703
704 #[cfg(test)]
705 fn clear(&self) {
706 self.windows.borrow_mut().clear();
707 }
708
709 fn register(&self, registration: NativeWindowRegistration) {
710 let revision = self.next_revision();
711 let key = registration.key;
712 self.windows.borrow_mut().insert(
713 key,
714 NativeWindowRequest {
715 key,
716 options: registration.options,
717 events: registration.events,
718 state: registration.state,
719 group: registration.group,
720 content: registration.content,
721 revision,
722 owner: registration.owner,
723 },
724 );
725 }
726
727 fn unregister(&self, key: NativeWindowKey, owner: NativeWindowOwner) {
728 let mut windows = self.windows.borrow_mut();
729 if windows
730 .get(&key)
731 .is_some_and(|request| Rc::ptr_eq(&request.owner, &owner))
732 {
733 windows.remove(&key);
734 }
735 }
736
737 fn next_revision(&self) -> u64 {
738 let current = self.next_revision.get().max(1);
739 self.next_revision.set(current.wrapping_add(1).max(1));
740 current
741 }
742}
743
744#[cfg(all(
745 feature = "desktop",
746 feature = "renderer-wgpu",
747 not(target_arch = "wasm32")
748))]
749thread_local! {
750 static CURRENT_NATIVE_WINDOW_REGISTRY: RefCell<Vec<Weak<NativeWindowRegistry>>> =
751 const { RefCell::new(Vec::new()) };
752}
753
754thread_local! {
755 static CURRENT_NATIVE_WINDOW_DISPATCH: RefCell<Vec<NativeWindowDispatchContext>> = const { RefCell::new(Vec::new()) };
756 #[cfg(all(
757 feature = "desktop",
758 feature = "renderer-wgpu",
759 not(target_arch = "wasm32")
760 ))]
761 static CURRENT_WINDOW_GROUP: RefCell<Option<NativeWindowGroupMembership>> = const { RefCell::new(None) };
762}
763
764#[allow(non_snake_case)]
769#[composable(no_skip)]
770pub fn Window(id: &'static str, config: WindowConfig, content: impl FnMut() + 'static) {
771 let (options, events, state) = config.into_parts();
772 let window_id = WindowId::from_static(id);
773 NativeWindowWithEvents(window_id, options, events, state, content);
774}
775
776#[allow(non_snake_case)]
781#[composable(no_skip)]
782pub fn WindowNode(id: WindowId, config: WindowConfig, content: impl FnMut() + 'static) {
783 let (options, events, state) = config.into_parts();
784 NativeWindowWithEvents(id, options, events, state, content);
785}
786
787#[allow(non_snake_case)]
789#[composable(no_skip)]
790pub fn WindowGroup(id: &'static str, policy: WindowAttachPolicy, content: impl FnMut() + 'static) {
791 #[cfg(all(
792 feature = "desktop",
793 feature = "renderer-wgpu",
794 not(target_arch = "wasm32")
795 ))]
796 {
797 with_window_group(
798 NativeWindowGroupMembership {
799 id: WindowGroupId::from_static(id),
800 policy,
801 },
802 content,
803 );
804 }
805
806 #[cfg(not(all(
807 feature = "desktop",
808 feature = "renderer-wgpu",
809 not(target_arch = "wasm32")
810 )))]
811 {
812 let _ = (id, policy);
813 let mut content = content;
814 content();
815 }
816}
817
818#[allow(non_snake_case)]
819#[composable(no_skip)]
820fn NativeWindowWithEvents(
821 id: WindowId,
822 options: NativeWindowOptions,
823 events: NativeWindowEvents,
824 state: Option<WindowState>,
825 content: impl FnMut() + 'static,
826) {
827 #[cfg(all(
828 feature = "desktop",
829 feature = "renderer-wgpu",
830 not(target_arch = "wasm32")
831 ))]
832 {
833 let key = id;
834 let group = current_window_group();
835 let owner = cranpose_core::remember(|| Rc::new(())).with(Rc::clone);
836 let content_cell =
837 cranpose_core::remember(|| Rc::new(RefCell::new(Box::new(|| {}) as Box<dyn FnMut()>)))
838 .with(Rc::clone);
839 *content_cell.borrow_mut() = Box::new(content);
840
841 {
842 let options = options.clone();
843 let events = events.clone();
844 let content = Rc::clone(&content_cell);
845 let owner = Rc::clone(&owner);
846 cranpose_core::SideEffect(move || {
847 register_native_window(key, options, events, state, group, content, owner);
848 });
849 }
850
851 {
852 let owner = Rc::clone(&owner);
853 cranpose_core::DisposableEffect!(key, move |scope| {
854 scope.on_dispose(move || unregister_native_window(key, owner))
855 });
856 }
857 }
858
859 #[cfg(not(all(
860 feature = "desktop",
861 feature = "renderer-wgpu",
862 not(target_arch = "wasm32")
863 )))]
864 {
865 let mut content = content;
866 let _ = (id, options, events, state);
867 content();
868 }
869}
870
871fn request_native_window_drag() -> bool {
876 current_native_window_dispatch_context()
877 .and_then(|context| context.drag_handler)
878 .is_some_and(|handler| handler())
879}
880
881fn request_native_window_resize(direction: WindowResizeDirection) -> bool {
882 current_native_window_dispatch_context()
883 .and_then(|context| context.resize_handler)
884 .is_some_and(|handler| {
885 handler(direction);
886 true
887 })
888}
889
890pub fn current_native_window_surface_origin() -> Option<Point> {
895 current_native_window_dispatch_context().and_then(|context| context.surface_origin)
896}
897
898#[cfg(all(
899 feature = "desktop",
900 feature = "renderer-wgpu",
901 not(target_arch = "wasm32")
902))]
903pub(crate) fn with_native_window_registry<R>(
904 registry: &Rc<NativeWindowRegistry>,
905 f: impl FnOnce() -> R,
906) -> R {
907 struct RegistryGuard;
908
909 impl Drop for RegistryGuard {
910 fn drop(&mut self) {
911 CURRENT_NATIVE_WINDOW_REGISTRY.with(|stack| {
912 stack.borrow_mut().pop();
913 });
914 }
915 }
916
917 CURRENT_NATIVE_WINDOW_REGISTRY.with(|stack| {
918 stack.borrow_mut().push(Rc::downgrade(registry));
919 });
920 let _guard = RegistryGuard;
921 f()
922}
923
924#[cfg(all(
925 feature = "desktop",
926 feature = "renderer-wgpu",
927 not(target_arch = "wasm32")
928))]
929fn current_native_window_registry() -> Option<Rc<NativeWindowRegistry>> {
930 CURRENT_NATIVE_WINDOW_REGISTRY.with(|stack| {
931 let mut stack = stack.borrow_mut();
932 loop {
933 match stack.last().and_then(Weak::upgrade) {
934 Some(registry) => return Some(registry),
935 None if stack.is_empty() => return None,
936 None => {
937 stack.pop();
938 }
939 }
940 }
941 })
942}
943
944#[cfg(all(
945 feature = "desktop",
946 feature = "renderer-wgpu",
947 not(target_arch = "wasm32")
948))]
949pub(crate) fn native_window_requests(registry: &NativeWindowRegistry) -> Vec<NativeWindowRequest> {
950 registry.requests()
951}
952
953#[cfg(all(
954 feature = "desktop",
955 feature = "renderer-wgpu",
956 not(target_arch = "wasm32")
957))]
958pub(crate) fn has_native_window_requests(registry: &NativeWindowRegistry) -> bool {
959 registry.has_requests()
960}
961
962#[cfg(all(
963 test,
964 feature = "desktop",
965 feature = "renderer-wgpu",
966 not(target_arch = "wasm32")
967))]
968pub(crate) fn clear_native_window_requests(registry: &NativeWindowRegistry) {
969 registry.clear();
970}
971
972fn current_native_window_dispatch_context() -> Option<NativeWindowDispatchContext> {
973 CURRENT_NATIVE_WINDOW_DISPATCH.with(|stack| stack.borrow().last().cloned())
974}
975
976#[cfg(all(
977 feature = "desktop",
978 feature = "renderer-wgpu",
979 not(target_arch = "wasm32")
980))]
981fn with_native_window_dispatch_context<R>(
982 context: NativeWindowDispatchContext,
983 f: impl FnOnce() -> R,
984) -> R {
985 struct DispatchContextGuard;
986
987 impl Drop for DispatchContextGuard {
988 fn drop(&mut self) {
989 CURRENT_NATIVE_WINDOW_DISPATCH.with(|stack| {
990 stack.borrow_mut().pop();
991 });
992 }
993 }
994
995 CURRENT_NATIVE_WINDOW_DISPATCH.with(|stack| {
996 stack.borrow_mut().push(context);
997 });
998 let _guard = DispatchContextGuard;
999 f()
1000}
1001
1002#[cfg(all(
1003 feature = "desktop",
1004 feature = "renderer-wgpu",
1005 not(target_arch = "wasm32")
1006))]
1007pub(crate) fn with_native_window_drag_handler<R>(
1008 handler: NativeWindowDragHandler,
1009 resize_handler: NativeWindowResizeHandler,
1010 f: impl FnOnce() -> R,
1011) -> R {
1012 let mut context = current_native_window_dispatch_context().unwrap_or_default();
1013 context.drag_handler = Some(handler);
1014 context.resize_handler = Some(resize_handler);
1015 with_native_window_dispatch_context(context, f)
1016}
1017
1018#[cfg(all(
1019 feature = "desktop",
1020 feature = "renderer-wgpu",
1021 not(target_arch = "wasm32")
1022))]
1023pub(crate) fn with_native_window_surface_origin<R>(
1024 origin: Option<Point>,
1025 f: impl FnOnce() -> R,
1026) -> R {
1027 let mut context = current_native_window_dispatch_context().unwrap_or_default();
1028 context.surface_origin = origin;
1029 with_native_window_dispatch_context(context, f)
1030}
1031
1032#[cfg(all(
1033 feature = "desktop",
1034 feature = "renderer-wgpu",
1035 not(target_arch = "wasm32")
1036))]
1037fn current_window_group() -> Option<NativeWindowGroupMembership> {
1038 CURRENT_WINDOW_GROUP.with(|slot| slot.borrow().clone())
1039}
1040
1041#[cfg(all(
1042 feature = "desktop",
1043 feature = "renderer-wgpu",
1044 not(target_arch = "wasm32")
1045))]
1046fn with_window_group<R>(group: NativeWindowGroupMembership, f: impl FnOnce() -> R) -> R {
1047 struct WindowGroupGuard(Option<NativeWindowGroupMembership>);
1048
1049 impl Drop for WindowGroupGuard {
1050 fn drop(&mut self) {
1051 CURRENT_WINDOW_GROUP.with(|slot| {
1052 *slot.borrow_mut() = self.0.take();
1053 });
1054 }
1055 }
1056
1057 let previous = CURRENT_WINDOW_GROUP.with(|slot| slot.borrow_mut().replace(group));
1058 let guard = WindowGroupGuard(previous);
1059 let result = f();
1060 drop(guard);
1061 result
1062}
1063
1064#[cfg(all(
1065 feature = "desktop",
1066 feature = "renderer-wgpu",
1067 not(target_arch = "wasm32")
1068))]
1069fn register_native_window(
1070 key: NativeWindowKey,
1071 options: NativeWindowOptions,
1072 events: NativeWindowEvents,
1073 state: Option<WindowState>,
1074 group: Option<NativeWindowGroupMembership>,
1075 content: NativeWindowContent,
1076 owner: NativeWindowOwner,
1077) {
1078 let Some(registry) = current_native_window_registry() else {
1079 log::error!(
1080 "native window declaration {key:?} ignored because no native-window registry is active"
1081 );
1082 return;
1083 };
1084 registry.register(NativeWindowRegistration {
1085 key,
1086 options,
1087 events,
1088 state,
1089 group,
1090 content,
1091 owner,
1092 });
1093}
1094
1095#[cfg(all(
1096 feature = "desktop",
1097 feature = "renderer-wgpu",
1098 not(target_arch = "wasm32")
1099))]
1100fn unregister_native_window(key: NativeWindowKey, owner: NativeWindowOwner) {
1101 let Some(registry) = current_native_window_registry() else {
1102 log::error!(
1103 "native window declaration {key:?} could not unregister because no native-window registry is active"
1104 );
1105 return;
1106 };
1107 registry.unregister(key, owner);
1108}
1109
1110fn hash_id(id: &'static str) -> u64 {
1111 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1112 id.hash(&mut hasher);
1113 hasher.finish()
1114}
1115
1116#[cfg(all(
1117 feature = "desktop",
1118 feature = "renderer-wgpu",
1119 not(target_arch = "wasm32")
1120))]
1121#[derive(Clone, Copy, Debug, PartialEq)]
1122pub(crate) struct WindowGraphNodeSnapshot {
1123 pub(crate) id: WindowId,
1124 pub(crate) position: Point,
1125 pub(crate) size: Size,
1126}
1127
1128#[cfg(all(
1129 feature = "desktop",
1130 feature = "renderer-wgpu",
1131 not(target_arch = "wasm32")
1132))]
1133#[derive(Clone, Debug, PartialEq)]
1134pub(crate) struct WindowGraphPeerSnapshot {
1135 pub(crate) node: WindowGraphNodeSnapshot,
1136 pub(crate) group: Option<NativeWindowGroupMembership>,
1137}
1138
1139#[cfg(all(
1140 feature = "desktop",
1141 feature = "renderer-wgpu",
1142 not(target_arch = "wasm32")
1143))]
1144#[derive(Clone, Copy, Debug, PartialEq)]
1145pub(crate) struct WindowGraphMove {
1146 pub(crate) id: WindowId,
1147 pub(crate) position: Point,
1148}
1149
1150#[cfg(all(
1151 feature = "desktop",
1152 feature = "renderer-wgpu",
1153 not(target_arch = "wasm32")
1154))]
1155#[derive(Clone, Debug)]
1156struct WindowGraphDragSession {
1157 group: Option<NativeWindowGroupMembership>,
1158 dragged: WindowId,
1159 start_dragged_position: Point,
1160 captured: Vec<WindowGraphNodeSnapshot>,
1161}
1162
1163#[cfg(all(
1165 feature = "desktop",
1166 feature = "renderer-wgpu",
1167 not(target_arch = "wasm32")
1168))]
1169#[derive(Default)]
1170pub(crate) struct WindowGraphState {
1171 active_drag: Option<WindowGraphDragSession>,
1172}
1173
1174#[cfg(all(
1175 feature = "desktop",
1176 feature = "renderer-wgpu",
1177 not(target_arch = "wasm32")
1178))]
1179impl WindowGraphState {
1180 pub(crate) fn start_drag(&mut self, windows: &[WindowGraphPeerSnapshot], dragged: WindowId) {
1181 let Some(dragged_window) = windows.iter().find(|window| window.node.id == dragged) else {
1182 self.active_drag = None;
1183 return;
1184 };
1185 let group = dragged_window.group.clone();
1186 let captured = if let Some(group) = &group {
1187 let group_windows = group_windows(windows, group);
1188 let moves_attached = group.policy.move_mode.moves_attached_component(dragged);
1189 let component = if moves_attached {
1190 attached_component(&group_windows, dragged, group.policy.attach_epsilon)
1191 } else {
1192 vec![dragged]
1193 };
1194 group_windows
1195 .into_iter()
1196 .filter(|window| component.contains(&window.id))
1197 .collect()
1198 } else {
1199 vec![dragged_window.node]
1200 };
1201
1202 self.active_drag = Some(WindowGraphDragSession {
1203 group,
1204 dragged,
1205 start_dragged_position: dragged_window.node.position,
1206 captured,
1207 });
1208 }
1209
1210 pub(crate) fn drag_to(
1211 &self,
1212 dragged: WindowId,
1213 target_position: Point,
1214 ) -> Vec<WindowGraphMove> {
1215 let Some(session) = &self.active_drag else {
1216 return vec![WindowGraphMove {
1217 id: dragged,
1218 position: target_position,
1219 }];
1220 };
1221 if session.dragged != dragged {
1222 return Vec::new();
1223 }
1224
1225 let delta = Point::new(
1226 target_position.x - session.start_dragged_position.x,
1227 target_position.y - session.start_dragged_position.y,
1228 );
1229 session
1230 .captured
1231 .iter()
1232 .map(|window| WindowGraphMove {
1233 id: window.id,
1234 position: Point::new(window.position.x + delta.x, window.position.y + delta.y),
1235 })
1236 .collect()
1237 }
1238
1239 pub(crate) fn cancel_drag(&mut self) {
1240 self.active_drag = None;
1241 }
1242
1243 pub(crate) fn finish_drag(
1244 &mut self,
1245 windows: &[WindowGraphPeerSnapshot],
1246 ) -> Vec<WindowGraphMove> {
1247 let Some(session) = self.active_drag.take() else {
1248 return Vec::new();
1249 };
1250 let Some(group) = &session.group else {
1251 return Vec::new();
1252 };
1253 let group_windows = group_windows(windows, group);
1254 if group_windows
1255 .iter()
1256 .all(|window| window.id != session.dragged)
1257 {
1258 return Vec::new();
1259 }
1260
1261 let moves_attached = group
1262 .policy
1263 .move_mode
1264 .moves_attached_component(session.dragged);
1265 let mut component = if moves_attached {
1266 session.captured.iter().map(|window| window.id).collect()
1267 } else {
1268 vec![session.dragged]
1269 };
1270 if let Some(snap) = closest_snap(&group_windows, &component, group.policy.snap_distance) {
1271 let mut moved = group_windows;
1272 translate_nodes(&mut moved, &component, snap.delta);
1273 if moves_attached {
1274 for id in attached_component(&moved, snap.target, group.policy.attach_epsilon) {
1275 if !component.contains(&id) {
1276 component.push(id);
1277 }
1278 }
1279 }
1280 return moved
1281 .into_iter()
1282 .filter(|window| component.contains(&window.id))
1283 .map(|window| WindowGraphMove {
1284 id: window.id,
1285 position: window.position,
1286 })
1287 .collect();
1288 }
1289
1290 Vec::new()
1291 }
1292
1293 pub(crate) fn external_move(
1294 &self,
1295 windows: &[WindowGraphPeerSnapshot],
1296 moved: WindowId,
1297 new_position: Point,
1298 ) -> Vec<WindowGraphMove> {
1299 let Some(moved_window) = windows.iter().find(|window| window.node.id == moved) else {
1300 return Vec::new();
1301 };
1302 let Some(group) = &moved_window.group else {
1303 return Vec::new();
1304 };
1305 if !group.policy.move_mode.moves_attached_component(moved) {
1306 return Vec::new();
1307 }
1308
1309 let delta = Point::new(
1310 new_position.x - moved_window.node.position.x,
1311 new_position.y - moved_window.node.position.y,
1312 );
1313 if delta.x.abs() <= f32::EPSILON && delta.y.abs() <= f32::EPSILON {
1314 return Vec::new();
1315 }
1316 let group_windows = group_windows(windows, group);
1317 let component = attached_component(&group_windows, moved, group.policy.attach_epsilon);
1318 group_windows
1319 .into_iter()
1320 .filter(|window| component.contains(&window.id) && window.id != moved)
1321 .map(|window| WindowGraphMove {
1322 id: window.id,
1323 position: Point::new(window.position.x + delta.x, window.position.y + delta.y),
1324 })
1325 .collect()
1326 }
1327}
1328
1329#[cfg(all(
1330 feature = "desktop",
1331 feature = "renderer-wgpu",
1332 not(target_arch = "wasm32")
1333))]
1334fn group_windows(
1335 windows: &[WindowGraphPeerSnapshot],
1336 group: &NativeWindowGroupMembership,
1337) -> Vec<WindowGraphNodeSnapshot> {
1338 windows
1339 .iter()
1340 .filter(|window| {
1341 window
1342 .group
1343 .as_ref()
1344 .is_some_and(|candidate| candidate.id == group.id)
1345 })
1346 .map(|window| window.node)
1347 .collect()
1348}
1349
1350#[cfg(all(
1351 feature = "desktop",
1352 feature = "renderer-wgpu",
1353 not(target_arch = "wasm32")
1354))]
1355fn attached_component(
1356 windows: &[WindowGraphNodeSnapshot],
1357 dragged: WindowId,
1358 attach_epsilon: f32,
1359) -> Vec<WindowId> {
1360 let mut component = vec![dragged];
1361 let mut changed = true;
1362
1363 while changed {
1364 changed = false;
1365 for candidate in windows {
1366 if component.contains(&candidate.id) {
1367 continue;
1368 }
1369 let attached_to_component = windows
1370 .iter()
1371 .filter(|window| component.contains(&window.id))
1372 .any(|window| rects_attached(candidate, window, attach_epsilon));
1373 if attached_to_component {
1374 component.push(candidate.id);
1375 changed = true;
1376 }
1377 }
1378 }
1379
1380 component
1381}
1382
1383#[cfg(all(
1384 feature = "desktop",
1385 feature = "renderer-wgpu",
1386 not(target_arch = "wasm32")
1387))]
1388fn rects_attached(
1389 child: &WindowGraphNodeSnapshot,
1390 main: &WindowGraphNodeSnapshot,
1391 attach_epsilon: f32,
1392) -> bool {
1393 let child_right = child.position.x + child.size.width;
1394 let child_bottom = child.position.y + child.size.height;
1395 let main_right = main.position.x + main.size.width;
1396 let main_bottom = main.position.y + main.size.height;
1397
1398 let touches_horizontal = near(child.position.x, main_right, attach_epsilon)
1399 || near(child_right, main.position.x, attach_epsilon);
1400 let overlaps_vertical = ranges_overlap(
1401 child.position.y,
1402 child_bottom,
1403 main.position.y,
1404 main_bottom,
1405 attach_epsilon,
1406 );
1407 let touches_vertical = near(child.position.y, main_bottom, attach_epsilon)
1408 || near(child_bottom, main.position.y, attach_epsilon);
1409 let overlaps_horizontal = ranges_overlap(
1410 child.position.x,
1411 child_right,
1412 main.position.x,
1413 main_right,
1414 attach_epsilon,
1415 );
1416
1417 touches_horizontal && overlaps_vertical || touches_vertical && overlaps_horizontal
1418}
1419
1420#[cfg(all(
1421 feature = "desktop",
1422 feature = "renderer-wgpu",
1423 not(target_arch = "wasm32")
1424))]
1425#[derive(Clone, Copy, Debug, PartialEq)]
1426struct GraphSnap {
1427 target: WindowId,
1428 delta: Point,
1429 distance: f32,
1430 contact: f32,
1431}
1432
1433#[cfg(all(
1434 feature = "desktop",
1435 feature = "renderer-wgpu",
1436 not(target_arch = "wasm32")
1437))]
1438#[derive(Clone, Copy, Debug, PartialEq)]
1439struct GraphSnapCandidate {
1440 delta: Point,
1441 contact: f32,
1442}
1443
1444#[cfg(all(
1445 feature = "desktop",
1446 feature = "renderer-wgpu",
1447 not(target_arch = "wasm32")
1448))]
1449fn closest_snap(
1450 windows: &[WindowGraphNodeSnapshot],
1451 component: &[WindowId],
1452 snap_distance: f32,
1453) -> Option<GraphSnap> {
1454 let mut closest = None::<GraphSnap>;
1455
1456 for moving in windows
1457 .iter()
1458 .filter(|window| component.contains(&window.id))
1459 {
1460 for stationary in windows
1461 .iter()
1462 .filter(|window| !component.contains(&window.id))
1463 {
1464 for candidate in snap_candidates(moving, stationary, snap_distance) {
1465 let snap = GraphSnap {
1466 target: stationary.id,
1467 delta: candidate.delta,
1468 distance: candidate.delta.x.abs() + candidate.delta.y.abs(),
1469 contact: candidate.contact,
1470 };
1471 if closest.is_none_or(|current| {
1472 snap.contact > current.contact
1473 || snap.contact == current.contact && snap.distance < current.distance
1474 }) {
1475 closest = Some(snap);
1476 }
1477 }
1478 }
1479 }
1480
1481 closest
1482}
1483
1484#[cfg(all(
1485 feature = "desktop",
1486 feature = "renderer-wgpu",
1487 not(target_arch = "wasm32")
1488))]
1489fn snap_candidates(
1490 moving: &WindowGraphNodeSnapshot,
1491 stationary: &WindowGraphNodeSnapshot,
1492 snap_distance: f32,
1493) -> Vec<GraphSnapCandidate> {
1494 let moving_left = moving.position.x;
1495 let moving_top = moving.position.y;
1496 let moving_right = moving.position.x + moving.size.width;
1497 let moving_bottom = moving.position.y + moving.size.height;
1498 let stationary_left = stationary.position.x;
1499 let stationary_top = stationary.position.y;
1500 let stationary_right = stationary.position.x + stationary.size.width;
1501 let stationary_bottom = stationary.position.y + stationary.size.height;
1502
1503 let mut candidates = Vec::new();
1504 if ranges_overlap_strict(moving_top, moving_bottom, stationary_top, stationary_bottom) {
1505 let contact =
1506 range_overlap_length(moving_top, moving_bottom, stationary_top, stationary_bottom);
1507 if near(moving_right, stationary_left, snap_distance) {
1508 candidates.push(GraphSnapCandidate {
1509 delta: Point::new(stationary_left - moving_right, 0.0),
1510 contact,
1511 });
1512 }
1513 if near(moving_left, stationary_right, snap_distance) {
1514 candidates.push(GraphSnapCandidate {
1515 delta: Point::new(stationary_right - moving_left, 0.0),
1516 contact,
1517 });
1518 }
1519 }
1520 if ranges_overlap_strict(moving_left, moving_right, stationary_left, stationary_right) {
1521 let contact =
1522 range_overlap_length(moving_left, moving_right, stationary_left, stationary_right);
1523 if near(moving_bottom, stationary_top, snap_distance) {
1524 candidates.push(GraphSnapCandidate {
1525 delta: Point::new(0.0, stationary_top - moving_bottom),
1526 contact,
1527 });
1528 }
1529 if near(moving_top, stationary_bottom, snap_distance) {
1530 candidates.push(GraphSnapCandidate {
1531 delta: Point::new(0.0, stationary_bottom - moving_top),
1532 contact,
1533 });
1534 }
1535 }
1536
1537 candidates
1538}
1539
1540#[cfg(all(
1541 feature = "desktop",
1542 feature = "renderer-wgpu",
1543 not(target_arch = "wasm32")
1544))]
1545fn translate_nodes(windows: &mut [WindowGraphNodeSnapshot], component: &[WindowId], delta: Point) {
1546 if delta.x.abs() <= f32::EPSILON && delta.y.abs() <= f32::EPSILON {
1547 return;
1548 }
1549 for window in windows {
1550 if component.contains(&window.id) {
1551 window.position.x += delta.x;
1552 window.position.y += delta.y;
1553 }
1554 }
1555}
1556
1557#[cfg(all(
1558 feature = "desktop",
1559 feature = "renderer-wgpu",
1560 not(target_arch = "wasm32")
1561))]
1562fn near(a: f32, b: f32, distance: f32) -> bool {
1563 (a - b).abs() <= distance
1564}
1565
1566#[cfg(all(
1567 feature = "desktop",
1568 feature = "renderer-wgpu",
1569 not(target_arch = "wasm32")
1570))]
1571fn ranges_overlap(a_start: f32, a_end: f32, b_start: f32, b_end: f32, attach_epsilon: f32) -> bool {
1572 a_start <= b_end + attach_epsilon && b_start <= a_end + attach_epsilon
1573}
1574
1575#[cfg(all(
1576 feature = "desktop",
1577 feature = "renderer-wgpu",
1578 not(target_arch = "wasm32")
1579))]
1580fn ranges_overlap_strict(a_start: f32, a_end: f32, b_start: f32, b_end: f32) -> bool {
1581 a_start < b_end && b_start < a_end
1582}
1583
1584#[cfg(all(
1585 feature = "desktop",
1586 feature = "renderer-wgpu",
1587 not(target_arch = "wasm32")
1588))]
1589fn range_overlap_length(a_start: f32, a_end: f32, b_start: f32, b_end: f32) -> f32 {
1590 (a_end.min(b_end) - a_start.max(b_start)).max(0.0)
1591}
1592
1593#[cfg(test)]
1594mod tests {
1595 use super::*;
1596 use std::sync::Arc;
1597
1598 fn test_window_state(
1599 width: f32,
1600 height: f32,
1601 ) -> (
1602 cranpose_core::Runtime,
1603 cranpose_core::OwnedMutableState<Option<Point>>,
1604 cranpose_core::OwnedMutableState<Size>,
1605 WindowState,
1606 ) {
1607 let runtime = cranpose_core::Runtime::new(Arc::new(cranpose_core::DefaultScheduler));
1608 let handle = runtime.handle();
1609 let position =
1610 cranpose_core::OwnedMutableState::with_runtime(None::<Point>, handle.clone());
1611 let size = cranpose_core::OwnedMutableState::with_runtime(Size::new(width, height), handle);
1612 let state = WindowState {
1613 position: position.handle(),
1614 size: size.handle(),
1615 };
1616 (runtime, position, size, state)
1617 }
1618
1619 #[cfg(all(
1620 feature = "desktop",
1621 feature = "renderer-wgpu",
1622 not(target_arch = "wasm32")
1623 ))]
1624 fn with_request_test_registry<R>(f: impl FnOnce(&Rc<NativeWindowRegistry>) -> R) -> R {
1625 let registry = Rc::new(NativeWindowRegistry::default());
1626 with_native_window_registry(®istry, || f(®istry))
1627 }
1628
1629 #[cfg(all(
1630 feature = "desktop",
1631 feature = "renderer-wgpu",
1632 not(target_arch = "wasm32")
1633 ))]
1634 fn reset_request_test_state(registry: &NativeWindowRegistry) {
1635 clear_native_window_requests(registry);
1636 }
1637
1638 #[cfg(all(
1639 feature = "desktop",
1640 feature = "renderer-wgpu",
1641 not(target_arch = "wasm32")
1642 ))]
1643 fn request_exists(registry: &NativeWindowRegistry, key: NativeWindowKey) -> bool {
1644 native_window_requests(registry)
1645 .into_iter()
1646 .any(|request| request.key == key)
1647 }
1648
1649 #[cfg(all(
1650 feature = "desktop",
1651 feature = "renderer-wgpu",
1652 not(target_arch = "wasm32")
1653 ))]
1654 #[test]
1655 fn native_window_requests_are_isolated_by_registry() {
1656 let first_registry = Rc::new(NativeWindowRegistry::default());
1657 let second_registry = Rc::new(NativeWindowRegistry::default());
1658 let first_key = WindowId::from_static("first-registry-window");
1659 let second_key = WindowId::from_static("second-registry-window");
1660 let first_owner = Rc::new(());
1661 let second_owner = Rc::new(());
1662 let first_content = Rc::new(RefCell::new(Box::new(|| {}) as Box<dyn FnMut()>));
1663 let second_content = Rc::new(RefCell::new(Box::new(|| {}) as Box<dyn FnMut()>));
1664
1665 with_native_window_registry(&first_registry, || {
1666 register_native_window(
1667 first_key,
1668 NativeWindowOptions::new("first", 80.0, 40.0),
1669 NativeWindowEvents::default(),
1670 None,
1671 None,
1672 first_content,
1673 first_owner,
1674 );
1675 });
1676 with_native_window_registry(&second_registry, || {
1677 register_native_window(
1678 second_key,
1679 NativeWindowOptions::new("second", 90.0, 45.0),
1680 NativeWindowEvents::default(),
1681 None,
1682 None,
1683 second_content,
1684 second_owner,
1685 );
1686 });
1687
1688 let first_requests = native_window_requests(&first_registry);
1689 let second_requests = native_window_requests(&second_registry);
1690
1691 assert_eq!(first_requests.len(), 1);
1692 assert_eq!(first_requests[0].key, first_key);
1693 assert_eq!(second_requests.len(), 1);
1694 assert_eq!(second_requests[0].key, second_key);
1695 }
1696
1697 #[cfg(all(
1698 feature = "desktop",
1699 feature = "renderer-wgpu",
1700 not(target_arch = "wasm32")
1701 ))]
1702 struct RequestTestComposition {
1703 _context: Rc<cranpose_ui::AppContext>,
1704 _scope: cranpose_ui::AppContextScope,
1705 runtime: cranpose_core::Runtime,
1706 composition: cranpose_core::Composition<cranpose_core::MemoryApplier>,
1707 registry: Rc<NativeWindowRegistry>,
1708 }
1709
1710 #[cfg(all(
1711 feature = "desktop",
1712 feature = "renderer-wgpu",
1713 not(target_arch = "wasm32")
1714 ))]
1715 impl RequestTestComposition {
1716 fn with_registry<R>(
1717 &mut self,
1718 f: impl FnOnce(&mut cranpose_core::Composition<cranpose_core::MemoryApplier>) -> R,
1719 ) -> R {
1720 let registry = Rc::clone(&self.registry);
1721 with_native_window_registry(®istry, || f(&mut self.composition))
1722 }
1723 }
1724
1725 #[cfg(all(
1726 feature = "desktop",
1727 feature = "renderer-wgpu",
1728 not(target_arch = "wasm32")
1729 ))]
1730 fn test_app_context_scope() -> (Rc<cranpose_ui::AppContext>, cranpose_ui::AppContextScope) {
1731 let context = cranpose_ui::AppContext::new();
1732 let scope = context.enter_scope();
1733 (context, scope)
1734 }
1735
1736 #[cfg(all(
1737 feature = "desktop",
1738 feature = "renderer-wgpu",
1739 not(target_arch = "wasm32")
1740 ))]
1741 fn request_test_composition() -> RequestTestComposition {
1742 let (context, scope) = test_app_context_scope();
1743 let runtime = cranpose_core::Runtime::new(Arc::new(cranpose_core::DefaultScheduler));
1744 let composition = cranpose_core::Composition::with_runtime(
1745 cranpose_core::MemoryApplier::new(),
1746 runtime.clone(),
1747 );
1748 RequestTestComposition {
1749 _context: context,
1750 _scope: scope,
1751 runtime,
1752 composition,
1753 registry: Rc::new(NativeWindowRegistry::default()),
1754 }
1755 }
1756
1757 #[cfg(all(
1758 feature = "desktop",
1759 feature = "renderer-wgpu",
1760 not(target_arch = "wasm32")
1761 ))]
1762 #[composable]
1763 #[allow(non_snake_case)]
1764 fn RequestCounterText(counter: cranpose_core::MutableState<i32>) {
1765 cranpose_ui::Text(
1766 format!("Counter {}", counter.get()),
1767 cranpose_ui::Modifier::empty(),
1768 cranpose_ui::TextStyle::default(),
1769 );
1770 }
1771
1772 #[cfg(all(
1773 feature = "desktop",
1774 feature = "renderer-wgpu",
1775 not(target_arch = "wasm32")
1776 ))]
1777 #[composable]
1778 #[allow(non_snake_case)]
1779 fn PersistentRequestRoot(counter: cranpose_core::MutableState<i32>) {
1780 RequestCounterText(counter);
1781 WindowNode(
1782 WindowId::from_static("persistent-request"),
1783 WindowConfig::new("Persistent request", 100.0, 50.0),
1784 || {},
1785 );
1786 }
1787
1788 #[cfg(all(
1789 feature = "desktop",
1790 feature = "renderer-wgpu",
1791 not(target_arch = "wasm32")
1792 ))]
1793 #[composable]
1794 #[allow(non_snake_case)]
1795 fn ConditionalRequestRoot(show: cranpose_core::MutableState<bool>) {
1796 if show.get() {
1797 WindowNode(
1798 WindowId::from_static("conditional-request"),
1799 WindowConfig::new("Conditional request", 100.0, 50.0),
1800 || {},
1801 );
1802 }
1803 }
1804
1805 #[cfg(all(
1806 feature = "desktop",
1807 feature = "renderer-wgpu",
1808 not(target_arch = "wasm32")
1809 ))]
1810 #[composable]
1811 #[allow(non_snake_case)]
1812 fn KeyedReplacementRequestRoot(show: cranpose_core::MutableState<bool>) {
1813 let active = show.get();
1814 cranpose_core::with_key(&active, || {
1815 if active {
1816 WindowNode(
1817 WindowId::from_static("keyed-replacement-request"),
1818 WindowConfig::new("Keyed replacement request", 100.0, 50.0),
1819 || {},
1820 );
1821 } else {
1822 cranpose_ui::Text(
1823 "Inactive branch",
1824 cranpose_ui::Modifier::empty(),
1825 cranpose_ui::TextStyle::default(),
1826 );
1827 }
1828 });
1829 }
1830
1831 #[cfg(all(
1832 feature = "desktop",
1833 feature = "renderer-wgpu",
1834 not(target_arch = "wasm32")
1835 ))]
1836 #[test]
1837 fn native_window_request_survives_unrelated_scoped_recompose() {
1838 let mut test = request_test_composition();
1839 reset_request_test_state(&test.registry);
1840 let counter = cranpose_core::MutableState::with_runtime(0i32, test.runtime.handle());
1841 let key = WindowId::from_static("persistent-request");
1842 let root_key = cranpose_core::location_key(file!(), line!(), column!());
1843 test.with_registry(|composition| {
1844 composition
1845 .render_stable(root_key, || PersistentRequestRoot(counter))
1846 .expect("initial persistent native-window request render");
1847 });
1848 assert!(request_exists(&test.registry, key));
1849
1850 counter.set(1);
1851 test.with_registry(|composition| {
1852 composition
1853 .reconcile(root_key, || PersistentRequestRoot(counter))
1854 .expect("persistent native-window request reconcile");
1855 });
1856
1857 assert!(
1858 request_exists(&test.registry, key),
1859 "unchanged native-window declarations must stay registered when only a sibling scope recomposes"
1860 );
1861 clear_native_window_requests(&test.registry);
1862 }
1863
1864 #[cfg(all(
1865 feature = "desktop",
1866 feature = "renderer-wgpu",
1867 not(target_arch = "wasm32")
1868 ))]
1869 #[test]
1870 fn native_window_request_unregisters_when_conditional_declaration_is_removed() {
1871 let mut test = request_test_composition();
1872 reset_request_test_state(&test.registry);
1873 let show = cranpose_core::MutableState::with_runtime(true, test.runtime.handle());
1874 let key = WindowId::from_static("conditional-request");
1875 let root_key = cranpose_core::location_key(file!(), line!(), column!());
1876 test.with_registry(|composition| {
1877 composition
1878 .render_stable(root_key, || ConditionalRequestRoot(show))
1879 .expect("initial conditional native-window request render");
1880 });
1881 assert!(request_exists(&test.registry, key));
1882
1883 show.set(false);
1884 test.with_registry(|composition| {
1885 composition
1886 .reconcile(root_key, || ConditionalRequestRoot(show))
1887 .expect("conditional native-window request reconcile");
1888 });
1889
1890 assert!(
1891 !request_exists(&test.registry, key),
1892 "removed native-window declarations must unregister through their disposable owner"
1893 );
1894 clear_native_window_requests(&test.registry);
1895 }
1896
1897 #[cfg(all(
1898 feature = "desktop",
1899 feature = "renderer-wgpu",
1900 not(target_arch = "wasm32")
1901 ))]
1902 #[test]
1903 fn native_window_request_unregisters_when_keyed_branch_is_replaced() {
1904 let mut test = request_test_composition();
1905 reset_request_test_state(&test.registry);
1906 let show = cranpose_core::MutableState::with_runtime(true, test.runtime.handle());
1907 let key = WindowId::from_static("keyed-replacement-request");
1908 let root_key = cranpose_core::location_key(file!(), line!(), column!());
1909 test.with_registry(|composition| {
1910 composition
1911 .render_stable(root_key, || KeyedReplacementRequestRoot(show))
1912 .expect("initial keyed native-window request render");
1913 });
1914 assert!(request_exists(&test.registry, key));
1915
1916 show.set(false);
1917 test.with_registry(|composition| {
1918 composition
1919 .reconcile(root_key, || KeyedReplacementRequestRoot(show))
1920 .expect("keyed native-window request reconcile");
1921 });
1922
1923 assert!(
1924 !request_exists(&test.registry, key),
1925 "keyed branch replacement must unregister native-window declarations from the inactive branch"
1926 );
1927 clear_native_window_requests(&test.registry);
1928 }
1929
1930 #[test]
1931 fn borderless_options_disable_decorations_and_resizing() {
1932 let options = NativeWindowOptions::borderless("Tool", 100.0, 50.0);
1933 assert_eq!(options.title, "Tool");
1934 assert_eq!(options.width, 100.0);
1935 assert_eq!(options.height, 50.0);
1936 assert_eq!(options.position_origin, NativeWindowPositionOrigin::Screen);
1937 assert!(!options.decorations);
1938 assert!(!options.resizable);
1939 assert!(options.visible);
1940 }
1941
1942 #[test]
1943 fn option_builders_update_specific_fields() {
1944 let options = NativeWindowOptions::new("Panel", 10.0, 20.0)
1945 .with_position(3.0, 4.0)
1946 .with_transparent(true)
1947 .with_resizable(false)
1948 .with_visible(false)
1949 .with_always_on_top(true)
1950 .with_min_size(5.0, 6.0)
1951 .with_max_size(50.0, 60.0);
1952 assert_eq!(options.x, Some(3.0));
1953 assert_eq!(options.y, Some(4.0));
1954 assert_eq!(options.position_origin, NativeWindowPositionOrigin::Screen);
1955 assert!(options.transparent);
1956 assert!(!options.resizable);
1957 assert!(!options.visible);
1958 assert!(options.always_on_top);
1959 assert_eq!(options.min_width, Some(5.0));
1960 assert_eq!(options.min_height, Some(6.0));
1961 assert_eq!(options.max_width, Some(50.0));
1962 assert_eq!(options.max_height, Some(60.0));
1963 }
1964
1965 #[test]
1966 fn host_window_position_records_origin() {
1967 let options =
1968 NativeWindowOptions::new("Panel", 10.0, 20.0).with_host_window_position(3.0, 4.0);
1969 assert_eq!(options.x, Some(3.0));
1970 assert_eq!(options.y, Some(4.0));
1971 assert_eq!(
1972 options.position_origin,
1973 NativeWindowPositionOrigin::HostWindow
1974 );
1975 }
1976
1977 #[test]
1978 fn events_builder_registers_move_callback() {
1979 let events = NativeWindowEvents::new().with_on_moved(|_, _| {});
1980 assert!(events.on_moved.is_some());
1981 }
1982
1983 #[test]
1984 fn window_state_accessors_update_position_and_size() {
1985 let (_runtime, _position_owner, _size_owner, state) = test_window_state(100.0, 50.0);
1986
1987 assert_eq!(state.position_non_reactive(), None);
1988 assert_eq!(state.size_non_reactive(), Size::new(100.0, 50.0));
1989
1990 state.set_position(Some(Point::new(4.0, 8.0)));
1991 assert_eq!(state.position_non_reactive(), Some(Point::new(4.0, 8.0)));
1992
1993 state.translate(3.0, -2.0);
1994 assert_eq!(state.position_non_reactive(), Some(Point::new(7.0, 6.0)));
1995
1996 state.set_size(Size::new(120.0, 64.0));
1997 assert_eq!(state.size_non_reactive(), Size::new(120.0, 64.0));
1998 }
1999
2000 #[test]
2001 fn window_config_collects_window_settings_and_callbacks() {
2002 let config = WindowConfig::borderless("Panel", 100.0, 50.0)
2003 .with_host_window_position(7.0, 9.0)
2004 .with_transparent(true)
2005 .with_resizable(false)
2006 .with_visible(false)
2007 .with_always_on_top(true)
2008 .with_min_size(20.0, 10.0)
2009 .with_max_size(400.0, 200.0)
2010 .on_moved(|_, _| {})
2011 .on_resized(|_, _| {})
2012 .on_close_requested(|| {});
2013
2014 let (options, callbacks, state) = config.into_parts();
2015 assert_eq!(options.title, "Panel");
2016 assert_eq!(options.width, 100.0);
2017 assert_eq!(options.height, 50.0);
2018 assert_eq!(options.x, Some(7.0));
2019 assert_eq!(options.y, Some(9.0));
2020 assert_eq!(
2021 options.position_origin,
2022 NativeWindowPositionOrigin::HostWindow
2023 );
2024 assert!(!options.decorations);
2025 assert!(options.transparent);
2026 assert!(!options.resizable);
2027 assert!(!options.visible);
2028 assert!(options.always_on_top);
2029 assert_eq!(options.min_width, Some(20.0));
2030 assert_eq!(options.min_height, Some(10.0));
2031 assert_eq!(options.max_width, Some(400.0));
2032 assert_eq!(options.max_height, Some(200.0));
2033 assert!(callbacks.on_moved.is_some());
2034 assert!(callbacks.on_resized.is_some());
2035 assert!(callbacks.on_close_requested.is_some());
2036 assert!(state.is_none());
2037 }
2038
2039 #[test]
2040 fn state_window_configs_bind_size_position() {
2041 let (_runtime, _position_owner, _size_owner, state) = test_window_state(100.0, 50.0);
2042 state.set_position(Some(Point::new(7.0, 9.0)));
2043
2044 let (options, callbacks, bound_state) =
2045 WindowConfig::borderless_for_state("Panel", state).into_parts();
2046 assert_eq!(options.title, "Panel");
2047 assert_eq!(options.width, 100.0);
2048 assert_eq!(options.height, 50.0);
2049 assert_eq!(options.x, Some(7.0));
2050 assert_eq!(options.y, Some(9.0));
2051 assert!(!options.decorations);
2052 assert!(!options.resizable);
2053 assert!(bound_state == Some(state));
2054 assert!(callbacks.on_moved.is_none());
2055 assert!(callbacks.on_resized.is_none());
2056
2057 state.set_size(Size::new(320.0, 200.0));
2058
2059 let (decorated_options, _, decorated_state) =
2060 WindowConfig::new_for_state("Decorated", state).into_parts();
2061 assert_eq!(decorated_options.width, 320.0);
2062 assert_eq!(decorated_options.height, 200.0);
2063 assert!(decorated_options.decorations);
2064 assert!(decorated_options.resizable);
2065 assert!(decorated_state == Some(state));
2066 }
2067
2068 #[test]
2069 fn drag_request_reports_missing_handler() {
2070 assert!(!request_native_window_drag());
2071 }
2072
2073 #[cfg(all(
2074 feature = "desktop",
2075 feature = "renderer-wgpu",
2076 not(target_arch = "wasm32")
2077 ))]
2078 #[test]
2079 fn drag_request_uses_handler_result() {
2080 let resize_handler: NativeWindowResizeHandler = Rc::new(|_| {});
2081
2082 with_native_window_drag_handler(Rc::new(|| true), Rc::clone(&resize_handler), || {
2083 assert!(request_native_window_drag());
2084 });
2085 with_native_window_drag_handler(Rc::new(|| false), resize_handler, || {
2086 assert!(!request_native_window_drag());
2087 });
2088
2089 assert!(!request_native_window_drag());
2090 }
2091
2092 #[cfg(all(
2093 feature = "desktop",
2094 feature = "renderer-wgpu",
2095 not(target_arch = "wasm32")
2096 ))]
2097 #[test]
2098 fn native_window_drag_handler_restores_outer_scope_after_nested_scope() {
2099 let outer_resize_calls = Rc::new(Cell::new(0));
2100 let inner_resize_calls = Rc::new(Cell::new(0));
2101 let outer_resize_handler: NativeWindowResizeHandler = {
2102 let outer_resize_calls = Rc::clone(&outer_resize_calls);
2103 Rc::new(move |_| outer_resize_calls.set(outer_resize_calls.get() + 1))
2104 };
2105 let inner_resize_handler: NativeWindowResizeHandler = {
2106 let inner_resize_calls = Rc::clone(&inner_resize_calls);
2107 Rc::new(move |_| inner_resize_calls.set(inner_resize_calls.get() + 1))
2108 };
2109
2110 with_native_window_drag_handler(Rc::new(|| true), outer_resize_handler, || {
2111 assert!(request_native_window_drag());
2112 assert!(request_native_window_resize(
2113 WindowResizeDirection::SouthEast
2114 ));
2115
2116 with_native_window_drag_handler(Rc::new(|| false), inner_resize_handler, || {
2117 assert!(!request_native_window_drag());
2118 assert!(request_native_window_resize(
2119 WindowResizeDirection::NorthWest
2120 ));
2121 });
2122
2123 assert!(request_native_window_drag());
2124 assert!(request_native_window_resize(
2125 WindowResizeDirection::SouthEast
2126 ));
2127 });
2128
2129 assert!(!request_native_window_drag());
2130 assert_eq!(outer_resize_calls.get(), 2);
2131 assert_eq!(inner_resize_calls.get(), 1);
2132 }
2133
2134 #[cfg(all(
2135 feature = "desktop",
2136 feature = "renderer-wgpu",
2137 not(target_arch = "wasm32")
2138 ))]
2139 #[test]
2140 fn drag_area_callbacks_follow_accepted_native_drag_lifecycle() {
2141 use cranpose_ui::{collect_slices_from_modifier, PointerEvent};
2142 use std::cell::Cell;
2143
2144 let (_app_context, _app_context_scope) = test_app_context_scope();
2145 let started = Rc::new(Cell::new(0));
2146 let finished = Rc::new(Cell::new(0));
2147 let modifier = Modifier::empty().window_drag_area_with_callbacks(
2148 {
2149 let started = Rc::clone(&started);
2150 move || started.set(started.get() + 1)
2151 },
2152 {
2153 let finished = Rc::clone(&finished);
2154 move || finished.set(finished.get() + 1)
2155 },
2156 );
2157 let slices = collect_slices_from_modifier(&modifier);
2158 let handler = slices
2159 .pointer_inputs()
2160 .first()
2161 .expect("window drag pointer handler")
2162 .clone();
2163 let resize_handler: NativeWindowResizeHandler = Rc::new(|_| {});
2164
2165 with_native_window_drag_handler(Rc::new(|| true), resize_handler, || {
2166 let down = PointerEvent::new(
2167 PointerEventKind::Down,
2168 Point::new(4.0, 5.0),
2169 Point::new(4.0, 5.0),
2170 );
2171 handler(down.clone());
2172 assert!(down.is_consumed());
2173
2174 handler(PointerEvent::new(
2175 PointerEventKind::Up,
2176 Point::new(4.0, 5.0),
2177 Point::new(4.0, 5.0),
2178 ));
2179 });
2180
2181 assert_eq!(started.get(), 1);
2182 assert_eq!(finished.get(), 1);
2183 }
2184
2185 #[test]
2186 fn resize_request_reports_missing_handler() {
2187 assert!(!request_native_window_resize(
2188 WindowResizeDirection::SouthEast
2189 ));
2190 }
2191
2192 #[cfg(all(
2193 feature = "desktop",
2194 feature = "renderer-wgpu",
2195 not(target_arch = "wasm32")
2196 ))]
2197 #[test]
2198 fn native_window_surface_origin_is_scoped_to_dispatch() {
2199 assert_eq!(current_native_window_surface_origin(), None);
2200
2201 with_native_window_surface_origin(Some(Point::new(4.0, 8.0)), || {
2202 assert_eq!(
2203 current_native_window_surface_origin(),
2204 Some(Point::new(4.0, 8.0))
2205 );
2206 with_native_window_surface_origin(Some(Point::new(12.0, 16.0)), || {
2207 assert_eq!(
2208 current_native_window_surface_origin(),
2209 Some(Point::new(12.0, 16.0))
2210 );
2211 });
2212 assert_eq!(
2213 current_native_window_surface_origin(),
2214 Some(Point::new(4.0, 8.0))
2215 );
2216 });
2217
2218 assert_eq!(current_native_window_surface_origin(), None);
2219 }
2220
2221 #[cfg(all(
2222 feature = "desktop",
2223 feature = "renderer-wgpu",
2224 not(target_arch = "wasm32")
2225 ))]
2226 #[test]
2227 fn static_keys_are_stable() {
2228 assert_eq!(
2229 NativeWindowKey::from_static("stable"),
2230 NativeWindowKey::from_static("stable")
2231 );
2232 assert_ne!(
2233 NativeWindowKey::from_static("stable"),
2234 NativeWindowKey::from_static("other")
2235 );
2236 }
2237
2238 #[cfg(all(
2239 feature = "desktop",
2240 feature = "renderer-wgpu",
2241 not(target_arch = "wasm32")
2242 ))]
2243 fn graph_group(policy: WindowAttachPolicy) -> NativeWindowGroupMembership {
2244 NativeWindowGroupMembership {
2245 id: WindowGroupId::from_static("test-group"),
2246 policy,
2247 }
2248 }
2249
2250 #[cfg(all(
2251 feature = "desktop",
2252 feature = "renderer-wgpu",
2253 not(target_arch = "wasm32")
2254 ))]
2255 fn graph_node(
2256 id: &'static str,
2257 position: Point,
2258 size: Size,
2259 group: &NativeWindowGroupMembership,
2260 ) -> WindowGraphPeerSnapshot {
2261 WindowGraphPeerSnapshot {
2262 node: WindowGraphNodeSnapshot {
2263 id: WindowId::from_static(id),
2264 position,
2265 size,
2266 },
2267 group: Some(group.clone()),
2268 }
2269 }
2270
2271 #[cfg(all(
2272 feature = "desktop",
2273 feature = "renderer-wgpu",
2274 not(target_arch = "wasm32")
2275 ))]
2276 fn graph_position(moves: &[WindowGraphMove], id: WindowId) -> Option<Point> {
2277 moves
2278 .iter()
2279 .find(|window_move| window_move.id == id)
2280 .map(|window_move| window_move.position)
2281 }
2282
2283 #[cfg(all(
2284 feature = "desktop",
2285 feature = "renderer-wgpu",
2286 not(target_arch = "wasm32")
2287 ))]
2288 #[test]
2289 fn graph_drag_capture_freezes_attached_component() {
2290 let main = WindowId::from_static("main");
2291 let eq = WindowId::from_static("eq");
2292 let playlist = WindowId::from_static("playlist");
2293 let group = graph_group(WindowAttachPolicy::default());
2294 let windows = vec![
2295 graph_node(
2296 "main",
2297 Point::new(100.0, 100.0),
2298 Size::new(100.0, 50.0),
2299 &group,
2300 ),
2301 graph_node(
2302 "eq",
2303 Point::new(100.0, 150.0),
2304 Size::new(100.0, 50.0),
2305 &group,
2306 ),
2307 graph_node(
2308 "playlist",
2309 Point::new(240.0, 150.0),
2310 Size::new(100.0, 50.0),
2311 &group,
2312 ),
2313 ];
2314
2315 let mut graph = WindowGraphState::default();
2316 graph.start_drag(&windows, main);
2317 let moves = graph.drag_to(main, Point::new(120.0, 100.0));
2318
2319 assert_eq!(graph_position(&moves, main), Some(Point::new(120.0, 100.0)));
2320 assert_eq!(graph_position(&moves, eq), Some(Point::new(120.0, 150.0)));
2321 assert_eq!(graph_position(&moves, playlist), None);
2322 }
2323
2324 #[cfg(all(
2325 feature = "desktop",
2326 feature = "renderer-wgpu",
2327 not(target_arch = "wasm32")
2328 ))]
2329 #[test]
2330 fn graph_does_not_attach_new_window_during_drag() {
2331 let main = WindowId::from_static("main");
2332 let playlist = WindowId::from_static("playlist");
2333 let group = graph_group(WindowAttachPolicy::default());
2334 let windows = vec![
2335 graph_node(
2336 "main",
2337 Point::new(100.0, 100.0),
2338 Size::new(100.0, 50.0),
2339 &group,
2340 ),
2341 graph_node(
2342 "playlist",
2343 Point::new(216.0, 100.0),
2344 Size::new(100.0, 50.0),
2345 &group,
2346 ),
2347 ];
2348
2349 let mut graph = WindowGraphState::default();
2350 graph.start_drag(&windows, main);
2351 let moves = graph.drag_to(main, Point::new(112.0, 100.0));
2352
2353 assert_eq!(graph_position(&moves, main), Some(Point::new(112.0, 100.0)));
2354 assert_eq!(graph_position(&moves, playlist), None);
2355 }
2356
2357 #[cfg(all(
2358 feature = "desktop",
2359 feature = "renderer-wgpu",
2360 not(target_arch = "wasm32")
2361 ))]
2362 #[test]
2363 fn graph_does_not_detach_captured_component_during_fast_drag() {
2364 let main = WindowId::from_static("main");
2365 let eq = WindowId::from_static("eq");
2366 let group = graph_group(WindowAttachPolicy::default());
2367 let windows = vec![
2368 graph_node(
2369 "main",
2370 Point::new(100.0, 100.0),
2371 Size::new(100.0, 50.0),
2372 &group,
2373 ),
2374 graph_node(
2375 "eq",
2376 Point::new(100.0, 150.0),
2377 Size::new(100.0, 50.0),
2378 &group,
2379 ),
2380 ];
2381
2382 let mut graph = WindowGraphState::default();
2383 graph.start_drag(&windows, main);
2384 let moves = graph.drag_to(main, Point::new(400.0, 280.0));
2385
2386 assert_eq!(graph_position(&moves, main), Some(Point::new(400.0, 280.0)));
2387 assert_eq!(graph_position(&moves, eq), Some(Point::new(400.0, 330.0)));
2388 }
2389
2390 #[cfg(all(
2391 feature = "desktop",
2392 feature = "renderer-wgpu",
2393 not(target_arch = "wasm32")
2394 ))]
2395 #[test]
2396 fn graph_release_recomputes_attachment_once() {
2397 let main = WindowId::from_static("main");
2398 let playlist = WindowId::from_static("playlist");
2399 let group = graph_group(WindowAttachPolicy::default());
2400 let start = vec![
2401 graph_node(
2402 "main",
2403 Point::new(100.0, 100.0),
2404 Size::new(100.0, 50.0),
2405 &group,
2406 ),
2407 graph_node(
2408 "playlist",
2409 Point::new(216.0, 100.0),
2410 Size::new(100.0, 50.0),
2411 &group,
2412 ),
2413 ];
2414 let finish = vec![
2415 graph_node(
2416 "main",
2417 Point::new(112.0, 100.0),
2418 Size::new(100.0, 50.0),
2419 &group,
2420 ),
2421 graph_node(
2422 "playlist",
2423 Point::new(216.0, 100.0),
2424 Size::new(100.0, 50.0),
2425 &group,
2426 ),
2427 ];
2428
2429 let mut graph = WindowGraphState::default();
2430 graph.start_drag(&start, main);
2431 let release_moves = graph.finish_drag(&finish);
2432 let second_release_moves = graph.finish_drag(&finish);
2433
2434 assert_eq!(
2435 graph_position(&release_moves, main),
2436 Some(Point::new(116.0, 100.0))
2437 );
2438 assert_eq!(
2439 graph_position(&release_moves, playlist),
2440 Some(Point::new(216.0, 100.0))
2441 );
2442 assert!(second_release_moves.is_empty());
2443 }
2444
2445 #[cfg(all(
2446 feature = "desktop",
2447 feature = "renderer-wgpu",
2448 not(target_arch = "wasm32")
2449 ))]
2450 #[test]
2451 fn graph_release_uses_drag_start_component_for_attached_windows() {
2452 let main = WindowId::from_static("main");
2453 let group = graph_group(WindowAttachPolicy::default());
2454 let start = vec![
2455 graph_node(
2456 "main",
2457 Point::new(100.0, 100.0),
2458 Size::new(100.0, 50.0),
2459 &group,
2460 ),
2461 graph_node(
2462 "equalizer",
2463 Point::new(100.0, 150.0),
2464 Size::new(100.0, 50.0),
2465 &group,
2466 ),
2467 graph_node(
2468 "playlist",
2469 Point::new(200.0, 150.0),
2470 Size::new(100.0, 50.0),
2471 &group,
2472 ),
2473 ];
2474 let finish_with_peer_position_lag = vec![
2475 graph_node(
2476 "main",
2477 Point::new(112.0, 100.0),
2478 Size::new(100.0, 50.0),
2479 &group,
2480 ),
2481 graph_node(
2482 "equalizer",
2483 Point::new(112.0, 150.0),
2484 Size::new(100.0, 50.0),
2485 &group,
2486 ),
2487 graph_node(
2488 "playlist",
2489 Point::new(216.0, 150.0),
2490 Size::new(100.0, 50.0),
2491 &group,
2492 ),
2493 ];
2494
2495 let mut graph = WindowGraphState::default();
2496 graph.start_drag(&start, main);
2497
2498 assert!(
2499 graph
2500 .finish_drag(&finish_with_peer_position_lag)
2501 .is_empty(),
2502 "release snapping must not drop a start-captured peer from the moving component because OS move events arrived out of phase"
2503 );
2504 }
2505
2506 #[cfg(all(
2507 feature = "desktop",
2508 feature = "renderer-wgpu",
2509 not(target_arch = "wasm32")
2510 ))]
2511 #[test]
2512 fn graph_cancel_drag_discards_active_capture_without_release_moves() {
2513 let main = WindowId::from_static("main");
2514 let group = graph_group(WindowAttachPolicy::default());
2515 let windows = vec![
2516 graph_node(
2517 "main",
2518 Point::new(100.0, 100.0),
2519 Size::new(100.0, 50.0),
2520 &group,
2521 ),
2522 graph_node(
2523 "playlist",
2524 Point::new(216.0, 100.0),
2525 Size::new(100.0, 50.0),
2526 &group,
2527 ),
2528 ];
2529
2530 let mut graph = WindowGraphState::default();
2531 graph.start_drag(&windows, main);
2532 graph.cancel_drag();
2533
2534 assert!(graph.finish_drag(&windows).is_empty());
2535 }
2536
2537 #[cfg(all(
2538 feature = "desktop",
2539 feature = "renderer-wgpu",
2540 not(target_arch = "wasm32")
2541 ))]
2542 #[test]
2543 fn graph_drag_leader_only_moves_attached_component() {
2544 let main = WindowId::from_static("main");
2545 let eq = WindowId::from_static("eq");
2546 let group = graph_group(WindowAttachPolicy::new(
2547 8.0,
2548 3.0,
2549 WindowMoveMode::DragLeaderOnly(vec![main]),
2550 ));
2551 let windows = vec![
2552 graph_node(
2553 "main",
2554 Point::new(100.0, 100.0),
2555 Size::new(100.0, 50.0),
2556 &group,
2557 ),
2558 graph_node(
2559 "eq",
2560 Point::new(100.0, 150.0),
2561 Size::new(100.0, 50.0),
2562 &group,
2563 ),
2564 ];
2565
2566 let mut graph = WindowGraphState::default();
2567 graph.start_drag(&windows, eq);
2568 let eq_moves = graph.drag_to(eq, Point::new(130.0, 170.0));
2569 graph.start_drag(&windows, main);
2570 let main_moves = graph.drag_to(main, Point::new(130.0, 110.0));
2571
2572 assert_eq!(
2573 graph_position(&eq_moves, eq),
2574 Some(Point::new(130.0, 170.0))
2575 );
2576 assert_eq!(graph_position(&eq_moves, main), None);
2577 assert_eq!(
2578 graph_position(&main_moves, main),
2579 Some(Point::new(130.0, 110.0))
2580 );
2581 assert_eq!(
2582 graph_position(&main_moves, eq),
2583 Some(Point::new(130.0, 160.0))
2584 );
2585 }
2586
2587 #[cfg(all(
2588 feature = "desktop",
2589 feature = "renderer-wgpu",
2590 not(target_arch = "wasm32")
2591 ))]
2592 fn test_owner() -> NativeWindowOwner {
2593 Rc::new(())
2594 }
2595
2596 #[cfg(all(
2597 feature = "desktop",
2598 feature = "renderer-wgpu",
2599 not(target_arch = "wasm32")
2600 ))]
2601 #[test]
2602 fn registry_replaces_native_window_declarations_by_key() {
2603 with_request_test_registry(|registry| {
2604 clear_native_window_requests(registry);
2605
2606 let key = NativeWindowKey::from_static("visibility-update");
2607 let owner = test_owner();
2608 let content: NativeWindowContent =
2609 Rc::new(RefCell::new(Box::new(|| {}) as Box<dyn FnMut()>));
2610
2611 register_native_window(
2612 key,
2613 NativeWindowOptions::new("Panel", 100.0, 50.0).with_visible(true),
2614 NativeWindowEvents::new(),
2615 None,
2616 None,
2617 Rc::clone(&content),
2618 Rc::clone(&owner),
2619 );
2620 let initial_revision = native_window_requests(registry)
2621 .into_iter()
2622 .next()
2623 .expect("first native window request")
2624 .revision;
2625
2626 register_native_window(
2627 key,
2628 NativeWindowOptions::new("Panel", 100.0, 50.0).with_visible(false),
2629 NativeWindowEvents::new(),
2630 None,
2631 None,
2632 content,
2633 owner,
2634 );
2635
2636 let requests = native_window_requests(registry);
2637 assert_eq!(requests.len(), 1);
2638 assert_eq!(requests[0].key, key);
2639 assert_ne!(requests[0].revision, initial_revision);
2640 assert!(!requests[0].options.visible);
2641
2642 clear_native_window_requests(registry);
2643 assert!(native_window_requests(registry).is_empty());
2644 });
2645 }
2646
2647 #[cfg(all(
2648 feature = "desktop",
2649 feature = "renderer-wgpu",
2650 not(target_arch = "wasm32")
2651 ))]
2652 #[test]
2653 fn registry_revisions_change_on_each_declaration() {
2654 with_request_test_registry(|registry| {
2655 clear_native_window_requests(registry);
2656
2657 let key = NativeWindowKey::from_static("content-update");
2658 let owner = test_owner();
2659 let first_content: NativeWindowContent =
2660 Rc::new(RefCell::new(Box::new(|| {}) as Box<dyn FnMut()>));
2661 let second_content: NativeWindowContent =
2662 Rc::new(RefCell::new(Box::new(|| {}) as Box<dyn FnMut()>));
2663
2664 register_native_window(
2665 key,
2666 NativeWindowOptions::new("Panel", 100.0, 50.0),
2667 NativeWindowEvents::new(),
2668 None,
2669 None,
2670 first_content,
2671 Rc::clone(&owner),
2672 );
2673 let first_revision = native_window_requests(registry)
2674 .into_iter()
2675 .next()
2676 .expect("first native window request")
2677 .revision;
2678
2679 register_native_window(
2680 key,
2681 NativeWindowOptions::new("Panel", 100.0, 50.0),
2682 NativeWindowEvents::new(),
2683 None,
2684 None,
2685 second_content,
2686 owner,
2687 );
2688 let second_revision = native_window_requests(registry)
2689 .into_iter()
2690 .next()
2691 .expect("second native window request")
2692 .revision;
2693
2694 assert_ne!(first_revision, second_revision);
2695
2696 clear_native_window_requests(registry);
2697 });
2698 }
2699
2700 #[cfg(all(
2701 feature = "desktop",
2702 feature = "renderer-wgpu",
2703 not(target_arch = "wasm32")
2704 ))]
2705 #[test]
2706 fn registry_revisions_change_when_same_content_is_updated() {
2707 with_request_test_registry(|registry| {
2708 clear_native_window_requests(registry);
2709
2710 let key = NativeWindowKey::from_static("same-content-update");
2711 let owner = test_owner();
2712 let content: NativeWindowContent =
2713 Rc::new(RefCell::new(Box::new(|| {}) as Box<dyn FnMut()>));
2714
2715 register_native_window(
2716 key,
2717 NativeWindowOptions::new("Panel", 100.0, 50.0),
2718 NativeWindowEvents::new(),
2719 None,
2720 None,
2721 Rc::clone(&content),
2722 Rc::clone(&owner),
2723 );
2724 let first_revision = native_window_requests(registry)
2725 .into_iter()
2726 .next()
2727 .expect("first native window request")
2728 .revision;
2729
2730 register_native_window(
2731 key,
2732 NativeWindowOptions::new("Panel", 100.0, 50.0),
2733 NativeWindowEvents::new(),
2734 None,
2735 None,
2736 content,
2737 owner,
2738 );
2739 let second_revision = native_window_requests(registry)
2740 .into_iter()
2741 .next()
2742 .expect("second native window request")
2743 .revision;
2744
2745 assert_ne!(first_revision, second_revision);
2746
2747 clear_native_window_requests(registry);
2748 });
2749 }
2750
2751 #[cfg(all(
2752 feature = "desktop",
2753 feature = "renderer-wgpu",
2754 not(target_arch = "wasm32")
2755 ))]
2756 #[test]
2757 fn unregister_ignores_stale_owner_after_redeclaration() {
2758 with_request_test_registry(|registry| {
2759 clear_native_window_requests(registry);
2760
2761 let key = NativeWindowKey::from_static("reattach-window");
2762 let stale_owner = test_owner();
2763 let current_owner = test_owner();
2764 let first_content: NativeWindowContent =
2765 Rc::new(RefCell::new(Box::new(|| {}) as Box<dyn FnMut()>));
2766 let second_content: NativeWindowContent =
2767 Rc::new(RefCell::new(Box::new(|| {}) as Box<dyn FnMut()>));
2768
2769 register_native_window(
2770 key,
2771 NativeWindowOptions::new("Panel", 100.0, 50.0),
2772 NativeWindowEvents::new(),
2773 None,
2774 None,
2775 Rc::clone(&first_content),
2776 Rc::clone(&stale_owner),
2777 );
2778
2779 register_native_window(
2780 key,
2781 NativeWindowOptions::new("Panel", 100.0, 50.0),
2782 NativeWindowEvents::new(),
2783 None,
2784 None,
2785 Rc::clone(&second_content),
2786 Rc::clone(¤t_owner),
2787 );
2788 unregister_native_window(key, stale_owner);
2789
2790 let requests = native_window_requests(registry);
2791 assert_eq!(requests.len(), 1);
2792 assert_eq!(requests[0].key, key);
2793 assert!(Rc::ptr_eq(&requests[0].content, &second_content));
2794
2795 unregister_native_window(key, current_owner);
2796 assert!(native_window_requests(registry).is_empty());
2797
2798 clear_native_window_requests(registry);
2799 });
2800 }
2801
2802 #[cfg(all(
2803 feature = "desktop",
2804 feature = "renderer-wgpu",
2805 not(target_arch = "wasm32")
2806 ))]
2807 #[test]
2808 fn clear_does_not_reuse_same_content_revision() {
2809 with_request_test_registry(|registry| {
2810 clear_native_window_requests(registry);
2811
2812 let key = NativeWindowKey::from_static("remove-window");
2813 let owner = test_owner();
2814 let content: NativeWindowContent =
2815 Rc::new(RefCell::new(Box::new(|| {}) as Box<dyn FnMut()>));
2816
2817 register_native_window(
2818 key,
2819 NativeWindowOptions::new("Panel", 100.0, 50.0),
2820 NativeWindowEvents::new(),
2821 None,
2822 None,
2823 Rc::clone(&content),
2824 Rc::clone(&owner),
2825 );
2826 let first_revision = native_window_requests(registry)
2827 .into_iter()
2828 .next()
2829 .expect("registered native window request")
2830 .revision;
2831
2832 clear_native_window_requests(registry);
2833 assert!(native_window_requests(registry).is_empty());
2834
2835 register_native_window(
2836 key,
2837 NativeWindowOptions::new("Panel", 100.0, 50.0),
2838 NativeWindowEvents::new(),
2839 None,
2840 None,
2841 content,
2842 owner,
2843 );
2844 let second_revision = native_window_requests(registry)
2845 .into_iter()
2846 .next()
2847 .expect("registered native window request after cleanup")
2848 .revision;
2849
2850 assert_ne!(first_revision, second_revision);
2851
2852 clear_native_window_requests(registry);
2853 });
2854 }
2855}