1use crate::{Bounds, DevicePixels, Pixels, Point, point, px, size, util::round_to_device_pixel};
15use std::fmt;
16
17#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub struct PlatformViewId(usize);
22
23impl PlatformViewId {
24 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 pub struct PlatformViewHandle {
51 view: NonNull<Object>,
52 }
53
54 impl PlatformViewHandle {
55 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 pub fn as_ns_view(&self) -> *mut c_void {
73 self.view.as_ptr().cast()
74 }
75
76 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 #[derive(Clone)]
130 pub struct PlatformViewHandle {
131 hwnd: HWND,
132 }
133
134 impl PlatformViewHandle {
135 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 pub fn as_hwnd(&self) -> HWND {
156 self.hwnd
157 }
158
159 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 #[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 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 pub fn id(&self) -> PlatformViewId {
215 self.id
216 }
217 }
218}
219
220pub use handle::PlatformViewHandle;
221
222#[derive(Clone, Debug, PartialEq)]
228pub struct PlatformViewPlacement {
229 pub handle: PlatformViewHandle,
231 pub bounds: Bounds<Pixels>,
233}
234
235#[derive(Clone, Debug, Default, PartialEq)]
237pub struct PlatformViewUpdate {
238 pub placements: Vec<PlatformViewPlacement>,
242 pub detached: Vec<PlatformViewId>,
245}
246
247impl PlatformViewUpdate {
248 pub fn is_empty(&self) -> bool {
250 self.placements.is_empty() && self.detached.is_empty()
251 }
252}
253
254#[derive(Default)]
257pub(crate) struct PlatformViewRegistry {
258 hosted: Vec<PlatformViewId>,
259}
260
261impl PlatformViewRegistry {
262 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 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
313fn 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
330fn detached_ids(hosted: &[PlatformViewId], painted: &[PlatformViewId]) -> Vec<PlatformViewId> {
333 hosted
334 .iter()
335 .filter(|id| !painted.contains(id))
336 .copied()
337 .collect()
338}
339
340pub 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
360pub 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
372pub 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
399pub struct PlatformViewHosting<A> {
407 hosted: Vec<HostedView<A>>,
409}
410
411struct HostedView<A> {
412 id: PlatformViewId,
413 attributes: A,
414 geometry: Option<HostedGeometry>,
415}
416
417#[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 pub fn is_empty(&self) -> bool {
437 self.hosted.is_empty()
438 }
439
440 pub fn contains(&self, id: PlatformViewId) -> bool {
442 self.hosted.iter().any(|hosted| hosted.id == id)
443 }
444
445 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 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 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 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 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 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 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}