Skip to main content

gpui/platform/
platform_view.rs

1//! Hosting of native platform views inside GPUI windows.
2//!
3//! A [`PlatformViewHandle`] refers to a view owned by the operating system's
4//! toolkit — an `NSView` on macOS or a child `HWND` on Windows. The
5//! [`crate::platform_view`] element gives such a view a place in GPUI's layout;
6//! GPUI owns its frame from then on and the platform layer repositions it after
7//! each drawn frame.
8//!
9//! Everything in this module apart from the handle itself is platform neutral:
10//! the frame-to-frame diffing, the device-pixel snapping and the y-flip used by
11//! bottom-left-origin coordinate systems live here so they can be unit tested
12//! without a window.
13
14use crate::{Bounds, DevicePixels, Pixels, Point, point, px, size, util::round_to_device_pixel};
15use std::fmt;
16
17/// The stable identity of a hosted platform view.
18///
19/// Two handles referring to the same native view compare equal.
20#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub struct PlatformViewId(usize);
22
23impl PlatformViewId {
24    /// Returns the identity as an opaque integer, useful for logging and for
25    /// platform layers that key their own bookkeeping by view identity.
26    pub fn as_usize(self) -> usize {
27        self.0
28    }
29}
30
31impl fmt::Debug for PlatformViewId {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write!(f, "PlatformViewId({:#x})", self.0)
34    }
35}
36
37#[cfg(target_os = "macos")]
38mod handle {
39    use super::PlatformViewId;
40    use objc::{msg_send, runtime::Object, sel, sel_impl};
41    use std::{ffi::c_void, fmt, ptr::NonNull};
42
43    /// A retained reference to a native view hosted inside a GPUI window.
44    ///
45    /// On macOS this owns a strong reference to an `NSView`. Cloning retains and
46    /// dropping releases, so the view outlives every element that paints it.
47    ///
48    /// Construct, clone and drop handles on the main thread: the wrapped object
49    /// is an AppKit view, and AppKit only promises main-thread safety.
50    pub struct PlatformViewHandle {
51        view: NonNull<Object>,
52    }
53
54    impl PlatformViewHandle {
55        /// Wraps an `NSView` so it can be hosted by a GPUI window. The view is
56        /// retained for the lifetime of the handle.
57        ///
58        /// # Safety
59        ///
60        /// `ns_view` must be a non-null pointer to a live `NSView` instance, and
61        /// this must be called on the main thread.
62        pub unsafe fn from_ns_view(ns_view: *mut c_void) -> Self {
63            let view = NonNull::new(ns_view.cast::<Object>())
64                .expect("PlatformViewHandle::from_ns_view requires a non-null NSView");
65            unsafe {
66                let _: *mut Object = msg_send![view.as_ptr(), retain];
67            }
68            Self { view }
69        }
70
71        /// Returns the hosted `NSView` pointer without transferring ownership.
72        pub fn as_ns_view(&self) -> *mut c_void {
73            self.view.as_ptr().cast()
74        }
75
76        /// Returns this view's stable identity.
77        pub fn id(&self) -> PlatformViewId {
78            PlatformViewId(self.view.as_ptr() as usize)
79        }
80    }
81
82    impl Clone for PlatformViewHandle {
83        fn clone(&self) -> Self {
84            unsafe {
85                let _: *mut Object = msg_send![self.view.as_ptr(), retain];
86            }
87            Self { view: self.view }
88        }
89    }
90
91    impl Drop for PlatformViewHandle {
92        fn drop(&mut self) {
93            unsafe {
94                let _: () = msg_send![self.view.as_ptr(), release];
95            }
96        }
97    }
98
99    impl fmt::Debug for PlatformViewHandle {
100        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101            f.debug_struct("PlatformViewHandle")
102                .field("id", &self.id())
103                .finish()
104        }
105    }
106
107    impl PartialEq for PlatformViewHandle {
108        fn eq(&self, other: &Self) -> bool {
109            self.id() == other.id()
110        }
111    }
112
113    impl Eq for PlatformViewHandle {}
114}
115
116#[cfg(target_os = "windows")]
117mod handle {
118    use super::PlatformViewId;
119    use std::fmt;
120    use windows::Win32::Foundation::HWND;
121
122    /// A non-owning reference to a child `HWND` hosted inside a GPUI window.
123    ///
124    /// Windows does not provide retain/release semantics for window handles.
125    /// Cloning duplicates this reference without transferring lifetime
126    /// ownership. The component that created the `HWND` remains responsible for
127    /// destroying it after it is no longer hosted and all handles have been
128    /// dropped.
129    #[derive(Clone)]
130    pub struct PlatformViewHandle {
131        hwnd: HWND,
132    }
133
134    impl PlatformViewHandle {
135        /// Wraps a child `HWND` so it can be hosted by a GPUI window.
136        ///
137        /// # Safety
138        ///
139        /// `hwnd` must be a non-null, live child window owned by the calling
140        /// process, and must have been created on the thread that owns the GPUI
141        /// window hosting it — hosting reparents it, and reparenting across
142        /// threads would attach their input queues to each other. It must remain
143        /// valid, and must not be destroyed, for as long as this handle or any
144        /// clone is painted or retained by GPUI. The caller remains responsible
145        /// for destroying it on its owning thread.
146        pub unsafe fn from_hwnd(hwnd: HWND) -> Self {
147            assert!(
148                !hwnd.is_invalid(),
149                "PlatformViewHandle::from_hwnd requires a non-null HWND"
150            );
151            Self { hwnd }
152        }
153
154        /// Returns the hosted child `HWND` without transferring ownership.
155        pub fn as_hwnd(&self) -> HWND {
156            self.hwnd
157        }
158
159        /// Returns this view's stable identity.
160        pub fn id(&self) -> PlatformViewId {
161            PlatformViewId(self.hwnd.0 as usize)
162        }
163    }
164
165    impl fmt::Debug for PlatformViewHandle {
166        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167            f.debug_struct("PlatformViewHandle")
168                .field("id", &self.id())
169                .finish()
170        }
171    }
172
173    impl PartialEq for PlatformViewHandle {
174        fn eq(&self, other: &Self) -> bool {
175            self.id() == other.id()
176        }
177    }
178
179    impl Eq for PlatformViewHandle {}
180}
181
182#[cfg(not(any(target_os = "macos", target_os = "windows")))]
183mod handle {
184    use super::PlatformViewId;
185    use std::sync::atomic::{AtomicUsize, Ordering};
186
187    /// An inert stand-in for a natively hosted view.
188    ///
189    /// This stub exists so cross-platform code that mentions
190    /// [`PlatformViewHandle`] still compiles; it does not refer to or host a
191    /// native view. Painting the [`crate::platform_view`] element with one only
192    /// reserves layout space.
193    #[derive(Clone, PartialEq, Eq, Debug)]
194    pub struct PlatformViewHandle {
195        id: PlatformViewId,
196    }
197
198    impl Default for PlatformViewHandle {
199        fn default() -> Self {
200            Self::inert()
201        }
202    }
203
204    impl PlatformViewHandle {
205        /// Creates a handle that refers to no native view.
206        pub fn inert() -> Self {
207            static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
208            Self {
209                id: PlatformViewId(NEXT_ID.fetch_add(1, Ordering::Relaxed)),
210            }
211        }
212
213        /// Returns this handle's stable identity.
214        pub fn id(&self) -> PlatformViewId {
215            self.id
216        }
217    }
218}
219
220pub use handle::PlatformViewHandle;
221
222/// A hosted view and the window-space bounds GPUI laid it out at.
223///
224/// Bounds use GPUI's window coordinate space: logical pixels with the origin at
225/// the window's top-left corner and y growing downwards. They are already
226/// snapped to the display's device-pixel grid.
227#[derive(Clone, Debug, PartialEq)]
228pub struct PlatformViewPlacement {
229    /// The hosted view.
230    pub handle: PlatformViewHandle,
231    /// Where the view belongs within the window.
232    pub bounds: Bounds<Pixels>,
233}
234
235/// The platform-view work owed by a single drawn frame.
236#[derive(Clone, Debug, Default, PartialEq)]
237pub struct PlatformViewUpdate {
238    /// Views painted by this frame, in paint order, each at its current bounds.
239    /// A view that was not hosted before is attached; one that already was is
240    /// repositioned.
241    pub placements: Vec<PlatformViewPlacement>,
242    /// Views that were hosted by the previous frame but were not painted by this
243    /// one, and so must be hidden and detached.
244    pub detached: Vec<PlatformViewId>,
245}
246
247impl PlatformViewUpdate {
248    /// Returns true when the frame asks for no platform-view work at all.
249    pub fn is_empty(&self) -> bool {
250        self.placements.is_empty() && self.detached.is_empty()
251    }
252}
253
254/// Tracks which platform views a window currently hosts so each frame can be
255/// turned into an attach/reposition/detach instruction set.
256#[derive(Default)]
257pub(crate) struct PlatformViewRegistry {
258    hosted: Vec<PlatformViewId>,
259}
260
261impl PlatformViewRegistry {
262    /// Reconciles the views painted by the frame just drawn against the views
263    /// hosted after the previous frame.
264    ///
265    /// Returns `None` when nothing is hosted and nothing was painted, so windows
266    /// that never use the element never reach the platform layer.
267    pub(crate) fn sync(
268        &mut self,
269        painted: &[PlatformViewPlacement],
270        scale_factor: f32,
271    ) -> Option<PlatformViewUpdate> {
272        if self.hosted.is_empty() && painted.is_empty() {
273            return None;
274        }
275
276        let painted_ids = painted
277            .iter()
278            .map(|placement| placement.handle.id())
279            .collect::<Vec<_>>();
280        let placements = last_placement_indices(&painted_ids)
281            .into_iter()
282            .map(|index| PlatformViewPlacement {
283                handle: painted[index].handle.clone(),
284                bounds: snap_platform_view_bounds(painted[index].bounds, scale_factor),
285            })
286            .collect::<Vec<_>>();
287        let detached = detached_ids(&self.hosted, &painted_ids);
288
289        self.hosted = placements
290            .iter()
291            .map(|placement| placement.handle.id())
292            .collect();
293
294        Some(PlatformViewUpdate {
295            placements,
296            detached,
297        })
298    }
299
300    /// Detaches every hosted view, as when the window goes away.
301    pub(crate) fn detach_all(&mut self) -> Option<PlatformViewUpdate> {
302        if self.hosted.is_empty() {
303            return None;
304        }
305
306        Some(PlatformViewUpdate {
307            placements: Vec::new(),
308            detached: std::mem::take(&mut self.hosted),
309        })
310    }
311}
312
313/// Returns the indices of `ids` that should survive deduplication: one index per
314/// distinct id, the last occurrence of that id, in first-appearance order.
315///
316/// A view painted more than once in a frame has a single native instance, so the
317/// topmost paint — the last one — owns its bounds.
318fn last_placement_indices(ids: &[PlatformViewId]) -> Vec<usize> {
319    let mut indices: Vec<usize> = Vec::with_capacity(ids.len());
320    for (index, id) in ids.iter().enumerate() {
321        match indices.iter_mut().find(|existing| ids[**existing] == *id) {
322            Some(existing) => *existing = index,
323            None => indices.push(index),
324        }
325    }
326    indices.sort_unstable();
327    indices
328}
329
330/// Returns the hosted ids that no longer appear among the painted ids, in the
331/// order they were hosted.
332fn detached_ids(hosted: &[PlatformViewId], painted: &[PlatformViewId]) -> Vec<PlatformViewId> {
333    hosted
334        .iter()
335        .filter(|id| !painted.contains(id))
336        .copied()
337        .collect()
338}
339
340/// Rounds window-space bounds onto the display's device-pixel grid, returning
341/// logical pixels again.
342///
343/// Native views are positioned in logical points, so honoring the scale factor
344/// means landing on the same grid the renderer rasterizes GPUI content to;
345/// otherwise a hosted view drifts by a fraction of a pixel against the GPUI
346/// content around it.
347pub fn snap_platform_view_bounds(bounds: Bounds<Pixels>, scale_factor: f32) -> Bounds<Pixels> {
348    if !scale_factor.is_finite() || scale_factor <= 0.0 {
349        return bounds;
350    }
351
352    let left = round_to_device_pixel(bounds.left().0, scale_factor) / scale_factor;
353    let top = round_to_device_pixel(bounds.top().0, scale_factor) / scale_factor;
354    let right = (round_to_device_pixel(bounds.right().0, scale_factor) / scale_factor).max(left);
355    let bottom = (round_to_device_pixel(bounds.bottom().0, scale_factor) / scale_factor).max(top);
356
357    Bounds::from_corners(point(px(left), px(top)), point(px(right), px(bottom)))
358}
359
360/// Converts window-space bounds, whose origin is the window's top-left corner
361/// with y growing downwards, into the origin a bottom-left-origin coordinate
362/// system wants — the convention AppKit uses for a non-flipped `NSView`.
363///
364/// `container_height` is the height of the view the hosted view is placed in.
365pub fn flip_bounds_origin_y(bounds: Bounds<Pixels>, container_height: Pixels) -> Point<Pixels> {
366    point(
367        bounds.origin.x,
368        container_height - bounds.origin.y - bounds.size.height,
369    )
370}
371
372/// Converts window-space bounds into the physical-pixel rectangle a platform
373/// layer positions a native view at.
374///
375/// Win32 and the other window systems that address windows in physical pixels
376/// need this conversion; the size is clamped at zero so a degenerate layout
377/// cannot ask a native view for a negative extent.
378pub fn platform_view_physical_bounds(
379    bounds: Bounds<Pixels>,
380    scale_factor: f32,
381) -> Bounds<DevicePixels> {
382    let scale_factor = if scale_factor.is_finite() && scale_factor > 0.0 {
383        scale_factor
384    } else {
385        1.0
386    };
387
388    let left = (bounds.left().0 * scale_factor).round() as i32;
389    let top = (bounds.top().0 * scale_factor).round() as i32;
390    let right = ((bounds.right().0 * scale_factor).round() as i32).max(left);
391    let bottom = ((bounds.bottom().0 * scale_factor).round() as i32).max(top);
392
393    Bounds {
394        origin: point(DevicePixels(left), DevicePixels(top)),
395        size: size(DevicePixels(right - left), DevicePixels(bottom - top)),
396    }
397}
398
399/// The bookkeeping a platform layer needs while it hosts native views, kept
400/// apart from the native calls so attach, reposition, restack and detach
401/// sequencing can be tested without a window.
402///
403/// `A` carries whatever the platform layer captured at attach time and must put
404/// back verbatim at detach: on Windows the child window's parent, window styles
405/// and window region.
406pub struct PlatformViewHosting<A> {
407    /// Hosted views in the stacking order last applied, bottom-most first.
408    hosted: Vec<HostedView<A>>,
409}
410
411struct HostedView<A> {
412    id: PlatformViewId,
413    attributes: A,
414    geometry: Option<HostedGeometry>,
415}
416
417/// The frame last applied to a hosted view, remembered so unchanged frames cost
418/// no native calls.
419///
420/// The scale factor is part of the identity because a per-monitor DPI change
421/// leaves the logical bounds alone while moving the view's physical rectangle.
422#[derive(Clone, Copy, PartialEq)]
423struct HostedGeometry {
424    bounds: Bounds<DevicePixels>,
425    scale_factor: f32,
426}
427
428impl<A> Default for PlatformViewHosting<A> {
429    fn default() -> Self {
430        Self { hosted: Vec::new() }
431    }
432}
433
434impl<A> PlatformViewHosting<A> {
435    /// Returns true while no view is hosted.
436    pub fn is_empty(&self) -> bool {
437        self.hosted.is_empty()
438    }
439
440    /// Returns whether the given view is hosted.
441    pub fn contains(&self, id: PlatformViewId) -> bool {
442        self.hosted.iter().any(|hosted| hosted.id == id)
443    }
444
445    /// Records a view as hosted, stacked above every view hosted so far.
446    ///
447    /// Attaching a view that is already hosted replaces what has to be restored
448    /// for it and forgets its applied frame, so the next placement is applied
449    /// unconditionally.
450    pub fn attach(&mut self, id: PlatformViewId, attributes: A) {
451        self.hosted.retain(|hosted| hosted.id != id);
452        self.hosted.push(HostedView {
453            id,
454            attributes,
455            geometry: None,
456        });
457    }
458
459    /// Forgets a hosted view, returning what the platform layer must restore.
460    pub fn detach(&mut self, id: PlatformViewId) -> Option<A> {
461        let index = self.hosted.iter().position(|hosted| hosted.id == id)?;
462        Some(self.hosted.remove(index).attributes)
463    }
464
465    /// Forgets every hosted view, bottom-most first.
466    pub fn detach_all(&mut self) -> Vec<(PlatformViewId, A)> {
467        std::mem::take(&mut self.hosted)
468            .into_iter()
469            .map(|hosted| (hosted.id, hosted.attributes))
470            .collect()
471    }
472
473    /// Adopts `order` — bottom-most first — as the stacking order, returning
474    /// true when it differs from the order last applied and the platform layer
475    /// therefore has to restack.
476    ///
477    /// Ids that are not hosted are ignored, and hosted views the frame did not
478    /// mention keep their relative order beneath the ordered ones.
479    pub fn restack(&mut self, order: &[PlatformViewId]) -> bool {
480        let mut ordered: Vec<usize> = Vec::with_capacity(self.hosted.len());
481        for id in order {
482            match self.hosted.iter().position(|hosted| hosted.id == *id) {
483                Some(index) if !ordered.contains(&index) => ordered.push(index),
484                _ => {}
485            }
486        }
487        let mut target = (0..self.hosted.len())
488            .filter(|index| !ordered.contains(index))
489            .collect::<Vec<_>>();
490        target.extend(ordered);
491
492        if target.iter().copied().eq(0..self.hosted.len()) {
493            return false;
494        }
495
496        let mut source = self
497            .hosted
498            .drain(..)
499            .map(Some)
500            .collect::<Vec<Option<HostedView<A>>>>();
501        self.hosted = target
502            .into_iter()
503            .map(|index| {
504                source[index]
505                    .take()
506                    .expect("every index appears exactly once")
507            })
508            .collect();
509        true
510    }
511
512    /// Records the frame a hosted view was laid out at, returning the physical
513    /// rectangle to move it to, or `None` when it already sits there.
514    pub fn place(
515        &mut self,
516        id: PlatformViewId,
517        bounds: Bounds<Pixels>,
518        scale_factor: f32,
519    ) -> Option<Bounds<DevicePixels>> {
520        let hosted = self.hosted.iter_mut().find(|hosted| hosted.id == id)?;
521        let geometry = HostedGeometry {
522            bounds: platform_view_physical_bounds(bounds, scale_factor),
523            scale_factor,
524        };
525        if hosted.geometry == Some(geometry) {
526            return None;
527        }
528        hosted.geometry = Some(geometry);
529        Some(geometry.bounds)
530    }
531
532    /// Returns what the platform layer must restore for a hosted view.
533    pub fn attributes(&self, id: PlatformViewId) -> Option<&A> {
534        self.hosted
535            .iter()
536            .find(|hosted| hosted.id == id)
537            .map(|hosted| &hosted.attributes)
538    }
539
540    /// Returns the hosted views in the stacking order last applied, bottom-most
541    /// first.
542    pub fn ids(&self) -> Vec<PlatformViewId> {
543        self.hosted.iter().map(|hosted| hosted.id).collect()
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550    use crate::size;
551
552    fn id(value: usize) -> PlatformViewId {
553        PlatformViewId(value)
554    }
555
556    #[test]
557    fn platform_view_bounds_snap_to_the_device_pixel_grid() {
558        let bounds = Bounds {
559            origin: point(px(10.3), px(20.4)),
560            size: size(px(100.2), px(50.1)),
561        };
562
563        let snapped = snap_platform_view_bounds(bounds, 2.0);
564
565        assert_eq!(snapped.origin, point(px(10.5), px(20.5)));
566        assert_eq!(snapped.size, size(px(100.0), px(50.0)));
567    }
568
569    #[test]
570    fn platform_view_bounds_are_left_alone_without_a_usable_scale_factor() {
571        let bounds = Bounds {
572            origin: point(px(10.3), px(20.4)),
573            size: size(px(100.2), px(50.1)),
574        };
575
576        assert_eq!(snap_platform_view_bounds(bounds, 0.0), bounds);
577        assert_eq!(snap_platform_view_bounds(bounds, f32::NAN), bounds);
578    }
579
580    #[test]
581    fn platform_view_bounds_never_snap_to_a_negative_size() {
582        let bounds = Bounds {
583            origin: point(px(10.0), px(20.0)),
584            size: size(px(0.0), px(0.0)),
585        };
586
587        let snapped = snap_platform_view_bounds(bounds, 2.0);
588
589        assert_eq!(snapped.size, size(px(0.0), px(0.0)));
590    }
591
592    #[test]
593    fn platform_view_bounds_flip_to_a_bottom_left_origin() {
594        let bounds = Bounds {
595            origin: point(px(10.0), px(30.0)),
596            size: size(px(100.0), px(50.0)),
597        };
598
599        let origin = flip_bounds_origin_y(bounds, px(200.0));
600
601        assert_eq!(origin, point(px(10.0), px(120.0)));
602    }
603
604    #[test]
605    fn platform_view_flip_is_its_own_inverse() {
606        let bounds = Bounds {
607            origin: point(px(4.0), px(7.0)),
608            size: size(px(20.0), px(11.0)),
609        };
610        let container_height = px(90.0);
611
612        let flipped = flip_bounds_origin_y(bounds, container_height);
613        let round_tripped = flip_bounds_origin_y(
614            Bounds {
615                origin: flipped,
616                size: bounds.size,
617            },
618            container_height,
619        );
620
621        assert_eq!(round_tripped, bounds.origin);
622    }
623
624    #[test]
625    fn platform_view_deduplication_keeps_the_last_paint_of_a_view() {
626        let ids = [id(1), id(2), id(1), id(3)];
627
628        assert_eq!(last_placement_indices(&ids), vec![1, 2, 3]);
629    }
630
631    #[test]
632    fn platform_view_deduplication_preserves_paint_order() {
633        let ids = [id(7), id(4), id(9)];
634
635        assert_eq!(last_placement_indices(&ids), vec![0, 1, 2]);
636        assert!(last_placement_indices(&[]).is_empty());
637    }
638
639    #[test]
640    fn platform_view_diff_detaches_only_views_that_stopped_painting() {
641        let hosted = [id(1), id(2), id(3)];
642        let painted = [id(2), id(4)];
643
644        assert_eq!(detached_ids(&hosted, &painted), vec![id(1), id(3)]);
645    }
646
647    #[test]
648    fn platform_view_registry_is_inert_until_a_view_is_painted() {
649        let mut registry = PlatformViewRegistry::default();
650
651        assert!(registry.sync(&[], 2.0).is_none());
652        assert!(registry.detach_all().is_none());
653    }
654
655    #[test]
656    fn platform_view_registry_detaches_views_that_stop_painting() {
657        let mut registry = PlatformViewRegistry::default();
658        registry.hosted = vec![id(1), id(2)];
659
660        let update = registry
661            .sync(&[], 2.0)
662            .expect("hosted views need an update");
663
664        assert!(update.placements.is_empty());
665        assert_eq!(update.detached, vec![id(1), id(2)]);
666        assert!(registry.hosted.is_empty());
667        assert!(registry.sync(&[], 2.0).is_none());
668    }
669
670    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
671    #[test]
672    fn platform_view_registry_retains_frame_placement_and_handle_identity() {
673        let mut registry = PlatformViewRegistry::default();
674        let handle = PlatformViewHandle::inert();
675        let placement = PlatformViewPlacement {
676            handle: handle.clone(),
677            bounds: Bounds {
678                origin: point(px(10.3), px(20.4)),
679                size: size(px(100.2), px(50.1)),
680            },
681        };
682
683        let update = registry
684            .sync(&[placement], 2.0)
685            .expect("painted views need an update");
686
687        assert_eq!(update.placements.len(), 1);
688        assert_eq!(update.placements[0].handle, handle);
689        assert_eq!(
690            update.placements[0].bounds.origin,
691            point(px(10.5), px(20.5))
692        );
693        assert_eq!(update.placements[0].bounds.size, size(px(100.), px(50.)));
694        assert!(update.detached.is_empty());
695
696        let detached = registry
697            .sync(&[], 2.0)
698            .expect("hosted views need a detach update");
699        assert_eq!(detached.detached, vec![handle.id()]);
700        let handle_id = handle.id();
701        #[expect(
702            clippy::redundant_clone,
703            reason = "this assertion covers clone identity"
704        )]
705        let cloned_handle = handle.clone();
706        assert_eq!(handle_id, cloned_handle.id());
707    }
708
709    #[test]
710    fn platform_view_physical_bounds_scale_the_snapped_rectangle() {
711        let bounds = Bounds {
712            origin: point(px(10.5), px(20.5)),
713            size: size(px(100.0), px(50.0)),
714        };
715
716        let physical = platform_view_physical_bounds(bounds, 2.0);
717
718        assert_eq!(physical.origin, point(DevicePixels(21), DevicePixels(41)));
719        assert_eq!(physical.size, size(DevicePixels(200), DevicePixels(100)));
720    }
721
722    #[test]
723    fn platform_view_physical_bounds_follow_a_dpi_change() {
724        let bounds = Bounds {
725            origin: point(px(10.0), px(20.0)),
726            size: size(px(100.0), px(50.0)),
727        };
728
729        assert_ne!(
730            platform_view_physical_bounds(bounds, 1.0),
731            platform_view_physical_bounds(bounds, 1.5)
732        );
733        assert_eq!(
734            platform_view_physical_bounds(bounds, 1.5).origin,
735            point(DevicePixels(15), DevicePixels(30))
736        );
737    }
738
739    #[test]
740    fn platform_view_physical_bounds_never_go_negative() {
741        let inverted = Bounds {
742            origin: point(px(40.0), px(30.0)),
743            size: size(px(-30.0), px(-25.0)),
744        };
745
746        let physical = platform_view_physical_bounds(inverted, 2.0);
747
748        assert_eq!(physical.origin, point(DevicePixels(80), DevicePixels(60)));
749        assert_eq!(physical.size, size(DevicePixels(0), DevicePixels(0)));
750    }
751
752    #[test]
753    fn platform_view_physical_bounds_fall_back_to_an_unscaled_rectangle() {
754        let bounds = Bounds {
755            origin: point(px(10.0), px(20.0)),
756            size: size(px(30.0), px(40.0)),
757        };
758
759        assert_eq!(
760            platform_view_physical_bounds(bounds, 0.0),
761            platform_view_physical_bounds(bounds, 1.0)
762        );
763        assert_eq!(
764            platform_view_physical_bounds(bounds, f32::NAN),
765            platform_view_physical_bounds(bounds, 1.0)
766        );
767    }
768
769    #[test]
770    fn platform_view_hosting_records_what_detaching_must_restore() {
771        let mut hosting = PlatformViewHosting::<&'static str>::default();
772
773        assert!(hosting.is_empty());
774        hosting.attach(id(1), "before-1");
775        hosting.attach(id(2), "before-2");
776
777        assert!(hosting.contains(id(1)));
778        assert_eq!(hosting.attributes(id(2)), Some(&"before-2"));
779        assert_eq!(hosting.detach(id(1)), Some("before-1"));
780        assert!(!hosting.contains(id(1)));
781        assert_eq!(hosting.detach(id(1)), None);
782        assert_eq!(hosting.detach_all(), vec![(id(2), "before-2")]);
783        assert!(hosting.is_empty());
784    }
785
786    #[test]
787    fn platform_view_hosting_reattaching_replaces_the_restore_state() {
788        let mut hosting = PlatformViewHosting::<&'static str>::default();
789        hosting.attach(id(1), "stale");
790        hosting.place(id(1), Bounds::default(), 1.0);
791
792        hosting.attach(id(1), "fresh");
793
794        assert_eq!(hosting.ids(), vec![id(1)]);
795        assert_eq!(hosting.attributes(id(1)), Some(&"fresh"));
796        assert!(
797            hosting.place(id(1), Bounds::default(), 1.0).is_some(),
798            "a freshly attached view has no applied frame to skip"
799        );
800    }
801
802    #[test]
803    fn platform_view_hosting_moves_only_when_the_frame_changed() {
804        let mut hosting = PlatformViewHosting::<()>::default();
805        hosting.attach(id(1), ());
806        let bounds = Bounds {
807            origin: point(px(10.0), px(20.0)),
808            size: size(px(100.0), px(50.0)),
809        };
810
811        assert_eq!(
812            hosting.place(id(1), bounds, 2.0),
813            Some(platform_view_physical_bounds(bounds, 2.0))
814        );
815        assert_eq!(hosting.place(id(1), bounds, 2.0), None);
816        assert_eq!(
817            hosting.place(id(1), bounds, 1.5),
818            Some(platform_view_physical_bounds(bounds, 1.5)),
819            "a scale factor change must move the view even at unchanged logical bounds"
820        );
821        assert_eq!(hosting.place(id(2), bounds, 1.5), None);
822    }
823
824    #[test]
825    fn platform_view_hosting_restacks_into_paint_order() {
826        let mut hosting = PlatformViewHosting::<()>::default();
827        hosting.attach(id(1), ());
828        hosting.attach(id(2), ());
829        hosting.attach(id(3), ());
830
831        assert!(!hosting.restack(&[id(1), id(2), id(3)]));
832        assert!(hosting.restack(&[id(3), id(1), id(2)]));
833        assert_eq!(hosting.ids(), vec![id(3), id(1), id(2)]);
834        assert!(!hosting.restack(&[id(3), id(1), id(2)]));
835    }
836
837    #[test]
838    fn platform_view_hosting_restack_keeps_unmentioned_views_underneath() {
839        let mut hosting = PlatformViewHosting::<()>::default();
840        hosting.attach(id(1), ());
841        hosting.attach(id(2), ());
842        hosting.attach(id(3), ());
843
844        assert!(hosting.restack(&[id(9), id(1)]));
845
846        assert_eq!(hosting.ids(), vec![id(2), id(3), id(1)]);
847    }
848
849    #[test]
850    fn platform_view_hosting_restack_preserves_applied_frames() {
851        let mut hosting = PlatformViewHosting::<()>::default();
852        hosting.attach(id(1), ());
853        hosting.attach(id(2), ());
854        let bounds = Bounds {
855            origin: point(px(1.0), px(2.0)),
856            size: size(px(3.0), px(4.0)),
857        };
858        hosting.place(id(1), bounds, 1.0);
859
860        assert!(hosting.restack(&[id(2), id(1)]));
861
862        assert_eq!(hosting.place(id(1), bounds, 1.0), None);
863    }
864
865    #[test]
866    fn platform_view_registry_detaches_everything_on_teardown() {
867        let mut registry = PlatformViewRegistry::default();
868        registry.hosted = vec![id(5), id(6)];
869
870        let update = registry.detach_all().expect("hosted views need detaching");
871
872        assert!(update.placements.is_empty());
873        assert_eq!(update.detached, vec![id(5), id(6)]);
874        assert!(registry.detach_all().is_none());
875    }
876}