#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum WindowOptionsError {
#[error("webview title exceeds 1024 UTF-8 bytes or contains a control character")]
InvalidTitle,
#[error("webview logical width and height must each be between 1 and 16384")]
InvalidSize,
#[error(
"webview app id must be 1 to 128 bytes of dot-separated [A-Za-z0-9_-] elements, \
at least two, none empty or starting with a digit"
)]
InvalidAppId,
#[error("webview logical position must have each coordinate between -32768 and 32767")]
InvalidPosition,
}
pub(crate) const MAX_APP_ID_BYTES: usize = 128;
const SIZE_BOUND: std::ops::RangeInclusive<u32> = 1..=16384;
const POSITION_BOUND: std::ops::RangeInclusive<i32> = -32768..=32767;
pub(crate) fn validate_size(width: u32, height: u32) -> Result<(), WindowOptionsError> {
if SIZE_BOUND.contains(&width) && SIZE_BOUND.contains(&height) {
Ok(())
} else {
Err(WindowOptionsError::InvalidSize)
}
}
pub(crate) fn validate_position(x: i32, y: i32) -> Result<(), WindowOptionsError> {
if POSITION_BOUND.contains(&x) && POSITION_BOUND.contains(&y) {
Ok(())
} else {
Err(WindowOptionsError::InvalidPosition)
}
}
pub(crate) fn validate_app_id(app_id: &str) -> Result<(), WindowOptionsError> {
let valid = (1..=MAX_APP_ID_BYTES).contains(&app_id.len())
&& app_id.contains('.')
&& app_id.split('.').all(|element| {
element
.bytes()
.next()
.is_some_and(|first| !first.is_ascii_digit())
&& element
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
});
if valid {
Ok(())
} else {
Err(WindowOptionsError::InvalidAppId)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BestEffort {
Requested,
Unsupported,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WebviewWindowSupport {
pub position: BestEffort,
pub keep_above: BestEffort,
pub skip_taskbar: BestEffort,
}
impl WebviewWindowSupport {
pub(crate) const fn for_host(compositor_places_windows: bool) -> Self {
let outcome = if compositor_places_windows {
BestEffort::Unsupported
} else {
BestEffort::Requested
};
Self {
position: outcome,
keep_above: outcome,
skip_taskbar: outcome,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WebviewWindowOptions {
pub(crate) title: String,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) decorations: bool,
pub(crate) transparent: bool,
pub(crate) always_on_top: bool,
pub(crate) skip_taskbar: bool,
pub(crate) position: Option<(i32, i32)>,
}
impl WebviewWindowOptions {
pub fn new(title: &str, width: u32, height: u32) -> Result<Self, WindowOptionsError> {
if title.len() > 1024 || title.chars().any(char::is_control) {
return Err(WindowOptionsError::InvalidTitle);
}
validate_size(width, height)?;
Ok(Self {
title: title.to_owned(),
width,
height,
decorations: true,
transparent: false,
always_on_top: false,
skip_taskbar: false,
position: None,
})
}
#[must_use]
pub const fn decorations(mut self, decorations: bool) -> Self {
self.decorations = decorations;
self
}
#[must_use]
pub const fn transparent(mut self, transparent: bool) -> Self {
self.transparent = transparent;
self
}
#[must_use]
pub const fn always_on_top(mut self, always_on_top: bool) -> Self {
self.always_on_top = always_on_top;
self
}
#[must_use]
pub const fn skip_taskbar(mut self, skip_taskbar: bool) -> Self {
self.skip_taskbar = skip_taskbar;
self
}
pub fn initial_position(mut self, x: i32, y: i32) -> Result<Self, WindowOptionsError> {
validate_position(x, y)?;
self.position = Some((x, y));
Ok(self)
}
pub const fn has_decorations(&self) -> bool {
self.decorations
}
pub const fn is_transparent(&self) -> bool {
self.transparent
}
pub const fn is_always_on_top(&self) -> bool {
self.always_on_top
}
pub const fn skips_taskbar(&self) -> bool {
self.skip_taskbar
}
pub const fn logical_position(&self) -> Option<(i32, i32)> {
self.position
}
pub fn title(&self) -> &str {
&self.title
}
pub const fn logical_size(&self) -> (u32, u32) {
(self.width, self.height)
}
}