use crate::etf_decode;
use eetf::{Map, Term};
use gpui::{
App, Bounds, DisplayId, Point, SharedString, TitlebarOptions, WindowBackgroundAppearance,
WindowBounds, WindowDecorations, WindowKind, WindowOptions, point, px, size,
};
#[derive(Clone, Debug, Default)]
pub(crate) struct WindowOptionsConfig {
pub window_bounds: Option<WindowBoundsConfig>,
pub titlebar: Option<TitlebarConfig>,
pub focus: Option<bool>,
pub show: Option<bool>,
pub kind: Option<WindowKindConfig>,
pub is_movable: Option<bool>,
pub is_resizable: Option<bool>,
pub is_minimizable: Option<bool>,
pub display_id: Option<u32>,
pub window_background: Option<WindowBackgroundConfig>,
pub app_id: Option<String>,
pub window_min_size: Option<SizeConfig>,
pub window_decorations: Option<WindowDecorationsConfig>,
pub tabbing_identifier: Option<String>,
}
#[derive(Clone, Debug)]
pub(crate) struct WindowBoundsConfig {
pub x: Option<i32>,
pub y: Option<i32>,
pub width: u32,
pub height: u32,
pub state: WindowBoundsState,
}
#[derive(Clone, Debug)]
pub(crate) enum TitlebarConfig {
Hidden,
Custom(TitlebarConfigOptions),
}
#[derive(Clone, Debug, Default)]
pub(crate) struct TitlebarConfigOptions {
pub title: Option<String>,
pub appears_transparent: Option<bool>,
pub traffic_light_position: Option<PointConfig>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct PointConfig {
pub x: u32,
pub y: u32,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct SizeConfig {
pub width: u32,
pub height: u32,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum WindowBoundsState {
Windowed,
Maximized,
Fullscreen,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum WindowKindConfig {
Normal,
PopUp,
Floating,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum WindowBackgroundConfig {
Opaque,
Transparent,
Blurred,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum WindowDecorationsConfig {
Server,
Client,
}
impl WindowOptionsConfig {
pub(crate) fn decode_etf(bytes: &[u8]) -> Result<Self, String> {
let term = etf_decode::decode_term(bytes)?;
Self::from_term(&term)
}
pub(crate) fn into_gpui(self, cx: &mut App) -> WindowOptions {
let Self {
window_bounds,
titlebar,
focus,
show,
kind,
is_movable,
is_resizable,
is_minimizable,
display_id,
window_background,
app_id,
window_min_size,
window_decorations,
tabbing_identifier,
} = self;
let mut options = WindowOptions::default();
let display_id = display_id.and_then(|id| display_id_from_raw(id, cx));
if let Some(window_bounds) = window_bounds {
options.window_bounds = Some(window_bounds.into_gpui(display_id, cx));
}
if let Some(titlebar) = titlebar {
options.titlebar = match titlebar {
TitlebarConfig::Hidden => None,
TitlebarConfig::Custom(config) => Some(config.into_gpui()),
};
}
if let Some(focus) = focus {
options.focus = focus;
}
if let Some(show) = show {
options.show = show;
}
if let Some(kind) = kind {
options.kind = kind.to_gpui();
}
if let Some(is_movable) = is_movable {
options.is_movable = is_movable;
}
if let Some(is_resizable) = is_resizable {
options.is_resizable = is_resizable;
}
if let Some(is_minimizable) = is_minimizable {
options.is_minimizable = is_minimizable;
}
options.display_id = display_id;
if let Some(window_background) = window_background {
options.window_background = window_background.to_gpui();
}
options.app_id = app_id;
if let Some(window_min_size) = window_min_size {
options.window_min_size = Some(window_min_size.to_gpui());
}
if let Some(window_decorations) = window_decorations {
options.window_decorations = Some(window_decorations.to_gpui());
}
options.tabbing_identifier = tabbing_identifier;
options
}
fn from_term(term: &Term) -> Result<Self, String> {
let map = expect_map(term)?;
ensure_allowed_fields(
map,
&[
"window_bounds",
"titlebar",
"focus",
"show",
"kind",
"is_movable",
"is_resizable",
"is_minimizable",
"display_id",
"window_background",
"app_id",
"window_min_size",
"window_decorations",
"tabbing_identifier",
],
"window options",
)?;
Ok(Self {
window_bounds: get_optional_map_field(map, "window_bounds")?
.map(WindowBoundsConfig::from_map)
.transpose()?,
titlebar: match get_field(map, "titlebar") {
Some(Term::Atom(atom)) if atom.name == "false" => Some(TitlebarConfig::Hidden),
Some(term) => Some(TitlebarConfig::Custom(TitlebarConfigOptions::from_map(
expect_map(term)?,
)?)),
None => None,
},
focus: get_optional_bool_field(map, "focus")?,
show: get_optional_bool_field(map, "show")?,
kind: get_optional_atom_field(map, "kind")?
.map(parse_window_kind)
.transpose()?,
is_movable: get_optional_bool_field(map, "is_movable")?,
is_resizable: get_optional_bool_field(map, "is_resizable")?,
is_minimizable: get_optional_bool_field(map, "is_minimizable")?,
display_id: get_optional_u32_field(map, "display_id")?,
window_background: get_optional_atom_field(map, "window_background")?
.map(parse_window_background)
.transpose()?,
app_id: get_optional_string_field(map, "app_id")?,
window_min_size: get_optional_map_field(map, "window_min_size")?
.map(SizeConfig::from_map)
.transpose()?,
window_decorations: get_optional_atom_field(map, "window_decorations")?
.map(parse_window_decorations)
.transpose()?,
tabbing_identifier: get_optional_string_field(map, "tabbing_identifier")?,
})
}
}
impl WindowBoundsConfig {
fn from_map(map: &Map) -> Result<Self, String> {
ensure_allowed_fields(
map,
&["x", "y", "width", "height", "state"],
"window_bounds",
)?;
Ok(Self {
x: get_optional_i32_field(map, "x")?,
y: get_optional_i32_field(map, "y")?,
width: get_u32_field(map, "width")?,
height: get_u32_field(map, "height")?,
state: get_optional_atom_field(map, "state")?
.map(parse_window_bounds_state)
.transpose()?
.unwrap_or(WindowBoundsState::Windowed),
})
}
fn into_gpui(self, display_id: Option<DisplayId>, cx: &mut App) -> WindowBounds {
let size_px = size(px(self.width as f32), px(self.height as f32));
let bounds = match (self.x, self.y) {
(Some(x), Some(y)) => Bounds::from_corners(
point(px(x as f32), px(y as f32)),
point(
px(x as f32 + self.width as f32),
px(y as f32 + self.height as f32),
),
),
_ => Bounds::centered(display_id, size_px, cx),
};
match self.state {
WindowBoundsState::Windowed => WindowBounds::Windowed(bounds),
WindowBoundsState::Maximized => WindowBounds::Maximized(bounds),
WindowBoundsState::Fullscreen => WindowBounds::Fullscreen(bounds),
}
}
}
impl TitlebarConfigOptions {
fn from_map(map: &Map) -> Result<Self, String> {
ensure_allowed_fields(
map,
&["title", "appears_transparent", "traffic_light_position"],
"titlebar",
)?;
Ok(Self {
title: get_optional_string_field(map, "title")?,
appears_transparent: get_optional_bool_field(map, "appears_transparent")?,
traffic_light_position: get_optional_map_field(map, "traffic_light_position")?
.map(PointConfig::from_map)
.transpose()?,
})
}
fn into_gpui(self) -> TitlebarOptions {
TitlebarOptions {
title: self.title.map(SharedString::from),
appears_transparent: self.appears_transparent.unwrap_or_default(),
traffic_light_position: self.traffic_light_position.map(PointConfig::to_gpui),
}
}
}
impl PointConfig {
fn from_map(map: &Map) -> Result<Self, String> {
ensure_allowed_fields(map, &["x", "y"], "point")?;
Ok(Self {
x: get_u32_field(map, "x")?,
y: get_u32_field(map, "y")?,
})
}
fn to_gpui(self) -> Point<gpui::Pixels> {
point(px(self.x as f32), px(self.y as f32))
}
}
impl SizeConfig {
fn from_map(map: &Map) -> Result<Self, String> {
ensure_allowed_fields(map, &["width", "height"], "size")?;
Ok(Self {
width: get_u32_field(map, "width")?,
height: get_u32_field(map, "height")?,
})
}
fn to_gpui(self) -> gpui::Size<gpui::Pixels> {
size(px(self.width as f32), px(self.height as f32))
}
}
impl WindowKindConfig {
fn to_gpui(self) -> WindowKind {
match self {
Self::Normal => WindowKind::Normal,
Self::PopUp => WindowKind::PopUp,
Self::Floating => WindowKind::Floating,
}
}
}
impl WindowBackgroundConfig {
fn to_gpui(self) -> WindowBackgroundAppearance {
match self {
Self::Opaque => WindowBackgroundAppearance::Opaque,
Self::Transparent => WindowBackgroundAppearance::Transparent,
Self::Blurred => WindowBackgroundAppearance::Blurred,
}
}
}
impl WindowDecorationsConfig {
fn to_gpui(self) -> WindowDecorations {
match self {
Self::Server => WindowDecorations::Server,
Self::Client => WindowDecorations::Client,
}
}
}
fn parse_window_bounds_state(value: &str) -> Result<WindowBoundsState, String> {
match value {
"windowed" => Ok(WindowBoundsState::Windowed),
"maximized" => Ok(WindowBoundsState::Maximized),
"fullscreen" => Ok(WindowBoundsState::Fullscreen),
_ => Err("invalid window_bounds.state".into()),
}
}
fn parse_window_kind(value: &str) -> Result<WindowKindConfig, String> {
match value {
"normal" => Ok(WindowKindConfig::Normal),
"popup" | "pop_up" => Ok(WindowKindConfig::PopUp),
"floating" => Ok(WindowKindConfig::Floating),
_ => Err("invalid kind".into()),
}
}
fn parse_window_background(value: &str) -> Result<WindowBackgroundConfig, String> {
match value {
"opaque" => Ok(WindowBackgroundConfig::Opaque),
"transparent" => Ok(WindowBackgroundConfig::Transparent),
"blurred" => Ok(WindowBackgroundConfig::Blurred),
_ => Err("invalid window_background".into()),
}
}
fn display_id_from_raw(id: u32, cx: &mut App) -> Option<DisplayId> {
cx.displays()
.iter()
.map(|display| display.id())
.find(|display_id| u32::from(*display_id) == id)
}
fn parse_window_decorations(value: &str) -> Result<WindowDecorationsConfig, String> {
match value {
"server" => Ok(WindowDecorationsConfig::Server),
"client" => Ok(WindowDecorationsConfig::Client),
_ => Err("invalid window_decorations".into()),
}
}
fn get_field<'a>(map: &'a Map, key: &str) -> Option<&'a Term> {
etf_decode::get_atom_keyed_field(&map.map, key)
}
fn ensure_allowed_fields(map: &Map, allowed: &[&str], context: &str) -> Result<(), String> {
etf_decode::ensure_atom_keyed_allowed_fields(&map.map, allowed, context)
}
fn get_optional_map_field<'a>(map: &'a Map, key: &str) -> Result<Option<&'a Map>, String> {
match get_field(map, key) {
Some(term) => Ok(Some(expect_map(term)?)),
None => Ok(None),
}
}
fn get_optional_bool_field(map: &Map, key: &str) -> Result<Option<bool>, String> {
match get_field(map, key) {
Some(Term::Atom(atom)) => match atom.name.as_str() {
"true" => Ok(Some(true)),
"false" => Ok(Some(false)),
_ => Err(format!("expected boolean for {key}")),
},
Some(_) => Err(format!("expected boolean for {key}")),
None => Ok(None),
}
}
fn get_optional_string_field(map: &Map, key: &str) -> Result<Option<String>, String> {
match get_field(map, key) {
Some(term) => Ok(Some(expect_string(term)?)),
None => Ok(None),
}
}
fn get_optional_atom_field<'a>(map: &'a Map, key: &str) -> Result<Option<&'a str>, String> {
match get_field(map, key) {
Some(Term::Atom(atom)) => Ok(Some(atom.name.as_str())),
Some(_) => Err(format!("expected atom for {key}")),
None => Ok(None),
}
}
fn get_u32_field(map: &Map, key: &str) -> Result<u32, String> {
get_optional_u32_field(map, key)?.ok_or_else(|| format!("missing required field {key}"))
}
fn get_optional_u32_field(map: &Map, key: &str) -> Result<Option<u32>, String> {
match get_field(map, key) {
Some(Term::FixInteger(value)) if value.value >= 0 => u32::try_from(value.value)
.map(Some)
.map_err(|_| format!("invalid integer for {key}")),
Some(Term::BigInteger(value)) => value
.value
.clone()
.try_into()
.map(Some)
.map_err(|_| format!("invalid integer for {key}")),
Some(_) => Err(format!("expected positive integer for {key}")),
None => Ok(None),
}
}
fn get_optional_i32_field(map: &Map, key: &str) -> Result<Option<i32>, String> {
match get_field(map, key) {
Some(Term::FixInteger(value)) => Ok(Some(value.value)),
Some(Term::BigInteger(value)) => value
.value
.clone()
.try_into()
.map(Some)
.map_err(|_| format!("invalid integer for {key}")),
Some(_) => Err(format!("expected integer for {key}")),
None => Ok(None),
}
}
fn expect_map(term: &Term) -> Result<&Map, String> {
etf_decode::expect_eetf_map(term, "expected map")
}
fn expect_string(term: &Term) -> Result<String, String> {
etf_decode::term_to_binary_string(term)
}
#[cfg(test)]
#[path = "window_options_tests.rs"]
mod tests;