use std::rc::Rc;
use crate::signal::Prop;
use crate::widget::EventContext;
use crate::widget_id::WidgetId;
use crate::widget_tree::WidgetTree;
use super::decorations::DecorationsMode;
use super::icon::WindowIcon;
use super::id::TeksiloWindowId;
use super::placement::WindowPlacement;
use super::state::WindowState;
#[derive(Debug, Clone)]
pub struct ModalConfig {
pub parent: TeksiloWindowId,
pub focus_target: Option<WidgetId>,
}
pub type RootBuilder = Box<dyn FnOnce(&mut WidgetTree, WindowState) -> WidgetId>;
pub type PostRootBuilder = Box<dyn FnOnce(&mut WidgetTree, WidgetId) -> WidgetId>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CloseResponse {
Close,
Veto,
}
pub type CloseGuard = Rc<dyn Fn(&mut EventContext) -> CloseResponse>;
pub type CloseBlockedCallback = Rc<dyn Fn(&mut EventContext)>;
#[derive(Debug, Clone)]
pub struct WindowRemovedEvent {
pub id: TeksiloWindowId,
pub string_id: Option<String>,
pub remaining_windows: usize,
}
pub type WindowRemovedCallback = Rc<dyn Fn(&WindowRemovedEvent)>;
pub struct WindowConfig {
pub title: String,
pub string_id: Option<String>,
pub size: (u32, u32),
pub position: Option<(i32, i32)>,
pub min_size: Option<(u32, u32)>,
pub max_size: Option<(u32, u32)>,
pub restore_geometry: bool,
pub initial_placement: WindowPlacement,
pub decorations: DecorationsMode,
pub resizable: bool,
pub size_to_content: SizeToContent,
pub always_on_top: bool,
pub skip_taskbar: bool,
pub activate_from_env: bool,
pub icon: Option<WindowIcon>,
pub modal: Option<ModalConfig>,
pub root_builder: Option<RootBuilder>,
pub post_root_builder: Option<PostRootBuilder>,
pub on_close_requested: Option<CloseGuard>,
pub can_close: Option<Prop<bool>>,
pub on_close_blocked: Option<CloseBlockedCallback>,
pub on_removed: Option<WindowRemovedCallback>,
}
impl std::fmt::Debug for WindowConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WindowConfig")
.field("title", &self.title)
.field("string_id", &self.string_id)
.field("size", &self.size)
.field("position", &self.position)
.field("min_size", &self.min_size)
.field("max_size", &self.max_size)
.field("initial_placement", &self.initial_placement)
.field("decorations", &self.decorations)
.field("resizable", &self.resizable)
.field("size_to_content", &self.size_to_content)
.field("always_on_top", &self.always_on_top)
.field("skip_taskbar", &self.skip_taskbar)
.field("activate_from_env", &self.activate_from_env)
.field("icon", &self.icon.as_ref().map(|i| (i.width, i.height)))
.field("modal", &self.modal)
.field(
"root_builder",
&self.root_builder.as_ref().map(|_| "<closure>"),
)
.field(
"post_root_builder",
&self.post_root_builder.as_ref().map(|_| "<closure>"),
)
.field(
"on_close_requested",
&self.on_close_requested.as_ref().map(|_| "<closure>"),
)
.field("can_close", &self.can_close.as_ref().map(|_| "<signal>"))
.field(
"on_close_blocked",
&self.on_close_blocked.as_ref().map(|_| "<closure>"),
)
.field("on_removed", &self.on_removed.as_ref().map(|_| "<closure>"))
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SizeToContent {
#[default]
Off,
Height,
}
impl SizeToContent {
pub fn sizes_height(self) -> bool {
matches!(self, Self::Height)
}
}
impl WindowConfig {
pub fn new() -> Self {
Self {
title: "Teksilo".to_string(),
string_id: None,
size: (800, 600),
position: None,
min_size: None,
max_size: None,
restore_geometry: true,
initial_placement: WindowPlacement::Floating,
decorations: DecorationsMode::Native,
resizable: true,
size_to_content: SizeToContent::Off,
always_on_top: false,
skip_taskbar: false,
activate_from_env: false,
icon: None,
modal: None,
root_builder: None,
post_root_builder: None,
on_close_requested: None,
can_close: None,
on_close_blocked: None,
on_removed: None,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn size(mut self, width: u32, height: u32) -> Self {
self.size = (width, height);
self
}
pub fn position(mut self, x: i32, y: i32) -> Self {
self.position = Some((x, y));
self
}
pub fn min_size(mut self, width: u32, height: u32) -> Self {
self.min_size = Some((width, height));
self
}
pub fn max_size(mut self, width: u32, height: u32) -> Self {
self.max_size = Some((width, height));
self
}
pub fn restore_geometry(mut self, restore: bool) -> Self {
self.restore_geometry = restore;
self
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.string_id = Some(id.into());
self
}
pub fn initial_placement(mut self, placement: WindowPlacement) -> Self {
self.initial_placement = placement;
self
}
pub fn decorations(mut self, mode: DecorationsMode) -> Self {
self.decorations = mode;
self
}
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
pub fn size_to_content(mut self, mode: SizeToContent) -> Self {
self.size_to_content = mode;
self
}
pub fn always_on_top(mut self, on_top: bool) -> Self {
self.always_on_top = on_top;
self
}
pub fn skip_taskbar(mut self, skip: bool) -> Self {
self.skip_taskbar = skip;
self
}
pub fn activate_from_env(mut self, on: bool) -> Self {
self.activate_from_env = on;
self
}
pub fn icon(mut self, icon: WindowIcon) -> Self {
self.icon = Some(icon);
self
}
pub fn modal_to(mut self, parent: TeksiloWindowId) -> Self {
self.modal = Some(ModalConfig {
parent,
focus_target: None,
});
self
}
pub fn modal(mut self, config: ModalConfig) -> Self {
self.modal = Some(config);
self
}
pub fn root(
mut self,
builder: impl FnOnce(&mut WidgetTree, WindowState) -> WidgetId + 'static,
) -> Self {
self.root_builder = Some(Box::new(builder));
self
}
pub fn take_root_builder(&mut self) -> Option<RootBuilder> {
self.root_builder.take()
}
pub fn post_root(
mut self,
builder: impl FnOnce(&mut WidgetTree, WidgetId) -> WidgetId + 'static,
) -> Self {
self.post_root_builder = Some(Box::new(builder));
self
}
pub fn take_post_root_builder(&mut self) -> Option<PostRootBuilder> {
self.post_root_builder.take()
}
pub fn on_close_requested(
mut self,
guard: impl Fn(&mut EventContext) -> CloseResponse + 'static,
) -> Self {
self.on_close_requested = Some(Rc::new(guard));
self
}
pub fn can_close(mut self, may_close: impl Into<Prop<bool>>) -> Self {
self.can_close = Some(may_close.into());
self
}
pub fn on_close_blocked(mut self, on_blocked: impl Fn(&mut EventContext) + 'static) -> Self {
self.on_close_blocked = Some(Rc::new(on_blocked));
self
}
pub fn take_close_guard(&mut self) -> Option<CloseGuard> {
self.on_close_requested.take()
}
pub fn take_can_close(&mut self) -> Option<Prop<bool>> {
self.can_close.take()
}
pub fn take_close_blocked(&mut self) -> Option<CloseBlockedCallback> {
self.on_close_blocked.take()
}
pub fn on_removed(mut self, hook: impl Fn(&WindowRemovedEvent) + 'static) -> Self {
self.on_removed = Some(Rc::new(hook));
self
}
pub fn take_on_removed(&mut self) -> Option<WindowRemovedCallback> {
self.on_removed.take()
}
pub fn is_modal(&self) -> bool {
self.modal.is_some()
}
pub fn modal_parent(&self) -> Option<TeksiloWindowId> {
self.modal.as_ref().map(|m| m.parent)
}
pub fn modal_focus_target(&self) -> Option<WidgetId> {
self.modal.as_ref().and_then(|m| m.focus_target)
}
}
impl Default for WindowConfig {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signal::Signal;
#[test]
fn window_config_defaults() {
let config = WindowConfig::new();
assert_eq!(config.title, "Teksilo");
assert_eq!(config.size, (800, 600));
assert_eq!(config.initial_placement, WindowPlacement::Floating);
assert_eq!(config.decorations, DecorationsMode::Native);
assert!(config.resizable);
assert!(!config.always_on_top);
assert!(!config.skip_taskbar);
assert!(config.modal.is_none());
assert!(config.string_id.is_none());
assert!(config.position.is_none());
assert!(config.min_size.is_none());
assert!(config.max_size.is_none());
assert!(config.on_close_requested.is_none());
assert!(config.can_close.is_none());
assert!(config.on_close_blocked.is_none());
assert!(config.on_removed.is_none());
assert!(config.restore_geometry);
}
#[test]
fn restore_geometry_can_be_opted_out_of_without_giving_up_persistence() {
let config = WindowConfig::new().id("main").restore_geometry(false);
assert!(!config.restore_geometry, "this window must not be restored");
assert_eq!(
config.string_id.as_deref(),
Some("main"),
"...but it keeps its id, so it still persists into that slot"
);
assert!(config.position.is_none());
}
#[test]
fn close_guard_builders_set_and_take() {
let may_close = Signal::new(false);
let mut config = WindowConfig::new()
.on_close_requested(|_ctx| CloseResponse::Veto)
.can_close(may_close.clone())
.on_close_blocked(|_ctx| {});
assert!(config.on_close_requested.is_some());
assert!(config.can_close.is_some());
assert!(config.on_close_blocked.is_some());
let guard = config.take_close_guard();
let signal = config.take_can_close();
let blocked = config.take_close_blocked();
assert!(guard.is_some());
assert!(signal.is_some());
assert!(blocked.is_some());
assert!(config.on_close_requested.is_none());
assert!(config.can_close.is_none());
assert!(config.on_close_blocked.is_none());
signal.unwrap().as_signal().set(true);
assert!(may_close.get());
}
#[test]
fn on_removed_builder_sets_and_takes() {
use std::cell::RefCell;
use std::rc::Rc;
let seen: Rc<RefCell<Vec<WindowRemovedEvent>>> = Rc::new(RefCell::new(Vec::new()));
let log = seen.clone();
let mut config =
WindowConfig::new().on_removed(move |ev| log.borrow_mut().push(ev.clone()));
assert!(config.on_removed.is_some());
let hook = config.take_on_removed();
assert!(hook.is_some());
assert!(config.on_removed.is_none());
let id = TeksiloWindowId::new(7);
hook.unwrap()(&WindowRemovedEvent {
id,
string_id: Some("main".to_string()),
remaining_windows: 0,
});
let logged = seen.borrow();
assert_eq!(logged.len(), 1);
assert_eq!(logged[0].id, id);
assert_eq!(logged[0].string_id.as_deref(), Some("main"));
assert_eq!(logged[0].remaining_windows, 0);
}
#[test]
fn builder_sets_fields() {
let config = WindowConfig::new()
.title("Test")
.size(400, 300)
.id("test-window")
.initial_placement(WindowPlacement::Fullscreen)
.decorations(DecorationsMode::CustomChrome)
.min_size(200, 150)
.resizable(false)
.always_on_top(true)
.skip_taskbar(true)
.position(100, 50);
assert_eq!(config.title, "Test");
assert_eq!(config.size, (400, 300));
assert_eq!(config.string_id, Some("test-window".to_string()));
assert_eq!(config.initial_placement, WindowPlacement::Fullscreen);
assert_eq!(config.decorations, DecorationsMode::CustomChrome);
assert_eq!(config.min_size, Some((200, 150)));
assert_eq!(config.position, Some((100, 50)));
assert!(!config.resizable);
assert!(config.always_on_top);
assert!(config.skip_taskbar);
}
#[test]
fn modal_to_sets_parent() {
let parent = TeksiloWindowId::new(3);
let config = WindowConfig::new().modal_to(parent);
assert!(config.is_modal());
assert_eq!(config.modal_parent(), Some(parent));
assert_eq!(config.modal_focus_target(), None);
}
#[test]
fn modal_with_focus_target() {
let parent = TeksiloWindowId::new(3);
let target = WidgetId::default();
let config = WindowConfig::new().modal(ModalConfig {
parent,
focus_target: Some(target),
});
assert!(config.is_modal());
assert_eq!(config.modal_parent(), Some(parent));
assert_eq!(config.modal_focus_target(), Some(target));
}
}