use std::cell::{Cell, OnceCell, RefCell};
use std::rc::Rc;
use day_reactive::{Scope, Signal};
use day_spec::props::{CoverPatch, CoverProps};
use day_spec::{
Attention, Event, NodeId, Point, Rect, Size, WindowChange, WindowKind, WindowLevel,
WindowOptions, WindowRole, WindowState, WindowTabbing, kinds,
};
use crate::AnyPiece;
use crate::build::{Boundary, BuildCx, Piece};
use crate::tree::{RNode, WindowRootReply, id_to_rnode, rnode_to_id, with_tree};
fn focus_revision() -> Signal<u64> {
FOCUS_REVISION.with(|revision| *revision.get_or_init(|| Signal::global(0)))
}
enum Tier {
Native { closing: Cell<bool> },
PendingNative {
build: Option<Box<dyn FnOnce() -> AnyPiece>>,
title: String,
},
Cover {
cover: RNode,
closing: Rc<Cell<bool>>,
},
}
type OnCloseList = Rc<RefCell<Vec<Box<dyn Fn()>>>>;
struct WindowRecord {
root: RNode,
key: Option<String>,
kind: WindowKind,
role: WindowRole,
scope: Scope,
content_scope: Option<Scope>,
tier: Tier,
focused: bool,
on_close: OnCloseList,
close_request: Option<Rc<dyn Fn()>>,
state: Signal<WindowState>,
reported: Rc<Cell<WindowState>>,
remember: Option<String>,
}
impl WindowRecord {
fn closing(&self) -> bool {
match &self.tier {
Tier::Native { closing } => closing.get(),
Tier::Cover { closing, .. } => closing.get(),
Tier::PendingNative { .. } => false,
}
}
}
day_reactive::tls_slots! {
windows;
static WINDOWS: RefCell<Vec<WindowRecord>> = const { RefCell::new(Vec::new()) };
static FOCUS_REVISION: OnceCell<Signal<u64>> = const { OnceCell::new() };
static INITIAL_WINDOW: Cell<Option<RNode>> = const { Cell::new(None) };
static QUITTING: Cell<bool> = const { Cell::new(false) };
static PREFS: RefCell<Option<PrefsRegistration>> = const { RefCell::new(None) };
static PREFS_ACTION: Cell<u64> = const { Cell::new(0) };
static ABOUT: RefCell<Option<Rc<dyn Fn()>>> = const { RefCell::new(None) };
static ABOUT_ACTION: Cell<u64> = const { Cell::new(0) };
static UPDATES: RefCell<Option<Rc<dyn Fn()>>> = const { RefCell::new(None) };
static UPDATES_ACTION: Cell<u64> = const { Cell::new(0) };
static NEW_WINDOW: RefCell<Option<Rc<dyn Fn() -> AnyPiece>>> = const { RefCell::new(None) };
static TAB_NEW: RefCell<Vec<(String, Rc<dyn Fn()>)>> = const { RefCell::new(Vec::new()) };
static GROUP_WINDOWS: RefCell<Vec<(String, Rc<dyn Fn() -> AnyPiece>)>> = const { RefCell::new(Vec::new()) };
static NEW_WINDOW_ACTION: Cell<u64> = const { Cell::new(0) };
static LAUNCH_OPTIONS: RefCell<Option<WindowOptions>> = const { RefCell::new(None) };
}
pub fn set_launch_options(options: &WindowOptions) {
LAUNCH_OPTIONS.with(|o| *o.borrow_mut() = Some(options.clone()));
}
pub fn launch_options() -> Option<WindowOptions> {
LAUNCH_OPTIONS.with(|o| o.borrow().clone())
}
#[derive(Clone)]
pub struct WindowHandle {
root: RNode,
}
impl WindowHandle {
pub fn set_content_scope(&self, scope: Scope) {
WINDOWS.with(|w| {
if let Some(r) = w.borrow_mut().iter_mut().find(|r| r.root == self.root) {
r.content_scope = Some(scope);
}
});
focus_revision().update(|n| *n += 1);
}
pub fn on_close_request(&self, f: impl Fn() + 'static) {
WINDOWS.with(|w| {
if let Some(r) = w.borrow_mut().iter_mut().find(|r| r.root == self.root) {
r.close_request = Some(Rc::new(f));
}
});
}
pub fn on_focus(&self, f: impl Fn() + 'static) {
with_tree(|t| {
t.on_event(
self.root,
Rc::new(move |ev| {
if matches!(ev, Event::WindowFocused(true)) {
f();
}
}),
)
});
}
pub fn tab_order(&self) -> Vec<NodeId> {
with_tree(|t| t.window_tab_order(self.root))
}
pub fn close(&self) {
let root = self.root;
let action = WINDOWS.with(|w| {
w.borrow()
.iter()
.find(|r| r.root == root)
.map(|r| match &r.tier {
Tier::Native { closing } => {
if closing.replace(true) {
CloseAction::None
} else {
CloseAction::Native
}
}
Tier::PendingNative { .. } => CloseAction::Immediate,
Tier::Cover { cover, closing } => {
if closing.get() {
CloseAction::None
} else {
closing.set(true);
CloseAction::Cover(*cover)
}
}
})
});
focus_revision().update(|n| *n += 1);
match action {
Some(CloseAction::Native) => with_tree(|t| t.close_native_window(root)),
Some(CloseAction::Immediate) => teardown(root),
Some(CloseAction::Cover(cover)) => {
with_tree(|t| t.patch(cover, Box::new(CoverPatch::Dismiss), false));
}
Some(CloseAction::None) | None => {}
}
}
pub fn focus(&self) {
let root = self.root;
if WINDOWS.with(|w| {
w.borrow()
.iter()
.any(|r| r.root == root && !r.closing() && matches!(r.tier, Tier::Native { .. }))
}) {
WINDOWS.with(|windows| {
for window in windows.borrow_mut().iter_mut() {
window.focused = window.root == root;
}
});
focus_revision().update(|n| *n += 1);
with_tree(|t| t.focus_native_window(root));
}
}
pub fn set_title(&self, title: &str) {
let root = self.root;
let title = &crate::decorate_window_title(title);
let live = WINDOWS.with(|w| {
let mut windows = w.borrow_mut();
match windows.iter_mut().find(|r| r.root == root) {
Some(r) => match &mut r.tier {
Tier::Native { .. } => true,
Tier::PendingNative { title: t, .. } => {
*t = title.to_string();
false
}
Tier::Cover { .. } => false,
},
None => false,
}
});
if live {
let title = title.to_string();
with_tree(|t| t.set_native_window_title(root, &title));
}
}
pub fn frame_clock(&self) -> crate::frame::FrameClock {
crate::frame::FrameClock::for_root(self.root)
}
pub fn is_open(&self) -> bool {
let root = self.root;
WINDOWS.with(|w| w.borrow().iter().any(|r| r.root == root))
}
pub fn on_close(&self, f: impl Fn() + 'static) {
let root = self.root;
WINDOWS.with(|w| {
if let Some(r) = w.borrow().iter().find(|r| r.root == root) {
r.on_close.borrow_mut().push(Box::new(f));
}
});
}
pub fn state(&self) -> Signal<WindowState> {
let root = self.root;
WINDOWS
.with(|w| w.borrow().iter().find(|r| r.root == root).map(|r| r.state))
.unwrap_or_else(|| Signal::new(WindowState::Normal))
}
pub fn safe_area(&self) -> day_geometry::Insets {
crate::ambient::window_safe_area(self.root)
}
pub fn set_state(&self, state: WindowState) {
self.state().set(state);
}
pub fn set_content_protected(&self, protected: bool) {
self.apply(WindowChange::ContentProtected(protected));
}
pub fn set_frame(&self, origin: Option<Point>, size: Option<Size>) {
self.apply(WindowChange::Frame { origin, size });
}
pub fn frame(&self) -> Option<Rect> {
if !self.is_native() {
return None;
}
let root = self.root;
with_tree(|t| t.native_window_frame(root))
}
pub fn set_limits(&self, min: Option<Size>, max: Option<Size>) {
self.apply(WindowChange::Limits { min, max });
}
pub fn set_resizable(&self, resizable: bool) {
self.apply(WindowChange::Resizable(resizable));
}
pub fn set_level(&self, level: WindowLevel) {
self.apply(WindowChange::Level(level));
}
pub fn set_on_all_workspaces(&self, on: bool) {
self.apply(WindowChange::OnAllWorkspaces(on));
}
pub fn set_skip_taskbar(&self, skip: bool) {
self.apply(WindowChange::SkipTaskbar(skip));
}
pub fn set_minimizable(&self, on: bool) {
self.apply(WindowChange::Minimizable(on));
}
pub fn set_maximizable(&self, on: bool) {
self.apply(WindowChange::Maximizable(on));
}
pub fn set_closable(&self, on: bool) {
self.apply(WindowChange::Closable(on));
}
pub fn set_visible(&self, visible: bool) {
self.apply(WindowChange::Visible(visible));
}
pub fn set_appearance(&self, dark: Option<bool>) {
self.apply(WindowChange::Appearance(dark));
}
pub fn request_attention(&self, attention: Attention) {
self.apply(WindowChange::RequestAttention(attention));
}
fn apply(&self, change: WindowChange) {
if self.is_native() {
let root = self.root;
with_tree(|t| t.apply_window(root, &change));
}
}
fn is_native(&self) -> bool {
let root = self.root;
WINDOWS.with(|w| {
w.borrow()
.iter()
.any(|r| r.root == root && matches!(r.tier, Tier::Native { .. }))
})
}
}
enum CloseAction {
Native,
Immediate,
Cover(RNode),
None,
}
pub fn open_window<P: Piece>(
mut options: WindowOptions,
build: impl FnOnce() -> P + 'static,
) -> WindowHandle {
let key = options.key.take();
let key = key.as_deref();
let kind = options.kind;
if let Some(k) = key
&& let Some(existing) = window_by_key(k)
{
revive_if_dismissing(existing.root);
existing.focus();
return existing;
}
options.title = crate::decorate_window_title(&options.title);
let scope = Scope::root().enter(Scope::child);
match with_tree(|t| t.open_window_root(&options, kind)) {
WindowRootReply::Open(root) => {
register(
root,
key,
kind,
scope,
Tier::Native {
closing: Cell::new(false),
},
);
wire_window_events(root);
if let Some(k) = &options.remember_frame {
remember_frame(root, k);
}
scope.enter(|| {
crate::toolbar::with_window(root, || {
let piece = build();
let mut cx = BuildCx::new(root);
let _ = piece.build(&mut cx);
})
});
with_tree(|t| {
t.mark_layout_dirty();
t.layout_if_needed();
});
if options.size_to_fit {
let fitted = with_tree(|t| {
t.first_child(root)
.and_then(|c| t.node_frame(c))
.map(|f| f.origin.y + f.size.height)
.unwrap_or(0.0)
});
if fitted > 0.0 && fitted < options.size.height {
with_tree(|t| {
t.fit_window(root, Size::new(options.size.width, fitted));
t.mark_layout_dirty();
t.layout_if_needed();
});
}
}
focus_revision().update(|n| *n += 1);
WindowHandle { root }
}
WindowRootReply::Pending(root) => {
register(
root,
key,
kind,
scope,
Tier::PendingNative {
build: Some(Box::new(move || AnyPiece::new(build()))),
title: options.title.clone(),
},
);
wire_window_events(root);
WindowHandle { root }
}
WindowRootReply::Unsupported => {
open_as_cover(key, kind, scope, Box::new(move || AnyPiece::new(build())))
}
}
}
pub fn window_by_key(key: &str) -> Option<WindowHandle> {
WINDOWS.with(|w| {
w.borrow()
.iter()
.find(|r| {
r.key.as_deref() == Some(key)
&& !matches!(&r.tier, Tier::Native { closing } if closing.get())
})
.map(|r| WindowHandle { root: r.root })
})
}
pub fn focused_window() -> Option<WindowHandle> {
WINDOWS.with(|w| {
w.borrow()
.iter()
.find(|r| r.focused && !r.closing())
.map(|r| WindowHandle { root: r.root })
})
}
pub(crate) fn focused_root() -> RNode {
focused_window()
.map(|w| w.root)
.unwrap_or_else(|| with_tree(|t| t.root_node()))
}
pub fn focused_scope() -> Option<Scope> {
focus_revision().get();
WINDOWS.with(|w| {
let windows = w.borrow();
let focused = windows.iter().find(|r| r.focused && !r.closing());
let initial = INITIAL_WINDOW.with(|c| c.get());
focused
.or_else(|| windows.iter().find(|r| Some(r.root) == initial))
.map(|r| r.content_scope.unwrap_or(r.scope))
})
}
pub(crate) fn focused_scope_chain() -> Vec<Scope> {
focus_revision().get();
WINDOWS.with(|w| {
let windows = w.borrow();
let initial = INITIAL_WINDOW.with(|c| c.get());
let primary = || {
windows
.iter()
.find(|r| Some(r.root) == initial)
.map(|r| r.content_scope.unwrap_or(r.scope))
};
match windows.iter().find(|r| r.focused && !r.closing()) {
Some(front) => {
let own = front.content_scope.unwrap_or(front.scope);
let mut chain = vec![own];
if matches!(front.tier, Tier::Cover { .. }) {
chain.extend(primary().filter(|scope| *scope != own));
}
chain
}
None => primary().into_iter().collect(),
}
})
}
pub fn window_root_by_key(key: &str) -> Option<RNode> {
WINDOWS.with(|w| {
w.borrow()
.iter()
.find(|r| {
r.key.as_deref() == Some(key)
&& !matches!(&r.tier, Tier::Native { closing } if closing.get())
})
.map(|r| match &r.tier {
Tier::Cover { .. } => with_tree(|t| t.root_node()),
_ => r.root,
})
})
}
pub fn finish_window_open(id: NodeId, raw: day_spec::RawHandle, size: Size) -> bool {
let root = id_to_rnode(id);
let pending = WINDOWS.with(|w| {
let mut windows = w.borrow_mut();
let r = windows.iter_mut().find(|r| r.root == root)?;
match &mut r.tier {
Tier::PendingNative { build, title } => {
let b = build.take();
let title = title.clone();
r.tier = Tier::Native {
closing: Cell::new(false),
};
Some((b, title, r.scope))
}
_ => None,
}
});
let Some((build, title, scope)) = pending else {
return false;
};
if !with_tree(|t| t.adopt_window_root(root, raw, size)) {
return false;
}
with_tree(|t| t.set_native_window_title(root, &title));
crate::ambient::set_window_size_class(
root,
day_spec::SizeClass::from_size(size.width, size.height),
);
if let Some(build) = build {
scope.enter(|| {
crate::toolbar::with_window(root, || {
let piece = build();
let mut cx = BuildCx::new(root);
let _ = piece.build(&mut cx);
})
});
}
with_tree(|t| {
t.mark_layout_dirty();
t.layout_if_needed();
});
crate::toolbar::chrome_changed();
focus_revision().update(|n| *n += 1);
true
}
fn register(root: RNode, key: Option<&str>, kind: WindowKind, scope: Scope, tier: Tier) {
WINDOWS.with(|w| {
let mut windows = w.borrow_mut();
for r in windows.iter_mut() {
r.focused = false;
}
let (state, reported) = state_rail(root, scope);
windows.push(WindowRecord {
root,
key: key.map(str::to_string),
kind,
role: WindowRole::from(kind),
scope,
content_scope: None,
tier,
focused: true,
on_close: Rc::default(),
close_request: None,
state,
reported,
remember: None,
})
});
focus_revision().update(|n| *n += 1);
}
fn remember_frame(root: RNode, key: &str) {
WINDOWS.with(|w| {
if let Some(r) = w.borrow_mut().iter_mut().find(|r| r.root == root) {
r.remember = Some(key.to_owned());
}
});
let Some(saved) = crate::frames::load(key) else {
return;
};
let areas: Vec<Rect> = monitors().iter().map(|m| m.work_area).collect();
let origin = crate::frames::on_screen(saved.frame, &areas).then_some(saved.frame.origin);
let handle = WindowHandle { root };
handle.set_frame(origin, Some(saved.frame.size));
if saved.maximized {
handle.set_state(WindowState::Maximized);
}
}
fn save_frame_of_record(root: RNode, key: &str, state: WindowState) {
if matches!(state, WindowState::Minimized | WindowState::Fullscreen) {
return;
}
if let Some(frame) = remembered_frame(root) {
crate::frames::save(
key,
crate::frames::Remembered {
frame,
maximized: state == WindowState::Maximized,
},
);
}
}
pub(crate) fn save_remembered_frames() {
let keyed: Vec<(RNode, String)> = WINDOWS
.try_with(|w| {
w.borrow()
.iter()
.filter(|r| matches!(r.tier, Tier::Native { .. }))
.filter_map(|r| r.remember.clone().map(|k| (r.root, k)))
.collect()
})
.unwrap_or_default();
for (root, key) in keyed {
save_frame(root, &key);
}
}
fn save_frame(root: RNode, key: &str) {
let handle = WindowHandle { root };
let state = handle.state().get_untracked();
if matches!(state, WindowState::Minimized | WindowState::Fullscreen) {
return;
}
if let Some(frame) = remembered_frame(root) {
crate::frames::save(
key,
crate::frames::Remembered {
frame,
maximized: state == WindowState::Maximized,
},
);
}
}
fn remembered_frame(root: RNode) -> Option<Rect> {
let outer = with_tree(|t| t.native_window_frame(root))?;
let laid_out = with_tree(|t| t.node_frame(root)).map_or(outer.size, |f| f.size);
Some(Rect::new(
outer.origin.x,
outer.origin.y,
laid_out.width,
laid_out.height,
))
}
fn state_rail(root: RNode, scope: Scope) -> (Signal<WindowState>, Rc<Cell<WindowState>>) {
let reported: Rc<Cell<WindowState>> = Rc::default();
let state = scope.enter(|| {
let state = Signal::new(WindowState::Normal);
let rep = reported.clone();
day_reactive::bind_seeded(
WindowState::Normal,
move || state.get(),
move |s| {
if *s != rep.get() {
WindowHandle { root }.apply(WindowChange::State(*s));
}
},
);
state
});
(state, reported)
}
fn wire_window_events(root: RNode) {
with_tree(|t| {
t.on_event(
root,
Rc::new(move |ev| match ev {
Event::WindowResized(size) => {
let s = *size;
with_tree(|t| t.set_root_size(root, s));
crate::ambient::set_window_size_class(
root,
day_spec::SizeClass::from_size(s.width, s.height),
);
}
Event::WindowFocused(f) => {
let f = *f;
WINDOWS.with(|w| {
for r in w.borrow_mut().iter_mut() {
if r.root == root {
r.focused = f;
} else if f {
r.focused = false;
}
}
});
focus_revision().update(|n| *n += 1);
}
Event::WindowClosed => {
day_reactive::on_main(move || teardown(root));
}
Event::WindowStateChanged(s) => {
let s = *s;
let signal = WINDOWS.with(|w| {
w.borrow().iter().find(|r| r.root == root).map(|r| {
r.reported.set(s);
r.state
})
});
if let Some(signal) = signal {
signal.set_if_changed(s);
}
}
_ => {}
}),
);
});
}
pub fn primary_window_count() -> usize {
WINDOWS.with(|w| {
w.borrow()
.iter()
.filter(|r| r.role == WindowRole::Primary)
.count()
})
}
fn app_has_primary_window() -> bool {
primary_window_count() > 0
}
fn last_primary_close_quits() -> bool {
match crate::status::keep_running_policy() {
_ if crate::status::keeps_running() => false,
crate::status::KeepRunning::Never => true,
_ => !cfg!(target_os = "macos"),
}
}
pub fn quit() {
let remaining: Vec<RNode> = WINDOWS.with(|w| w.borrow().iter().map(|r| r.root).collect());
for root in remaining {
teardown_quietly(root);
}
crate::lifecycle::dispatch_lifecycle(day_spec::Lifecycle::WillTerminate);
with_tree(|t| t.quit_app());
}
fn teardown_quietly(root: RNode) {
QUITTING.with(|q| q.set(true));
teardown(root);
QUITTING.with(|q| q.set(false));
}
fn quit_after_last_primary() {
if !last_primary_close_quits() {
return;
}
let remaining: Vec<RNode> = WINDOWS.with(|w| w.borrow().iter().map(|r| r.root).collect());
for root in remaining {
teardown(root);
}
crate::lifecycle::dispatch_lifecycle(day_spec::Lifecycle::WillTerminate);
with_tree(|t| t.quit_app());
}
fn teardown(root: RNode) {
let Some(record) = WINDOWS.with(|w| {
let mut windows = w.borrow_mut();
windows
.iter()
.position(|r| r.root == root)
.map(|i| windows.remove(i))
}) else {
return;
};
focus_revision().update(|n| *n += 1);
if let (Some(key), Tier::Native { .. }) = (&record.remember, &record.tier) {
save_frame_of_record(root, key, record.state.get_untracked());
}
crate::frame::forget_window(root);
crate::toolbar::forget_window(root);
record.scope.dispose();
crate::ambient::forget_window(root);
match record.tier {
Tier::Native { .. } | Tier::PendingNative { .. } => {
while let Some(c) = with_tree(|t| t.first_child(root)) {
with_tree(|t| t.remove_subtree(c));
}
with_tree(|t| t.remove_window_root(root));
}
Tier::Cover { cover, .. } => {
with_tree(|t| {
t.drop_extra_layout_root(cover);
t.remove_subtree(cover);
});
}
}
with_tree(|t| {
t.mark_layout_dirty();
t.layout_if_needed();
});
for f in record.on_close.borrow().iter() {
f();
}
if record.role == WindowRole::Primary
&& !app_has_primary_window()
&& !QUITTING.with(|q| q.get())
{
quit_after_last_primary();
}
}
fn revive_if_dismissing(root: RNode) {
let cover = WINDOWS.with(|w| {
w.borrow()
.iter()
.find(|r| r.root == root)
.and_then(|r| match &r.tier {
Tier::Cover { cover, closing } if closing.get() => {
closing.set(false);
Some(*cover)
}
_ => None,
})
});
let Some(cover) = cover else { return };
with_tree(|t| {
t.patch(
cover,
Box::new(CoverPatch::Present {
background: None,
dismiss_disabled: false,
}),
false,
)
});
}
fn open_as_cover(
key: Option<&str>,
kind: WindowKind,
scope: Scope,
build: Box<dyn FnOnce() -> AnyPiece>,
) -> WindowHandle {
let size: Rc<RefCell<Option<Size>>> = Rc::default();
let primary = with_tree(|t| t.root_node());
let sheet = matches!(kind, WindowKind::Preferences | WindowKind::About)
&& crate::window_size_class_untracked(primary)
.is_some_and(|size| size.width != day_spec::WidthClass::Compact);
let composed_sheet =
sheet && crate::capability(day_spec::Cap::CoverSheet) == day_spec::Support::Unsupported;
let cover = {
let mut cx = BuildCx::new(primary);
cx.native(
kinds::COVER,
&CoverProps { sheet, half: false },
Rc::new(crate::CoverLayout { size: size.clone() }),
crate::Flex::default(),
Boundary::Yes,
)
};
with_tree(|t| t.add_extra_layout_root(cover, Size::new(0.0, 0.0)));
let closing: Rc<Cell<bool>> = Rc::default();
register(
cover,
key,
kind,
scope,
Tier::Cover {
cover,
closing: closing.clone(),
},
);
{
let (size, closing) = (size.clone(), closing.clone());
with_tree(|t| {
t.on_event(
cover,
Rc::new(move |ev| match ev {
Event::FrameChanged(sz) => {
if *size.borrow() != Some(*sz) {
*size.borrow_mut() = Some(*sz);
with_tree(|t| {
t.set_root_size(cover, *sz);
t.mark_layout_dirty();
t.layout_if_needed();
});
}
}
Event::NavBack { .. } => {
if !closing.get() {
closing.set(true);
with_tree(|t| t.patch(cover, Box::new(CoverPatch::Dismiss), false));
}
}
Event::CoverHidden if closing.get() => {
day_reactive::on_main(move || teardown(cover));
}
_ => {}
}),
);
});
}
scope.enter(|| {
crate::toolbar::with_window(cover, || {
let piece = build();
let mut cx = BuildCx::new(cover);
if composed_sheet {
let _ = crate::ComposedSheet { content: piece }.build(&mut cx);
} else {
let _ = piece.build(&mut cx);
}
})
});
with_tree(|t| {
t.patch(
cover,
Box::new(CoverPatch::Present {
background: composed_sheet.then_some(day_spec::Color::CLEAR),
dismiss_disabled: false,
}),
false,
);
t.mark_needs_measure(cover);
t.mark_layout_dirty();
t.layout_if_needed();
});
focus_revision().update(|n| *n += 1);
WindowHandle { root: cover }
}
type PrefsRegistration = (Rc<dyn Fn() -> AnyPiece>, WindowOptions);
pub const PREFERENCES_KEY: &str = "day.preferences";
pub fn register_preferences<P: Piece>(build: impl Fn() -> P + 'static) {
register_preferences_with(
WindowOptions {
title: day_localization::t(day_localization::key::day_settings),
version: None,
size: Size::new(520.0, 640.0),
min_size: None,
size_to_fit: true,
app_name: None,
locales: None,
title_fn: None,
tabbing: WindowTabbing::Automatic,
..WindowOptions::default()
},
build,
);
}
pub fn register_preferences_with<P: Piece>(
options: WindowOptions,
build: impl Fn() -> P + 'static,
) {
PREFS.with(|p| *p.borrow_mut() = Some((Rc::new(move || AnyPiece::new(build())), options)));
if PREFS_ACTION.with(|c| c.get()) == 0 {
PREFS_ACTION.with(|c| {
c.set(crate::menu::register_menu_action(Rc::new(|| {
open_preferences();
})))
});
}
crate::menu::reinstall_app_menu();
}
pub fn open_preferences() -> bool {
let Some((build, options)) = PREFS.with(|p| p.borrow().clone()) else {
log::warn!("open_preferences without register_preferences — ignored");
return false;
};
open_window(
options.key(PREFERENCES_KEY).kind(WindowKind::Preferences),
move || build(),
);
true
}
pub const ABOUT_KEY: &str = "day.about";
pub fn register_about(open: impl Fn() + 'static) {
ABOUT.with(|a| *a.borrow_mut() = Some(Rc::new(open)));
if ABOUT_ACTION.with(|c| c.get()) == 0 {
ABOUT_ACTION.with(|c| {
c.set(crate::menu::register_menu_action(Rc::new(|| {
open_about();
})))
});
}
crate::menu::reinstall_app_menu();
}
pub fn register_about_window<P: Piece>(options: WindowOptions, build: impl Fn() -> P + 'static) {
let build: Rc<dyn Fn() -> AnyPiece> = Rc::new(move || AnyPiece::new(build()));
register_about(move || {
let build = build.clone();
open_window(
options.clone().key(ABOUT_KEY).kind(WindowKind::About),
move || build(),
);
});
}
pub fn open_about() -> bool {
let Some(open) = ABOUT.with(|a| a.borrow().clone()) else {
log::warn!("open_about without register_about — ignored");
return false;
};
open();
true
}
pub fn about_action_id() -> u64 {
ABOUT_ACTION.with(|c| c.get())
}
pub fn register_check_for_updates(check: impl Fn() + 'static) {
UPDATES.with(|u| *u.borrow_mut() = Some(Rc::new(check)));
if UPDATES_ACTION.with(|c| c.get()) == 0 {
UPDATES_ACTION.with(|c| {
c.set(crate::menu::register_menu_action(Rc::new(|| {
check_for_updates();
})))
});
}
crate::menu::reinstall_app_menu();
}
pub fn check_for_updates() -> bool {
let Some(check) = UPDATES.with(|u| u.borrow().clone()) else {
return false;
};
check();
true
}
pub fn check_for_updates_action_id() -> u64 {
UPDATES_ACTION.with(|c| c.get())
}
pub fn register_new_window<P: Piece>(build: impl Fn() -> P + 'static) {
NEW_WINDOW.with(|p| *p.borrow_mut() = Some(Rc::new(move || AnyPiece::new(build()))));
if NEW_WINDOW_ACTION.with(|c| c.get()) == 0 {
NEW_WINDOW_ACTION.with(|c| {
c.set(crate::menu::register_menu_action(Rc::new(|| {
open_new_window();
})))
});
}
crate::menu::reinstall_app_menu();
}
pub fn open_new_window() -> Option<WindowHandle> {
let build = NEW_WINDOW.with(|p| p.borrow().clone())?;
let launch = LAUNCH_OPTIONS.with(|o| o.borrow().clone());
let size = with_tree(|t| {
let root = t.root_node();
t.node_frame(root).map(|f| f.size)
})
.unwrap_or(Size::new(800.0, 600.0));
Some(open_window(
WindowOptions {
title: launch.as_ref().map(|o| o.title.clone()).unwrap_or_default(),
size,
min_size: launch.as_ref().and_then(|o| o.min_size),
size_to_fit: false,
app_name: launch.as_ref().and_then(|o| o.app_name.clone()),
version: launch.as_ref().and_then(|o| o.version.clone()),
locales: None,
title_fn: None,
tabbing: WindowTabbing::Automatic,
..WindowOptions::default()
},
move || build(),
))
}
pub fn register_new_window_for<P: Piece>(group: &str, build: impl Fn() -> P + 'static) {
let build: Rc<dyn Fn() -> AnyPiece> = Rc::new(move || AnyPiece::new(build()));
GROUP_WINDOWS.with(|g| {
let mut groups = g.borrow_mut();
groups.retain(|(name, _)| name != group);
groups.push((group.to_string(), build));
});
}
pub fn open_new_window_for_group(group: &str) -> Option<WindowHandle> {
let action = TAB_NEW.with(|g| {
g.borrow()
.iter()
.find(|(name, _)| name == group)
.map(|(_, f)| f.clone())
});
if action.is_some() {
let group = group.to_string();
day_reactive::on_main(move || {
let action = TAB_NEW.with(|g| {
g.borrow()
.iter()
.find(|(name, _)| name == &group)
.map(|(_, f)| f.clone())
});
if let Some(f) = action {
f();
}
});
return None;
}
let Some(build) = GROUP_WINDOWS.with(|g| {
g.borrow()
.iter()
.find(|(name, _)| name == group)
.map(|(_, b)| b.clone())
}) else {
return open_new_window();
};
let launch = LAUNCH_OPTIONS.with(|o| o.borrow().clone());
let size = with_tree(|t| {
let root = t.root_node();
t.node_frame(root).map(|f| f.size)
})
.unwrap_or(Size::new(800.0, 600.0));
Some(open_window(
WindowOptions {
title: launch.as_ref().map(|o| o.title.clone()).unwrap_or_default(),
size,
min_size: launch.as_ref().and_then(|o| o.min_size),
app_name: launch.as_ref().and_then(|o| o.app_name.clone()),
version: launch.as_ref().and_then(|o| o.version.clone()),
tabbing: WindowTabbing::Group(group.to_string()),
..WindowOptions::default()
},
move || build(),
))
}
pub fn current_window() -> Option<WindowHandle> {
let building = crate::toolbar::window_being_built();
let known = WINDOWS.with(|w| w.borrow().iter().any(|r| r.root == building));
if known {
return Some(WindowHandle { root: building });
}
focused_window().or_else(initial_window)
}
pub fn window_title(f: impl Fn() -> String + 'static) {
let root = crate::toolbar::window_being_built();
day_reactive::bind(f, move |title| {
let title = crate::decorate_window_title(title);
with_tree(|t| t.set_native_window_title(root, &title));
});
}
pub fn preferences_action_id() -> u64 {
PREFS_ACTION.with(|c| c.get())
}
pub fn new_window_action_id() -> u64 {
NEW_WINDOW_ACTION.with(|c| c.get())
}
pub fn open_window_count() -> usize {
WINDOWS.with(|w| w.borrow().len())
}
pub fn window_kind_of(handle: &WindowHandle) -> Option<WindowKind> {
WINDOWS.with(|w| {
w.borrow()
.iter()
.find(|r| r.root == handle.root)
.map(|r| r.kind)
})
}
pub fn reset_windows() {
WINDOWS.with(|w| w.borrow_mut().clear());
INITIAL_WINDOW.with(|c| c.set(None));
crate::toolbar::reset_toolbars();
PREFS.with(|p| *p.borrow_mut() = None);
ABOUT.with(|a| *a.borrow_mut() = None);
UPDATES.with(|u| *u.borrow_mut() = None);
NEW_WINDOW.with(|p| *p.borrow_mut() = None);
GROUP_WINDOWS.with(|g| g.borrow_mut().clear());
}
pub fn adopt_initial_window(root: RNode, scope: Scope) {
register(
root,
None,
WindowKind::Normal,
scope,
Tier::Native {
closing: Cell::new(false),
},
);
wire_window_events(root);
INITIAL_WINDOW.with(|c| c.set(Some(root)));
let key = LAUNCH_OPTIONS.with(|o| o.borrow().as_ref().and_then(|o| o.remember_frame.clone()));
if let Some(key) = key {
remember_frame(root, &key);
}
}
pub fn monitors() -> Vec<day_spec::Monitor> {
let listed = with_tree(|t| t.monitors());
if !listed.is_empty() {
return listed;
}
let size = with_tree(|t| {
let root = t.root_node();
t.node_frame(root).map(|f| f.size)
})
.unwrap_or(Size::new(0.0, 0.0));
let frame = Rect::new(0.0, 0.0, size.width, size.height);
vec![day_spec::Monitor {
id: "0".into(),
name: String::new(),
frame,
work_area: frame,
scale: 1.0,
primary: true,
}]
}
pub fn initial_window() -> Option<WindowHandle> {
let root = INITIAL_WINDOW.with(|c| c.get())?;
WINDOWS.with(|w| {
w.borrow()
.iter()
.any(|r| r.root == root)
.then_some(WindowHandle { root })
})
}
pub fn window_node_id(handle: &WindowHandle) -> NodeId {
rnode_to_id(handle.root)
}
pub fn native_window_tabs() -> bool {
with_tree(|t| t.native_window_tabs())
}
pub fn group_windows(windows: &[WindowHandle]) {
with_tree(|t| t.group_windows(&windows.iter().map(|w| w.root).collect::<Vec<_>>()));
}
pub fn native_close_requested(id: NodeId) -> bool {
let handler = WINDOWS.with(|w| {
w.borrow()
.iter()
.find(|r| r.root == id_to_rnode(id))
.and_then(|r| r.close_request.clone())
});
if handler.is_some() {
day_reactive::on_main(move || {
let handler = WINDOWS.with(|w| {
w.borrow()
.iter()
.find(|r| r.root == id_to_rnode(id))
.and_then(|r| r.close_request.clone())
});
if let Some(f) = handler {
f();
}
});
false
} else {
true
}
}
pub fn register_tab_new(group: String, action: Rc<dyn Fn()>) {
TAB_NEW.with(|g| g.borrow_mut().push((group.clone(), action)));
Scope::current()
.on_cleanup(move || TAB_NEW.with(|g| g.borrow_mut().retain(|(name, _)| *name != group)));
}