Skip to main content

incular_platform/
display.rs

1//! Portable display geometry and top-level placement values.
2//!
3//! Desktop-global coordinates are intentionally distinct from widget/view
4//! coordinates. A backend only publishes values that its window system can
5//! actually observe; Wayland top-level placement, for example, remains
6//! unavailable rather than being synthesized.
7
8use crate::PhysicalSize;
9use std::fmt;
10
11/// Generational identity for one display discovered by the active backend.
12///
13/// Names, enumeration order, and native monitor handles are not identities.
14#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
15pub struct DisplayId {
16    index: u32,
17    generation: u32,
18}
19
20impl DisplayId {
21    #[must_use]
22    pub const fn from_parts(index: u32, generation: u32) -> Self {
23        Self { index, generation }
24    }
25
26    #[must_use]
27    pub const fn index(self) -> u32 {
28        self.index
29    }
30
31    #[must_use]
32    pub const fn generation(self) -> u32 {
33        self.generation
34    }
35}
36
37impl fmt::Debug for DisplayId {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(formatter, "DisplayId({}, {})", self.index, self.generation)
40    }
41}
42
43impl fmt::Display for DisplayId {
44    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(formatter, "Display#{}@{}", self.index + 1, self.generation)
46    }
47}
48
49/// Physical desktop-global point in native screen pixels.
50#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
51pub struct PhysicalScreenPosition {
52    pub x: i32,
53    pub y: i32,
54}
55
56/// Physical point relative to the top-left of one known display.
57///
58/// Unlike [`PhysicalScreenPosition`], this value is meaningful even when the
59/// backend cannot expose a desktop-global origin. Converting it to screen space
60/// requires a [`DisplaySnapshot::physical_bounds`] value.
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
62pub struct PhysicalDisplayPosition {
63    pub x: i32,
64    pub y: i32,
65}
66
67impl PhysicalDisplayPosition {
68    #[must_use]
69    pub const fn new(x: i32, y: i32) -> Self {
70        Self { x, y }
71    }
72}
73
74impl PhysicalScreenPosition {
75    #[must_use]
76    pub const fn new(x: i32, y: i32) -> Self {
77        Self { x, y }
78    }
79}
80
81/// Physical desktop-global rectangle in native screen pixels.
82#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
83pub struct PhysicalScreenRect {
84    pub origin: PhysicalScreenPosition,
85    pub size: PhysicalSize,
86}
87
88impl PhysicalScreenRect {
89    #[must_use]
90    pub const fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
91        Self {
92            origin: PhysicalScreenPosition::new(x, y),
93            size: PhysicalSize::new(width, height),
94        }
95    }
96
97    #[must_use]
98    pub fn centered_position(self, outer_size: PhysicalSize) -> PhysicalScreenPosition {
99        let x = i64::from(self.origin.x)
100            + (i64::from(self.size.width) - i64::from(outer_size.width)) / 2;
101        let y = i64::from(self.origin.y)
102            + (i64::from(self.size.height) - i64::from(outer_size.height)) / 2;
103        PhysicalScreenPosition::new(clamp_i64_to_i32(x), clamp_i64_to_i32(y))
104    }
105
106    #[must_use]
107    pub fn intersection(self, other: Self) -> Option<Self> {
108        let left = i64::from(self.origin.x).max(i64::from(other.origin.x));
109        let top = i64::from(self.origin.y).max(i64::from(other.origin.y));
110        let right = (i64::from(self.origin.x) + i64::from(self.size.width))
111            .min(i64::from(other.origin.x) + i64::from(other.size.width));
112        let bottom = (i64::from(self.origin.y) + i64::from(self.size.height))
113            .min(i64::from(other.origin.y) + i64::from(other.size.height));
114        (right > left && bottom > top).then(|| {
115            Self::new(
116                clamp_i64_to_i32(left),
117                clamp_i64_to_i32(top),
118                u32::try_from(right - left).unwrap_or(u32::MAX),
119                u32::try_from(bottom - top).unwrap_or(u32::MAX),
120            )
121        })
122    }
123}
124
125/// Logical desktop-global point, only published by backends with a coherent
126/// logical global coordinate space.
127#[derive(Clone, Copy, Debug, Default, PartialEq)]
128pub struct LogicalScreenPosition {
129    pub x: f64,
130    pub y: f64,
131}
132
133/// Logical point relative to one display, independent of desktop-global
134/// coordinate availability.
135#[derive(Clone, Copy, Debug, Default, PartialEq)]
136pub struct LogicalDisplayPosition {
137    pub x: f64,
138    pub y: f64,
139}
140
141impl LogicalDisplayPosition {
142    #[must_use]
143    pub const fn new(x: f64, y: f64) -> Self {
144        Self { x, y }
145    }
146}
147
148/// Logical desktop-global rectangle. Mixed-DPI backends that cannot provide a
149/// coherent global logical coordinate space leave this absent rather than
150/// dividing a physical global origin by one monitor's scale factor.
151#[derive(Clone, Copy, Debug, PartialEq)]
152pub struct LogicalScreenRect {
153    pub origin: LogicalScreenPosition,
154    pub width: f64,
155    pub height: f64,
156}
157
158/// Immutable portable snapshot for one currently connected display.
159#[derive(Clone, Debug, PartialEq)]
160pub struct DisplaySnapshot {
161    pub id: DisplayId,
162    pub name: Option<String>,
163    pub scale_factor: f64,
164    /// Physical pixel extent even when the window system does not expose a
165    /// desktop-global origin (notably Wayland top-level placement).
166    pub physical_size: PhysicalSize,
167    pub physical_bounds: Option<PhysicalScreenRect>,
168    pub logical_bounds: Option<LogicalScreenRect>,
169    pub physical_work_area: Option<PhysicalScreenRect>,
170    pub logical_work_area: Option<LogicalScreenRect>,
171    pub is_primary: bool,
172}
173
174impl DisplaySnapshot {
175    #[must_use]
176    pub fn logical_size(&self) -> Option<(f64, f64)> {
177        (self.scale_factor.is_finite() && self.scale_factor > 0.0).then(|| {
178            (
179                f64::from(self.physical_size.width) / self.scale_factor,
180                f64::from(self.physical_size.height) / self.scale_factor,
181            )
182        })
183    }
184
185    #[must_use]
186    pub fn placement_rect(&self, area: DisplayPlacementArea) -> Option<PhysicalScreenRect> {
187        match area {
188            DisplayPlacementArea::WorkArea => self.physical_work_area,
189            DisplayPlacementArea::FullBounds => self.physical_bounds,
190        }
191    }
192
193    /// Converts display-local logical coordinates into display-local physical
194    /// coordinates. No desktop-global origin is involved, so this remains
195    /// well-defined on mixed-DPI desktops and Wayland.
196    #[must_use]
197    pub fn logical_to_physical_local(
198        &self,
199        position: LogicalDisplayPosition,
200    ) -> Option<PhysicalDisplayPosition> {
201        if !self.scale_factor.is_finite()
202            || self.scale_factor <= 0.0
203            || !position.x.is_finite()
204            || !position.y.is_finite()
205        {
206            return None;
207        }
208        let x = position.x * self.scale_factor;
209        let y = position.y * self.scale_factor;
210        Some(PhysicalDisplayPosition::new(
211            rounded_f64_to_i32(x)?,
212            rounded_f64_to_i32(y)?,
213        ))
214    }
215
216    /// Converts display-local physical coordinates into logical coordinates.
217    #[must_use]
218    pub fn physical_to_logical_local(
219        &self,
220        position: PhysicalDisplayPosition,
221    ) -> Option<LogicalDisplayPosition> {
222        (self.scale_factor.is_finite() && self.scale_factor > 0.0).then(|| {
223            LogicalDisplayPosition::new(
224                f64::from(position.x) / self.scale_factor,
225                f64::from(position.y) / self.scale_factor,
226            )
227        })
228    }
229
230    /// Resolves a display-local physical point into desktop-global physical
231    /// coordinates when this backend actually exposes a global display origin.
232    #[must_use]
233    pub fn screen_position(
234        &self,
235        position: PhysicalDisplayPosition,
236    ) -> Option<PhysicalScreenPosition> {
237        let bounds = self.physical_bounds?;
238        Some(PhysicalScreenPosition::new(
239            checked_i64_to_i32(i64::from(bounds.origin.x) + i64::from(position.x))?,
240            checked_i64_to_i32(i64::from(bounds.origin.y) + i64::from(position.y))?,
241        ))
242    }
243}
244
245#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
246pub enum DisplayPlacementArea {
247    #[default]
248    WorkArea,
249    FullBounds,
250}
251
252const fn clamp_i64_to_i32(value: i64) -> i32 {
253    if value < i32::MIN as i64 {
254        i32::MIN
255    } else if value > i32::MAX as i64 {
256        i32::MAX
257    } else {
258        value as i32
259    }
260}
261
262fn checked_i64_to_i32(value: i64) -> Option<i32> {
263    i32::try_from(value).ok()
264}
265
266fn rounded_f64_to_i32(value: f64) -> Option<i32> {
267    if !value.is_finite() || value < f64::from(i32::MIN) || value > f64::from(i32::MAX) {
268        return None;
269    }
270    Some(value.round() as i32)
271}