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)]
95pub enum WindowIconError {
96    ZeroDimension,
97    DimensionsOverflow,
98    InvalidByteCount { expected: usize, actual: usize },
99}
100
101impl fmt::Display for WindowIconError {
102    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            Self::ZeroDimension => formatter.write_str("window icon dimensions must be non-zero"),
105            Self::DimensionsOverflow => {
106                formatter.write_str("window icon dimensions overflow addressable memory")
107            }
108            Self::InvalidByteCount { expected, actual } => write!(
109                formatter,
110                "window icon RGBA byte count is {actual}, expected {expected}"
111            ),
112        }
113    }
114}
115
116impl std::error::Error for WindowIconError {}
117
118/// Validated dynamic logical-size constraints for a native window.
119#[derive(Clone, Copy, Debug, Default, PartialEq)]
120pub struct LogicalSizeLimits {
121    minimum: Option<Size>,
122    maximum: Option<Size>,
123}
124
125impl LogicalSizeLimits {
126    pub fn new(
127        minimum: Option<Size>,
128        maximum: Option<Size>,
129    ) -> Result<Self, LogicalSizeLimitsError> {
130        if minimum.is_some_and(|size| !is_positive_size(size)) {
131            return Err(LogicalSizeLimitsError::InvalidMinimum);
132        }
133        if maximum.is_some_and(|size| !is_positive_size(size)) {
134            return Err(LogicalSizeLimitsError::InvalidMaximum);
135        }
136        if let (Some(minimum), Some(maximum)) = (minimum, maximum)
137            && (minimum.width > maximum.width || minimum.height > maximum.height)
138        {
139            return Err(LogicalSizeLimitsError::MinimumExceedsMaximum);
140        }
141        Ok(Self { minimum, maximum })
142    }
143
144    #[must_use]
145    pub const fn minimum(self) -> Option<Size> {
146        self.minimum
147    }
148
149    #[must_use]
150    pub const fn maximum(self) -> Option<Size> {
151        self.maximum
152    }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq, Eq)]
156pub enum LogicalSizeLimitsError {
157    InvalidMinimum,
158    InvalidMaximum,
159    MinimumExceedsMaximum,
160}
161
162impl fmt::Display for LogicalSizeLimitsError {
163    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164        formatter.write_str(match self {
165            Self::InvalidMinimum => "minimum logical size must be finite and positive",
166            Self::InvalidMaximum => "maximum logical size must be finite and positive",
167            Self::MinimumExceedsMaximum => {
168                "minimum logical size cannot exceed maximum logical size"
169            }
170        })
171    }
172}
173
174impl std::error::Error for LogicalSizeLimitsError {}
175
176/// Application-requested mutable top-level state.
177///
178/// This is intentionally distinct from [`WindowObservedState`]. A setter only
179/// changes requested state; the native backend publishes what the OS actually
180/// reports separately.
181#[derive(Clone, Copy, Debug, PartialEq)]
182pub struct WindowRequestedState {
183    pub visible: bool,
184    pub minimized: bool,
185    pub maximized: bool,
186    pub fullscreen: Option<Fullscreen>,
187    pub resizable: bool,
188    pub decorations: bool,
189    pub size_limits: LogicalSizeLimits,
190    pub level: WindowLevel,
191}
192
193/// Native state that can be queried reliably by the active backend.
194///
195/// `None` means the backend/session does not expose that observation; it does
196/// not mean false.
197#[derive(Clone, Copy, Debug, Default, PartialEq)]
198pub struct WindowObservedState {
199    pub visible: Option<bool>,
200    pub minimized: Option<bool>,
201    pub maximized: Option<bool>,
202    pub fullscreen: Option<bool>,
203    pub resizable: Option<bool>,
204    pub decorations: Option<bool>,
205    pub outer_position: Option<PhysicalScreenPosition>,
206    pub outer_size: Option<PhysicalSize>,
207    pub current_display: Option<DisplayId>,
208}
209
210const fn is_positive_size(size: Size) -> bool {
211    size.width.is_finite() && size.height.is_finite() && size.width > 0.0 && size.height > 0.0
212}