use core::fmt;
use crate::{AspectRatio, LogicalSize, MonitorId, PhysicalSize};
#[derive(Debug)]
pub enum Window {}
pub type WindowId = crcbl_core::Handle<Window>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum DisplayMode {
#[default]
Windowed,
Borderless {
monitor: Option<MonitorId>,
},
}
impl DisplayMode {
#[inline]
#[must_use]
pub const fn is_borderless(self) -> bool {
matches!(self, Self::Borderless { .. })
}
#[must_use]
pub const fn satisfied_by(self, effective: Self) -> bool {
match (self, effective) {
(Self::Windowed, Self::Windowed) => true,
(Self::Borderless { monitor: None }, Self::Borderless { .. }) => true,
(
Self::Borderless {
monitor: Some(wanted),
},
Self::Borderless { monitor: Some(got) },
) => wanted.0 == got.0,
_ => false,
}
}
}
impl fmt::Display for DisplayMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Windowed => f.write_str("windowed"),
Self::Borderless { monitor: None } => f.write_str("borderless"),
Self::Borderless {
monitor: Some(monitor),
} => write!(f, "borderless on {monitor}"),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct SizeConstraints {
pub min: Option<LogicalSize>,
pub max: Option<LogicalSize>,
pub aspect: Option<AspectRatio>,
}
impl SizeConstraints {
pub const NONE: Self = Self {
min: None,
max: None,
aspect: None,
};
#[must_use]
pub const fn min(size: LogicalSize) -> Self {
Self {
min: Some(size),
max: None,
aspect: None,
}
}
#[must_use]
pub const fn with_aspect(mut self, aspect: AspectRatio) -> Self {
self.aspect = Some(aspect);
self
}
#[must_use]
pub fn apply(&self, size: PhysicalSize, scale_factor: f64) -> PhysicalSize {
let mut result = size;
if let Some(min) = self.min {
let min = min.to_physical(scale_factor);
result.width = result.width.max(min.width);
result.height = result.height.max(min.height);
}
if let Some(max) = self.max {
let max = max.to_physical(scale_factor);
result.width = result.width.min(max.width);
result.height = result.height.min(max.height);
}
if let Some(aspect) = self.aspect {
result = aspect.fit(result);
}
result
}
}
#[derive(Clone, Copy, Debug)]
pub struct WindowDesc<'a> {
pub title: &'a str,
pub app_id: &'a str,
pub size: LogicalSize,
pub constraints: SizeConstraints,
pub mode: DisplayMode,
pub resizable: bool,
pub visible: bool,
pub accept_drops: bool,
pub role: crate::WindowRole<'a>,
pub transparent: bool,
}
impl Default for WindowDesc<'_> {
fn default() -> Self {
Self {
title: "Crucible",
app_id: "sh.kryptic.crcbl",
size: LogicalSize::new(1280.0, 720.0),
constraints: SizeConstraints::NONE,
mode: DisplayMode::Windowed,
resizable: true,
visible: true,
accept_drops: false,
role: crate::WindowRole::Toplevel,
transparent: false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WindowConfiguration {
pub size: PhysicalSize,
pub scale_factor: f64,
pub mode: DisplayMode,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WindowState {
pub configuration: Option<WindowConfiguration>,
pub requested_mode: DisplayMode,
pub requested_constraints: SizeConstraints,
pub focused: bool,
pub visible: bool,
pub pointer_mode: crate::PointerMode,
pub close_pending: bool,
}
impl WindowState {
#[inline]
#[must_use]
pub fn size(&self) -> Option<PhysicalSize> {
self.configuration.map(|config| config.size)
}
#[inline]
#[must_use]
pub fn scale_factor(&self) -> Option<f64> {
self.configuration.map(|config| config.scale_factor)
}
#[inline]
#[must_use]
pub fn effective_mode(&self) -> Option<DisplayMode> {
self.configuration.map(|config| config.mode)
}
#[inline]
#[must_use]
pub const fn is_configured(&self) -> bool {
self.configuration.is_some()
}
#[must_use]
pub fn mode_request_honoured(&self) -> bool {
self.effective_mode()
.is_some_and(|effective| self.requested_mode.satisfied_by(effective))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum CloseReply {
#[default]
Close,
Keep,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn there_is_no_true_fullscreen_variant() {
let modes = [
DisplayMode::Windowed,
DisplayMode::Borderless { monitor: None },
];
for mode in modes {
match mode {
DisplayMode::Windowed => assert!(!mode.is_borderless()),
DisplayMode::Borderless { .. } => assert!(mode.is_borderless()),
}
}
assert_eq!(DisplayMode::default(), DisplayMode::Windowed);
assert_eq!(DisplayMode::Windowed.to_string(), "windowed");
assert_eq!(
DisplayMode::Borderless { monitor: None }.to_string(),
"borderless"
);
assert_eq!(
DisplayMode::Borderless {
monitor: Some(MonitorId(3))
}
.to_string(),
"borderless on monitor 3"
);
}
#[test]
fn constraints_clamp_then_fit() {
let constraints = SizeConstraints {
min: Some(LogicalSize::new(320.0, 240.0)),
max: Some(LogicalSize::new(1920.0, 1080.0)),
aspect: None,
};
assert_eq!(
constraints.apply(PhysicalSize::new(100, 100), 1.0),
PhysicalSize::new(320, 240)
);
assert_eq!(
constraints.apply(PhysicalSize::new(4000, 4000), 1.0),
PhysicalSize::new(1920, 1080)
);
assert_eq!(
constraints.apply(PhysicalSize::new(100, 100), 2.0),
PhysicalSize::new(640, 480)
);
}
#[test]
fn an_aspect_lock_wins_against_the_minimum() {
let constraints = SizeConstraints::min(LogicalSize::new(640.0, 640.0))
.with_aspect(AspectRatio::WIDESCREEN);
let result = constraints.apply(PhysicalSize::new(100, 100), 1.0);
assert_eq!(result, PhysicalSize::new(640, 360));
assert!(result.height < 640, "the aspect fit took precedence");
}
#[test]
fn no_constraints_is_the_identity() {
let size = PhysicalSize::new(1234, 567);
assert_eq!(SizeConstraints::NONE.apply(size, 1.0), size);
assert_eq!(SizeConstraints::default(), SizeConstraints::NONE);
}
#[test]
fn an_unconfigured_window_has_no_size_at_all() {
let unconfigured = WindowState {
configuration: None,
requested_mode: DisplayMode::Borderless { monitor: None },
requested_constraints: SizeConstraints::NONE,
focused: false,
visible: true,
pointer_mode: crate::PointerMode::Free,
close_pending: false,
};
assert!(!unconfigured.is_configured());
assert_eq!(unconfigured.size(), None);
assert_eq!(unconfigured.scale_factor(), None);
assert_eq!(unconfigured.effective_mode(), None);
assert!(
!unconfigured.mode_request_honoured(),
"nothing has been honoured before the first configure"
);
}
#[test]
fn a_refused_mode_request_is_visible_as_a_disagreement() {
let refused = WindowState {
configuration: Some(WindowConfiguration {
size: PhysicalSize::new(1280, 720),
scale_factor: 1.0,
mode: DisplayMode::Windowed,
}),
requested_mode: DisplayMode::Borderless { monitor: None },
requested_constraints: SizeConstraints::NONE,
focused: true,
visible: true,
pointer_mode: crate::PointerMode::Free,
close_pending: false,
};
assert!(refused.is_configured());
assert_eq!(refused.size(), Some(PhysicalSize::new(1280, 720)));
assert_eq!(refused.effective_mode(), Some(DisplayMode::Windowed));
assert!(!refused.mode_request_honoured());
let honoured = WindowState {
requested_mode: DisplayMode::Windowed,
..refused
};
assert!(honoured.mode_request_honoured());
}
#[test]
fn a_request_naming_no_monitor_is_answered_by_one_that_names_a_monitor() {
let anywhere = DisplayMode::Borderless { monitor: None };
let there = DisplayMode::Borderless {
monitor: Some(MonitorId(1)),
};
let elsewhere = DisplayMode::Borderless {
monitor: Some(MonitorId(2)),
};
assert!(anywhere.satisfied_by(there));
assert!(anywhere.satisfied_by(anywhere));
assert!(there.satisfied_by(there));
assert!(!there.satisfied_by(anywhere));
assert!(!there.satisfied_by(elsewhere));
assert!(!anywhere.satisfied_by(DisplayMode::Windowed));
assert!(!DisplayMode::Windowed.satisfied_by(there));
assert!(DisplayMode::Windowed.satisfied_by(DisplayMode::Windowed));
let granted = WindowState {
configuration: Some(WindowConfiguration {
size: PhysicalSize::new(1920, 1080),
scale_factor: 1.0,
mode: there,
}),
requested_mode: anywhere,
requested_constraints: SizeConstraints::NONE,
focused: true,
visible: true,
pointer_mode: crate::PointerMode::Free,
close_pending: false,
};
assert!(granted.mode_request_honoured());
assert!(
!WindowState {
requested_mode: elsewhere,
..granted
}
.mode_request_honoured(),
"asking for monitor 2 and being put on monitor 1 is a refusal"
);
}
#[test]
fn the_default_window_is_a_sane_720p_one() {
let desc = WindowDesc::default();
assert_eq!(desc.size, LogicalSize::new(1280.0, 720.0));
assert_eq!(desc.mode, DisplayMode::Windowed);
assert!(desc.resizable && desc.visible);
assert!(!desc.accept_drops, "drops are opt-in");
assert!(!desc.app_id.is_empty(), "app_id has no useful empty value");
assert_eq!(CloseReply::default(), CloseReply::Close);
}
}