use crate::{Bounds, DevicePixels, Pixels, Point, point, px, size, util::round_to_device_pixel};
use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PlatformViewId(usize);
impl PlatformViewId {
pub fn as_usize(self) -> usize {
self.0
}
}
impl fmt::Debug for PlatformViewId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PlatformViewId({:#x})", self.0)
}
}
#[cfg(target_os = "macos")]
mod handle {
use super::PlatformViewId;
use objc::{msg_send, runtime::Object, sel, sel_impl};
use std::{ffi::c_void, fmt, ptr::NonNull};
pub struct PlatformViewHandle {
view: NonNull<Object>,
}
impl PlatformViewHandle {
pub unsafe fn from_ns_view(ns_view: *mut c_void) -> Self {
let view = NonNull::new(ns_view.cast::<Object>())
.expect("PlatformViewHandle::from_ns_view requires a non-null NSView");
unsafe {
let _: *mut Object = msg_send![view.as_ptr(), retain];
}
Self { view }
}
pub fn as_ns_view(&self) -> *mut c_void {
self.view.as_ptr().cast()
}
pub fn id(&self) -> PlatformViewId {
PlatformViewId(self.view.as_ptr() as usize)
}
}
impl Clone for PlatformViewHandle {
fn clone(&self) -> Self {
unsafe {
let _: *mut Object = msg_send![self.view.as_ptr(), retain];
}
Self { view: self.view }
}
}
impl Drop for PlatformViewHandle {
fn drop(&mut self) {
unsafe {
let _: () = msg_send![self.view.as_ptr(), release];
}
}
}
impl fmt::Debug for PlatformViewHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PlatformViewHandle")
.field("id", &self.id())
.finish()
}
}
impl PartialEq for PlatformViewHandle {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id()
}
}
impl Eq for PlatformViewHandle {}
}
#[cfg(target_os = "windows")]
mod handle {
use super::PlatformViewId;
use std::fmt;
use windows::Win32::Foundation::HWND;
#[derive(Clone)]
pub struct PlatformViewHandle {
hwnd: HWND,
}
impl PlatformViewHandle {
pub unsafe fn from_hwnd(hwnd: HWND) -> Self {
assert!(
!hwnd.is_invalid(),
"PlatformViewHandle::from_hwnd requires a non-null HWND"
);
Self { hwnd }
}
pub fn as_hwnd(&self) -> HWND {
self.hwnd
}
pub fn id(&self) -> PlatformViewId {
PlatformViewId(self.hwnd.0 as usize)
}
}
impl fmt::Debug for PlatformViewHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PlatformViewHandle")
.field("id", &self.id())
.finish()
}
}
impl PartialEq for PlatformViewHandle {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id()
}
}
impl Eq for PlatformViewHandle {}
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
mod handle {
use super::PlatformViewId;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct PlatformViewHandle {
id: PlatformViewId,
}
impl Default for PlatformViewHandle {
fn default() -> Self {
Self::inert()
}
}
impl PlatformViewHandle {
pub fn inert() -> Self {
static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
Self {
id: PlatformViewId(NEXT_ID.fetch_add(1, Ordering::Relaxed)),
}
}
pub fn id(&self) -> PlatformViewId {
self.id
}
}
}
pub use handle::PlatformViewHandle;
#[derive(Clone, Debug, PartialEq)]
pub struct PlatformViewPlacement {
pub handle: PlatformViewHandle,
pub bounds: Bounds<Pixels>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PlatformViewUpdate {
pub placements: Vec<PlatformViewPlacement>,
pub detached: Vec<PlatformViewId>,
}
impl PlatformViewUpdate {
pub fn is_empty(&self) -> bool {
self.placements.is_empty() && self.detached.is_empty()
}
}
#[derive(Default)]
pub(crate) struct PlatformViewRegistry {
hosted: Vec<PlatformViewId>,
}
impl PlatformViewRegistry {
pub(crate) fn sync(
&mut self,
painted: &[PlatformViewPlacement],
scale_factor: f32,
) -> Option<PlatformViewUpdate> {
if self.hosted.is_empty() && painted.is_empty() {
return None;
}
let painted_ids = painted
.iter()
.map(|placement| placement.handle.id())
.collect::<Vec<_>>();
let placements = last_placement_indices(&painted_ids)
.into_iter()
.map(|index| PlatformViewPlacement {
handle: painted[index].handle.clone(),
bounds: snap_platform_view_bounds(painted[index].bounds, scale_factor),
})
.collect::<Vec<_>>();
let detached = detached_ids(&self.hosted, &painted_ids);
self.hosted = placements
.iter()
.map(|placement| placement.handle.id())
.collect();
Some(PlatformViewUpdate {
placements,
detached,
})
}
pub(crate) fn detach_all(&mut self) -> Option<PlatformViewUpdate> {
if self.hosted.is_empty() {
return None;
}
Some(PlatformViewUpdate {
placements: Vec::new(),
detached: std::mem::take(&mut self.hosted),
})
}
}
fn last_placement_indices(ids: &[PlatformViewId]) -> Vec<usize> {
let mut indices: Vec<usize> = Vec::with_capacity(ids.len());
for (index, id) in ids.iter().enumerate() {
match indices.iter_mut().find(|existing| ids[**existing] == *id) {
Some(existing) => *existing = index,
None => indices.push(index),
}
}
indices.sort_unstable();
indices
}
fn detached_ids(hosted: &[PlatformViewId], painted: &[PlatformViewId]) -> Vec<PlatformViewId> {
hosted
.iter()
.filter(|id| !painted.contains(id))
.copied()
.collect()
}
pub fn snap_platform_view_bounds(bounds: Bounds<Pixels>, scale_factor: f32) -> Bounds<Pixels> {
if !scale_factor.is_finite() || scale_factor <= 0.0 {
return bounds;
}
let left = round_to_device_pixel(bounds.left().0, scale_factor) / scale_factor;
let top = round_to_device_pixel(bounds.top().0, scale_factor) / scale_factor;
let right = (round_to_device_pixel(bounds.right().0, scale_factor) / scale_factor).max(left);
let bottom = (round_to_device_pixel(bounds.bottom().0, scale_factor) / scale_factor).max(top);
Bounds::from_corners(point(px(left), px(top)), point(px(right), px(bottom)))
}
pub fn flip_bounds_origin_y(bounds: Bounds<Pixels>, container_height: Pixels) -> Point<Pixels> {
point(
bounds.origin.x,
container_height - bounds.origin.y - bounds.size.height,
)
}
pub fn platform_view_physical_bounds(
bounds: Bounds<Pixels>,
scale_factor: f32,
) -> Bounds<DevicePixels> {
let scale_factor = if scale_factor.is_finite() && scale_factor > 0.0 {
scale_factor
} else {
1.0
};
let left = (bounds.left().0 * scale_factor).round() as i32;
let top = (bounds.top().0 * scale_factor).round() as i32;
let right = ((bounds.right().0 * scale_factor).round() as i32).max(left);
let bottom = ((bounds.bottom().0 * scale_factor).round() as i32).max(top);
Bounds {
origin: point(DevicePixels(left), DevicePixels(top)),
size: size(DevicePixels(right - left), DevicePixels(bottom - top)),
}
}
pub struct PlatformViewHosting<A> {
hosted: Vec<HostedView<A>>,
}
struct HostedView<A> {
id: PlatformViewId,
attributes: A,
geometry: Option<HostedGeometry>,
}
#[derive(Clone, Copy, PartialEq)]
struct HostedGeometry {
bounds: Bounds<DevicePixels>,
scale_factor: f32,
}
impl<A> Default for PlatformViewHosting<A> {
fn default() -> Self {
Self { hosted: Vec::new() }
}
}
impl<A> PlatformViewHosting<A> {
pub fn is_empty(&self) -> bool {
self.hosted.is_empty()
}
pub fn contains(&self, id: PlatformViewId) -> bool {
self.hosted.iter().any(|hosted| hosted.id == id)
}
pub fn attach(&mut self, id: PlatformViewId, attributes: A) {
self.hosted.retain(|hosted| hosted.id != id);
self.hosted.push(HostedView {
id,
attributes,
geometry: None,
});
}
pub fn detach(&mut self, id: PlatformViewId) -> Option<A> {
let index = self.hosted.iter().position(|hosted| hosted.id == id)?;
Some(self.hosted.remove(index).attributes)
}
pub fn detach_all(&mut self) -> Vec<(PlatformViewId, A)> {
std::mem::take(&mut self.hosted)
.into_iter()
.map(|hosted| (hosted.id, hosted.attributes))
.collect()
}
pub fn restack(&mut self, order: &[PlatformViewId]) -> bool {
let mut ordered: Vec<usize> = Vec::with_capacity(self.hosted.len());
for id in order {
match self.hosted.iter().position(|hosted| hosted.id == *id) {
Some(index) if !ordered.contains(&index) => ordered.push(index),
_ => {}
}
}
let mut target = (0..self.hosted.len())
.filter(|index| !ordered.contains(index))
.collect::<Vec<_>>();
target.extend(ordered);
if target.iter().copied().eq(0..self.hosted.len()) {
return false;
}
let mut source = self
.hosted
.drain(..)
.map(Some)
.collect::<Vec<Option<HostedView<A>>>>();
self.hosted = target
.into_iter()
.map(|index| {
source[index]
.take()
.expect("every index appears exactly once")
})
.collect();
true
}
pub fn place(
&mut self,
id: PlatformViewId,
bounds: Bounds<Pixels>,
scale_factor: f32,
) -> Option<Bounds<DevicePixels>> {
let hosted = self.hosted.iter_mut().find(|hosted| hosted.id == id)?;
let geometry = HostedGeometry {
bounds: platform_view_physical_bounds(bounds, scale_factor),
scale_factor,
};
if hosted.geometry == Some(geometry) {
return None;
}
hosted.geometry = Some(geometry);
Some(geometry.bounds)
}
pub fn attributes(&self, id: PlatformViewId) -> Option<&A> {
self.hosted
.iter()
.find(|hosted| hosted.id == id)
.map(|hosted| &hosted.attributes)
}
pub fn ids(&self) -> Vec<PlatformViewId> {
self.hosted.iter().map(|hosted| hosted.id).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::size;
fn id(value: usize) -> PlatformViewId {
PlatformViewId(value)
}
#[test]
fn platform_view_bounds_snap_to_the_device_pixel_grid() {
let bounds = Bounds {
origin: point(px(10.3), px(20.4)),
size: size(px(100.2), px(50.1)),
};
let snapped = snap_platform_view_bounds(bounds, 2.0);
assert_eq!(snapped.origin, point(px(10.5), px(20.5)));
assert_eq!(snapped.size, size(px(100.0), px(50.0)));
}
#[test]
fn platform_view_bounds_are_left_alone_without_a_usable_scale_factor() {
let bounds = Bounds {
origin: point(px(10.3), px(20.4)),
size: size(px(100.2), px(50.1)),
};
assert_eq!(snap_platform_view_bounds(bounds, 0.0), bounds);
assert_eq!(snap_platform_view_bounds(bounds, f32::NAN), bounds);
}
#[test]
fn platform_view_bounds_never_snap_to_a_negative_size() {
let bounds = Bounds {
origin: point(px(10.0), px(20.0)),
size: size(px(0.0), px(0.0)),
};
let snapped = snap_platform_view_bounds(bounds, 2.0);
assert_eq!(snapped.size, size(px(0.0), px(0.0)));
}
#[test]
fn platform_view_bounds_flip_to_a_bottom_left_origin() {
let bounds = Bounds {
origin: point(px(10.0), px(30.0)),
size: size(px(100.0), px(50.0)),
};
let origin = flip_bounds_origin_y(bounds, px(200.0));
assert_eq!(origin, point(px(10.0), px(120.0)));
}
#[test]
fn platform_view_flip_is_its_own_inverse() {
let bounds = Bounds {
origin: point(px(4.0), px(7.0)),
size: size(px(20.0), px(11.0)),
};
let container_height = px(90.0);
let flipped = flip_bounds_origin_y(bounds, container_height);
let round_tripped = flip_bounds_origin_y(
Bounds {
origin: flipped,
size: bounds.size,
},
container_height,
);
assert_eq!(round_tripped, bounds.origin);
}
#[test]
fn platform_view_deduplication_keeps_the_last_paint_of_a_view() {
let ids = [id(1), id(2), id(1), id(3)];
assert_eq!(last_placement_indices(&ids), vec![1, 2, 3]);
}
#[test]
fn platform_view_deduplication_preserves_paint_order() {
let ids = [id(7), id(4), id(9)];
assert_eq!(last_placement_indices(&ids), vec![0, 1, 2]);
assert!(last_placement_indices(&[]).is_empty());
}
#[test]
fn platform_view_diff_detaches_only_views_that_stopped_painting() {
let hosted = [id(1), id(2), id(3)];
let painted = [id(2), id(4)];
assert_eq!(detached_ids(&hosted, &painted), vec![id(1), id(3)]);
}
#[test]
fn platform_view_registry_is_inert_until_a_view_is_painted() {
let mut registry = PlatformViewRegistry::default();
assert!(registry.sync(&[], 2.0).is_none());
assert!(registry.detach_all().is_none());
}
#[test]
fn platform_view_registry_detaches_views_that_stop_painting() {
let mut registry = PlatformViewRegistry::default();
registry.hosted = vec![id(1), id(2)];
let update = registry
.sync(&[], 2.0)
.expect("hosted views need an update");
assert!(update.placements.is_empty());
assert_eq!(update.detached, vec![id(1), id(2)]);
assert!(registry.hosted.is_empty());
assert!(registry.sync(&[], 2.0).is_none());
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn platform_view_registry_retains_frame_placement_and_handle_identity() {
let mut registry = PlatformViewRegistry::default();
let handle = PlatformViewHandle::inert();
let placement = PlatformViewPlacement {
handle: handle.clone(),
bounds: Bounds {
origin: point(px(10.3), px(20.4)),
size: size(px(100.2), px(50.1)),
},
};
let update = registry
.sync(&[placement], 2.0)
.expect("painted views need an update");
assert_eq!(update.placements.len(), 1);
assert_eq!(update.placements[0].handle, handle);
assert_eq!(
update.placements[0].bounds.origin,
point(px(10.5), px(20.5))
);
assert_eq!(update.placements[0].bounds.size, size(px(100.), px(50.)));
assert!(update.detached.is_empty());
let detached = registry
.sync(&[], 2.0)
.expect("hosted views need a detach update");
assert_eq!(detached.detached, vec![handle.id()]);
let handle_id = handle.id();
#[expect(
clippy::redundant_clone,
reason = "this assertion covers clone identity"
)]
let cloned_handle = handle.clone();
assert_eq!(handle_id, cloned_handle.id());
}
#[test]
fn platform_view_physical_bounds_scale_the_snapped_rectangle() {
let bounds = Bounds {
origin: point(px(10.5), px(20.5)),
size: size(px(100.0), px(50.0)),
};
let physical = platform_view_physical_bounds(bounds, 2.0);
assert_eq!(physical.origin, point(DevicePixels(21), DevicePixels(41)));
assert_eq!(physical.size, size(DevicePixels(200), DevicePixels(100)));
}
#[test]
fn platform_view_physical_bounds_follow_a_dpi_change() {
let bounds = Bounds {
origin: point(px(10.0), px(20.0)),
size: size(px(100.0), px(50.0)),
};
assert_ne!(
platform_view_physical_bounds(bounds, 1.0),
platform_view_physical_bounds(bounds, 1.5)
);
assert_eq!(
platform_view_physical_bounds(bounds, 1.5).origin,
point(DevicePixels(15), DevicePixels(30))
);
}
#[test]
fn platform_view_physical_bounds_never_go_negative() {
let inverted = Bounds {
origin: point(px(40.0), px(30.0)),
size: size(px(-30.0), px(-25.0)),
};
let physical = platform_view_physical_bounds(inverted, 2.0);
assert_eq!(physical.origin, point(DevicePixels(80), DevicePixels(60)));
assert_eq!(physical.size, size(DevicePixels(0), DevicePixels(0)));
}
#[test]
fn platform_view_physical_bounds_fall_back_to_an_unscaled_rectangle() {
let bounds = Bounds {
origin: point(px(10.0), px(20.0)),
size: size(px(30.0), px(40.0)),
};
assert_eq!(
platform_view_physical_bounds(bounds, 0.0),
platform_view_physical_bounds(bounds, 1.0)
);
assert_eq!(
platform_view_physical_bounds(bounds, f32::NAN),
platform_view_physical_bounds(bounds, 1.0)
);
}
#[test]
fn platform_view_hosting_records_what_detaching_must_restore() {
let mut hosting = PlatformViewHosting::<&'static str>::default();
assert!(hosting.is_empty());
hosting.attach(id(1), "before-1");
hosting.attach(id(2), "before-2");
assert!(hosting.contains(id(1)));
assert_eq!(hosting.attributes(id(2)), Some(&"before-2"));
assert_eq!(hosting.detach(id(1)), Some("before-1"));
assert!(!hosting.contains(id(1)));
assert_eq!(hosting.detach(id(1)), None);
assert_eq!(hosting.detach_all(), vec![(id(2), "before-2")]);
assert!(hosting.is_empty());
}
#[test]
fn platform_view_hosting_reattaching_replaces_the_restore_state() {
let mut hosting = PlatformViewHosting::<&'static str>::default();
hosting.attach(id(1), "stale");
hosting.place(id(1), Bounds::default(), 1.0);
hosting.attach(id(1), "fresh");
assert_eq!(hosting.ids(), vec![id(1)]);
assert_eq!(hosting.attributes(id(1)), Some(&"fresh"));
assert!(
hosting.place(id(1), Bounds::default(), 1.0).is_some(),
"a freshly attached view has no applied frame to skip"
);
}
#[test]
fn platform_view_hosting_moves_only_when_the_frame_changed() {
let mut hosting = PlatformViewHosting::<()>::default();
hosting.attach(id(1), ());
let bounds = Bounds {
origin: point(px(10.0), px(20.0)),
size: size(px(100.0), px(50.0)),
};
assert_eq!(
hosting.place(id(1), bounds, 2.0),
Some(platform_view_physical_bounds(bounds, 2.0))
);
assert_eq!(hosting.place(id(1), bounds, 2.0), None);
assert_eq!(
hosting.place(id(1), bounds, 1.5),
Some(platform_view_physical_bounds(bounds, 1.5)),
"a scale factor change must move the view even at unchanged logical bounds"
);
assert_eq!(hosting.place(id(2), bounds, 1.5), None);
}
#[test]
fn platform_view_hosting_restacks_into_paint_order() {
let mut hosting = PlatformViewHosting::<()>::default();
hosting.attach(id(1), ());
hosting.attach(id(2), ());
hosting.attach(id(3), ());
assert!(!hosting.restack(&[id(1), id(2), id(3)]));
assert!(hosting.restack(&[id(3), id(1), id(2)]));
assert_eq!(hosting.ids(), vec![id(3), id(1), id(2)]);
assert!(!hosting.restack(&[id(3), id(1), id(2)]));
}
#[test]
fn platform_view_hosting_restack_keeps_unmentioned_views_underneath() {
let mut hosting = PlatformViewHosting::<()>::default();
hosting.attach(id(1), ());
hosting.attach(id(2), ());
hosting.attach(id(3), ());
assert!(hosting.restack(&[id(9), id(1)]));
assert_eq!(hosting.ids(), vec![id(2), id(3), id(1)]);
}
#[test]
fn platform_view_hosting_restack_preserves_applied_frames() {
let mut hosting = PlatformViewHosting::<()>::default();
hosting.attach(id(1), ());
hosting.attach(id(2), ());
let bounds = Bounds {
origin: point(px(1.0), px(2.0)),
size: size(px(3.0), px(4.0)),
};
hosting.place(id(1), bounds, 1.0);
assert!(hosting.restack(&[id(2), id(1)]));
assert_eq!(hosting.place(id(1), bounds, 1.0), None);
}
#[test]
fn platform_view_registry_detaches_everything_on_teardown() {
let mut registry = PlatformViewRegistry::default();
registry.hosted = vec![id(5), id(6)];
let update = registry.detach_all().expect("hosted views need detaching");
assert!(update.placements.is_empty());
assert_eq!(update.detached, vec![id(5), id(6)]);
assert!(registry.detach_all().is_none());
}
}