Skip to main content

incular_platform/
window_control.rs

1//! Portable window-control values shared by runtime and native adapters.
2
3use crate::{DisplayId, Fullscreen, PhysicalScreenPosition, PhysicalSize};
4use incular_core::Size;
5use std::{fmt, sync::Arc};
6
7/// Portable z-level hints for a desktop top-level window.
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
9pub enum WindowLevel {
10    #[default]
11    Normal,
12    AlwaysOnTop,
13}
14
15/// Strength of a desktop user-attention request.
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
17pub enum UserAttentionType {
18    #[default]
19    Informational,
20    Critical,
21}
22
23/// Validated RGBA8 window-icon pixels.
24///
25/// The byte storage is shared so cloning `WindowOptions`, requests, and retained
26/// state does not duplicate image data. Platform adapters create their native
27/// icon object only at the native boundary.
28#[derive(Clone, PartialEq, Eq)]
29pub struct WindowIcon {
30    rgba: Arc<[u8]>,
31    width: u32,
32    height: u32,
33}
34
35impl WindowIcon {
36    pub fn from_rgba(
37        rgba: impl Into<Vec<u8>>,
38        width: u32,
39        height: u32,
40    ) -> Result<Self, WindowIconError> {
41        let rgba = rgba.into();
42        if width == 0 || height == 0 {
43            return Err(WindowIconError::ZeroDimension);
44        }
45        let pixels = usize::try_from(width)
46            .ok()
47            .and_then(|width| {
48                usize::try_from(height)
49                    .ok()
50                    .and_then(|height| width.checked_mul(height))
51            })
52            .and_then(|pixels| pixels.checked_mul(4))
53            .ok_or(WindowIconError::DimensionsOverflow)?;
54        if rgba.len() != pixels {
55            return Err(WindowIconError::InvalidByteCount {
56                expected: pixels,
57                actual: rgba.len(),
58            });
59        }
60        Ok(Self {
61            rgba: rgba.into(),
62            width,
63            height,
64        })
65    }
66
67    #[must_use]
68    pub fn rgba(&self) -> &[u8] {
69        &self.rgba
70    }
71
72    #[must_use]
73    pub const fn width(&self) -> u32 {
74        self.width
75    }
76
77    #[must_use]
78    pub const fn height(&self) -> u32 {
79        self.height
80    }
81}
82
83impl fmt::Debug for WindowIcon {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        formatter
86            .debug_struct("WindowIcon")
87            .field("width", &self.width)
88            .field("height", &self.height)
89            .field("rgba_bytes", &self.rgba.len())
90            .finish()
91    }
92}
93
94#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
95pub enum WindowIconError {
96    #[error("window icon dimensions must be non-zero")]
97    ZeroDimension,
98    #[error("window icon dimensions overflow addressable memory")]
99    DimensionsOverflow,
100    #[error("window icon RGBA byte count is {actual}, expected {expected}")]
101    InvalidByteCount { expected: usize, actual: usize },
102}
103
104/// Validated dynamic logical-size constraints for a native window.
105#[derive(Clone, Copy, Debug, Default, PartialEq)]
106pub struct LogicalSizeLimits {
107    minimum: Option<Size>,
108    maximum: Option<Size>,
109}
110
111impl LogicalSizeLimits {
112    pub fn new(
113        minimum: Option<Size>,
114        maximum: Option<Size>,
115    ) -> Result<Self, LogicalSizeLimitsError> {
116        if minimum.is_some_and(|size| !is_positive_size(size)) {
117            return Err(LogicalSizeLimitsError::InvalidMinimum);
118        }
119        if maximum.is_some_and(|size| !is_positive_size(size)) {
120            return Err(LogicalSizeLimitsError::InvalidMaximum);
121        }
122        if let (Some(minimum), Some(maximum)) = (minimum, maximum)
123            && (minimum.width > maximum.width || minimum.height > maximum.height)
124        {
125            return Err(LogicalSizeLimitsError::MinimumExceedsMaximum);
126        }
127        Ok(Self { minimum, maximum })
128    }
129
130    #[must_use]
131    pub const fn minimum(self) -> Option<Size> {
132        self.minimum
133    }
134
135    #[must_use]
136    pub const fn maximum(self) -> Option<Size> {
137        self.maximum
138    }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
142pub enum LogicalSizeLimitsError {
143    #[error("minimum logical size must be finite and positive")]
144    InvalidMinimum,
145    #[error("maximum logical size must be finite and positive")]
146    InvalidMaximum,
147    #[error("minimum logical size cannot exceed maximum logical size")]
148    MinimumExceedsMaximum,
149}
150
151/// Application-requested mutable top-level state.
152///
153/// This is intentionally distinct from [`WindowObservedState`]. A setter only
154/// changes requested state; the native backend publishes what the OS actually
155/// reports separately.
156#[derive(Clone, Copy, Debug, PartialEq)]
157pub struct WindowRequestedState {
158    pub visible: bool,
159    pub minimized: bool,
160    pub maximized: bool,
161    pub fullscreen: Option<Fullscreen>,
162    pub resizable: bool,
163    pub decorations: bool,
164    pub size_limits: LogicalSizeLimits,
165    pub level: WindowLevel,
166}
167
168/// Native state that can be queried reliably by the active backend.
169///
170/// `None` means the backend/session does not expose that observation; it does
171/// not mean false.
172#[derive(Clone, Copy, Debug, Default, PartialEq)]
173pub struct WindowObservedState {
174    pub visible: Option<bool>,
175    pub minimized: Option<bool>,
176    pub maximized: Option<bool>,
177    pub fullscreen: Option<bool>,
178    pub resizable: Option<bool>,
179    pub decorations: Option<bool>,
180    pub outer_position: Option<PhysicalScreenPosition>,
181    pub outer_size: Option<PhysicalSize>,
182    pub current_display: Option<DisplayId>,
183}
184
185const fn is_positive_size(size: Size) -> bool {
186    size.width.is_finite() && size.height.is_finite() && size.width > 0.0 && size.height > 0.0
187}