pub const VERSION: &str = env!("CARGO_PKG_VERSION");
day_reactive::tls_root! {
layout: crate::layout::TlsGroupSlots,
ambient: crate::ambient::TlsGroupSlots,
anim: crate::anim::TlsGroupSlots,
conformance: crate::conformance::TlsGroupSlots,
frame: crate::frame::TlsGroupSlots,
image: crate::image::TlsGroupSlots,
list: crate::list::TlsGroupSlots,
lifecycle: crate::lifecycle::TlsGroupSlots,
menu: crate::menu::TlsGroupSlots,
nav: crate::nav::TlsGroupSlots,
present: crate::present::TlsGroupSlots,
root: crate::TlsGroupSlots,
shield: crate::shield::TlsGroupSlots,
shortcuts: crate::shortcuts::TlsGroupSlots,
status: crate::status::TlsGroupSlots,
toolbar: crate::toolbar::TlsGroupSlots,
documents: crate::documents::TlsGroupSlots,
urls: crate::urls::TlsGroupSlots,
tree: crate::tree::TlsGroupSlots,
windows: crate::windows::TlsGroupSlots,
}
pub use day_async::{
Blocking, BlockingError, blocking_inline, blocking_limit, blocking_threads, spawn_blocking,
};
#[cfg(target_arch = "wasm32")]
#[doc(hidden)]
pub use day_macros::retain_element_id as __retain_element_id;
pub use day_reactive::tls_group;
#[cfg(target_arch = "wasm32")]
#[doc(hidden)]
pub use inventory;
pub use linkme;
mod ambient;
mod anim;
mod build;
pub mod conformance;
pub mod element_id;
pub mod frame;
pub use element_id::{ElementId, IntoElementKey};
mod frames;
pub mod image;
mod layout;
pub mod lifecycle;
pub mod list;
mod memory_profile;
mod modal_sheet;
pub use modal_sheet::ComposedSheet;
pub mod menu;
mod nav;
mod present;
pub mod shield;
pub mod shortcuts;
pub mod status;
pub mod testing;
pub mod toolbar;
mod tree;
pub mod tree_driver;
pub mod tween;
pub mod windows;
pub use ambient::{
Ambient, app_environment, environment, focused_environment, override_size_class, reset_ambient,
restore_reported_size_class, safe_area, seed_safe_area, set_safe_area, set_size_class,
set_window_safe_area, set_window_size_class, size_class, window_safe_area, window_size_class,
window_size_class_untracked, with_environment,
};
pub use anim::{current_anim, with_animation};
pub use build::*;
pub use frame::{
FrameConsumer, add_frame_consumer, frame_consumer_count, install_frame_requester,
remove_frame_consumer,
};
pub use image::{
Bitmap, CanvasExports, RenderBitmap, RenderPdf, canvas_exports, decode as decode_image,
decode_async as decode_image_async, image_decode_support, image_encode_formats,
image_encode_support, render_bitmap, render_bitmap_async, render_pdf, render_pdf_async,
resolve_bitmap_render, resolve_image_decode, resolve_image_encode, resolve_pdf_render,
};
pub use layout::*;
pub use lifecycle::{dispatch_lifecycle, lifecycle_supported, on_lifecycle};
pub use list::{
BuiltRow, ListDeleteDriver, ListDriver, ListReorderDriver, ListSectionsDriver, ListSwipeDriver,
install_list, list_reload, list_scroll_to_end, list_scroll_to_row, list_set_editing,
list_set_selected, list_splice, list_try_delete, list_try_reorder, list_try_swipe,
set_scripted_reorder,
};
pub use menu::{
dispatch_menu_action, has_app_menu, register_menu_action, register_scoped_menu_action,
set_app_menu, set_dock_menu,
};
pub use nav::*;
pub use shortcuts::set_launcher_shortcuts;
pub use status::{
KeepRunning, remove_status_item, set_app_progress, set_dock_visible, set_keep_running,
set_status_item, status_items,
};
pub mod documents;
#[doc(hidden)]
pub use documents::{forward_launch_args, launch_link, take_launch_files};
mod urls;
pub use documents::{on_open_files, request_open_files};
pub use present::*;
pub use toolbar::{
Chrome, chrome_changed, current_chrome, current_page_column, current_page_gate,
current_page_window, dispatch_toolbar_value, patch_chrome, patch_toolbar,
register_contribution, register_contribution_gated, register_toolbar_value,
relabel_contribution, toggle_sidebar, unregister_contribution, update_contribution,
window_being_built, with_page, with_page_gated, with_page_in,
};
pub use tree_driver::{
TreeBuiltRow, TreeDriver, TreeMovesDriver, install_tree, tree_driver, tree_reload, tree_reveal,
tree_set_expanded, tree_set_selected, tree_try_move, tree_visible_rows,
};
pub use urls::{on_open_url, request_open_url};
pub use day_spec::resource::{
AssetDir, AssetName, FontFamily, ImageName, Resource, ResourceOpener, VectorName, resource,
set_resource_opener,
};
pub use tree::*;
pub use windows::{
WindowHandle, about_action_id, check_for_updates, check_for_updates_action_id, current_window,
finish_window_open, focused_scope, focused_window, initial_window, launch_options, monitors,
open_about, open_new_window, open_new_window_for_group, open_preferences, open_window, quit,
register_about, register_about_window, register_check_for_updates, register_new_window,
register_new_window_for, register_preferences, register_preferences_with, window_by_key,
window_title,
};
pub fn layout_direction() -> day_geometry::LayoutDirection {
DIRECTION.with(|d| {
if let Some(dir) = d.get() {
return dir;
}
let dir = std::env::var("DAY_LOCALE")
.map(|l| direction_of_locale(&l))
.unwrap_or_default();
d.set(Some(dir));
dir
})
}
pub fn set_layout_direction(dir: day_geometry::LayoutDirection) {
DIRECTION.with(|d| d.set(Some(dir)));
}
pub fn is_rtl() -> bool {
layout_direction() == day_geometry::LayoutDirection::Rtl
}
pub fn direction_of_locale(locale: &str) -> day_geometry::LayoutDirection {
let lang = locale
.split(['-', '_'])
.next()
.unwrap_or("")
.to_ascii_lowercase();
match lang.as_str() {
"ar" | "he" | "iw" | "fa" | "ur" | "ps" | "sd" | "ug" | "yi" | "dv" | "ku" => {
day_geometry::LayoutDirection::Rtl
}
_ => day_geometry::LayoutDirection::Ltr,
}
}
day_reactive::tls_slots! {
root;
static SEARCH_HOSTS: std::cell::RefCell<Vec<(RNode, RNode)>> =
const { std::cell::RefCell::new(Vec::new()) };
static DIRECTION: std::cell::Cell<Option<day_geometry::LayoutDirection>> =
const { std::cell::Cell::new(None) };
static UNDO_INVOKE: std::cell::RefCell<Option<UndoInvoke>> =
const { std::cell::RefCell::new(None) };
static MODIFIER_OVERRIDE: std::cell::Cell<Option<day_spec::Modifiers>> =
const { std::cell::Cell::new(None) };
static EDIT_INVOKE: std::cell::RefCell<Option<EditInvoke>> =
const { std::cell::RefCell::new(None) };
static UNDO_ACTION_IDS: std::cell::Cell<(u64, u64)> = const { std::cell::Cell::new((0, 0)) };
static EDIT_ACTION_IDS: std::cell::Cell<(u64, u64, u64, u64)> =
const { std::cell::Cell::new((0, 0, 0, 0)) };
static DARK_SIGNAL: std::cell::OnceCell<day_reactive::Signal<bool>> =
const { std::cell::OnceCell::new() };
static REDUCE_MOTION: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
static REDUCE_MOTION_SIGNAL: std::cell::OnceCell<day_reactive::Signal<bool>> =
const { std::cell::OnceCell::new() };
static FONT_FAMILIES: std::cell::OnceCell<std::rc::Rc<[day_spec::FontFamilyInfo]>> =
const { std::cell::OnceCell::new() };
static TEXT_METRICS: std::cell::RefCell<TextMetricsCache> =
std::cell::RefCell::new(TextMetricsCache::new());
}
use day_spec::{Platform, WindowOptions};
static CONTAINED_PANIC_OBSERVER: std::sync::OnceLock<fn()> = std::sync::OnceLock::new();
pub fn set_contained_panic_observer(f: fn()) {
let _ = CONTAINED_PANIC_OBSERVER.set(f);
}
pub(crate) fn notify_contained_panic() {
if let Some(f) = CONTAINED_PANIC_OBSERVER.get() {
f();
}
}
pub fn backend_name() -> Option<&'static str> {
BACKEND_NAME.get().copied()
}
static BACKEND_NAME: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
pub fn toolkit_key() -> Option<&'static str> {
TOOLKIT_KEY.get().copied()
}
static TOOLKIT_KEY: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
pub fn debug_title_tag() -> Option<String> {
if !cfg!(debug_assertions) {
return None;
}
let toolkit = toolkit_key()?;
if toolkit == "mock" {
return None;
}
let mut parts = Vec::new();
if let Ok(v) = std::env::var("DAY_APP_VERSION")
&& !v.is_empty()
{
parts.push(v);
}
parts.push(toolkit.to_string());
if let Ok(s) = std::env::var("DAY_SCRIPT")
&& !s.is_empty()
{
parts.push(s);
}
Some(format!("({})", parts.join("/")))
}
pub(crate) fn decorate_window_title(title: &str) -> String {
tag_title(title, debug_title_tag().as_deref())
}
fn tag_title(title: &str, tag: Option<&str>) -> String {
match tag {
Some(tag) if !title.is_empty() && !title.ends_with(tag) => format!("{title} {tag}"),
_ => title.to_string(),
}
}
#[cfg(test)]
mod title_tag_tests {
use super::tag_title;
#[test]
fn the_tag_is_appended_once_and_never_to_an_empty_title() {
assert_eq!(
tag_title("Day Sheets", Some("(0.1.0/appkit)")),
"Day Sheets (0.1.0/appkit)"
);
assert_eq!(tag_title("Day Sheets", None), "Day Sheets");
assert_eq!(tag_title("", Some("(0.1.0/appkit)")), "");
assert_eq!(
tag_title("Day Sheets (0.1.0/appkit)", Some("(0.1.0/appkit)")),
"Day Sheets (0.1.0/appkit)"
);
}
}
static LOG_SINK: std::sync::OnceLock<fn(log::Level, &str)> = std::sync::OnceLock::new();
pub fn set_log_sink(f: fn(log::Level, &str)) {
let _ = LOG_SINK.set(f);
}
const LEVEL_WIDTH: usize = 5;
#[cfg(not(target_arch = "wasm32"))]
fn level_style(level: log::Level) -> anstyle::Style {
let color = match level {
log::Level::Error => anstyle::AnsiColor::Red,
log::Level::Warn => anstyle::AnsiColor::Yellow,
log::Level::Info => anstyle::AnsiColor::Green,
log::Level::Debug => anstyle::AnsiColor::Blue,
log::Level::Trace => anstyle::AnsiColor::Cyan,
};
anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(color)))
}
#[cfg(not(target_arch = "wasm32"))]
fn write_line(level: log::Level, line: &str) {
use std::io::Write as _;
let style = level_style(level);
let (lvl, rest) = line.split_at_checked(LEVEL_WIDTH).unwrap_or((line, ""));
let _ = writeln!(anstream::stderr(), "{style}{lvl}{style:#}{rest}");
}
#[cfg(target_arch = "wasm32")]
fn write_line(_level: log::Level, line: &str) {
use std::io::Write as _;
let _ = writeln!(std::io::stderr(), "{line}");
}
struct DayLogger;
impl log::Log for DayLogger {
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
metadata.level() <= log::max_level()
}
fn log(&self, record: &log::Record<'_>) {
if !self.enabled(record.metadata()) {
return;
}
let sink = LOG_SINK.get();
let line = format!(
"{:<LEVEL_WIDTH$} {}: {}",
record.level(),
record.target(),
record.args()
);
match sink {
Some(f) => f(record.level(), &line),
None => write_line(record.level(), &line),
}
}
fn flush(&self) {}
}
pub fn init_logging() {
let level = std::env::var("DAY_LOG")
.ok()
.and_then(|v| v.parse::<log::LevelFilter>().ok())
.unwrap_or(if cfg!(debug_assertions) {
log::LevelFilter::Debug
} else {
log::LevelFilter::Info
});
set_log_level(level);
let _ = log::set_logger(&DayLogger);
}
pub fn set_log_level(level: log::LevelFilter) {
log::set_max_level(level);
}
pub(crate) fn contain_posted_panic(f: Box<dyn FnOnce() + Send>) {
if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
let msg = payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".to_string());
log::warn!(
"a posted main-thread task panicked and was contained — the app continues, but \
reactive/UI state may be inconsistent until the next interaction. Cause: {msg}"
);
day_reactive::recover_from_panic();
notify_contained_panic();
}
}
type UndoInvoke = std::rc::Rc<dyn Fn(bool)>;
pub fn install_undo_bridge(
can_undo: day_reactive::Signal<bool>,
can_redo: day_reactive::Signal<bool>,
undo_label: day_reactive::Signal<String>,
redo_label: day_reactive::Signal<String>,
on_invoke: impl Fn(bool) + 'static,
) {
UNDO_INVOKE.with(|u| *u.borrow_mut() = Some(std::rc::Rc::new(on_invoke)));
day_reactive::bind(
move || day_spec::UndoState {
can_undo: can_undo.get(),
can_redo: can_redo.get(),
undo_label: undo_label.get(),
redo_label: redo_label.get(),
},
|state: &day_spec::UndoState| {
let state = state.clone();
with_tree(|t| t.set_undo_state(&state));
},
);
}
pub fn edit_action_id(op: day_spec::EditOp) -> u64 {
use day_spec::EditOp as E;
let (c, y, p, a) = EDIT_ACTION_IDS.with(|s| s.get());
let have = match op {
E::Cut => c,
E::Copy => y,
E::Paste => p,
E::SelectAll => a,
};
if have != 0 {
return have;
}
let id = menu::register_menu_action(std::rc::Rc::new(move || dispatch_edit_invoke(op)));
EDIT_ACTION_IDS.with(|s| {
let (c0, y0, p0, a0) = s.get();
s.set(match op {
E::Cut => (id, y0, p0, a0),
E::Copy => (c0, id, p0, a0),
E::Paste => (c0, y0, id, a0),
E::SelectAll => (c0, y0, p0, id),
});
});
id
}
fn dispatch_undo_invoke(redo: bool) {
let _ = invoke_undo(redo);
}
pub fn invoke_undo(redo: bool) -> bool {
let f = UNDO_INVOKE.with(|u| u.borrow().clone());
match f {
Some(f) => {
day_reactive::batch(|| f(redo));
true
}
None => false,
}
}
type EditInvoke = std::rc::Rc<dyn Fn(day_spec::EditOp)>;
pub fn modifiers() -> day_spec::Modifiers {
if let Some(m) = MODIFIER_OVERRIDE.with(|o| o.get()) {
return m;
}
with_tree(|t| t.modifiers())
}
pub fn set_modifier_override(m: Option<day_spec::Modifiers>) {
MODIFIER_OVERRIDE.with(|o| o.set(m));
}
pub fn install_edit_bridge(
state: impl Fn() -> day_spec::EditState + 'static,
on_invoke: impl Fn(day_spec::EditOp) + 'static,
) {
EDIT_INVOKE.with(|u| *u.borrow_mut() = Some(std::rc::Rc::new(on_invoke)));
day_reactive::bind(state, |state: &day_spec::EditState| {
let state = *state;
with_tree(|t| t.set_edit_state(&state));
});
}
fn dispatch_edit_invoke(op: day_spec::EditOp) {
let _ = invoke_edit(op);
}
pub fn invoke_edit(op: day_spec::EditOp) -> bool {
let f = EDIT_INVOKE.with(|u| u.borrow().clone());
match f {
Some(f) => {
day_reactive::batch(|| f(op));
true
}
None => false,
}
}
pub fn undo_action_id(redo: bool) -> u64 {
let (u, r) = UNDO_ACTION_IDS.with(|c| c.get());
let have = if redo { r } else { u };
if have != 0 {
return have;
}
let id = menu::register_menu_action(std::rc::Rc::new(move || dispatch_undo_invoke(redo)));
UNDO_ACTION_IDS.with(|c| {
let (u0, r0) = c.get();
c.set(if redo { (u0, id) } else { (id, r0) });
});
id
}
fn handle_route_request(route: &str) {
nav::apply_route_request(route);
}
pub fn launch_with<P: Platform>(
backend: P,
mut options: WindowOptions,
root_piece: impl FnOnce() -> AnyPiece + 'static,
) {
init_logging();
let _ = BACKEND_NAME.set(P::TARGET);
let _ = TOOLKIT_KEY.set(P::TOOLKIT);
if options.app_name.is_none() && !options.title.is_empty() {
options.app_name = Some(options.title.clone());
}
options.title = decorate_window_title(&options.title);
if let Ok(v) = std::env::var("DAY_WINDOW")
&& let Some((w, h)) = v.split_once('x')
&& let (Ok(w), Ok(h)) = (w.trim().parse::<f64>(), h.trim().parse::<f64>())
&& w > 0.0
&& h > 0.0
{
options.size = day_spec::Size::new(w, h);
}
windows::set_launch_options(&options);
day_spec::ffi_guard::set_recovery(day_reactive::recover_from_panic);
day_reactive::install_main_poster(|f| {
P::post(Box::new(move || tree::run_posted_or_defer(f)));
});
day_reactive::install_delayed_poster(|ms, f| {
P::post_delayed(ms, Box::new(move || tree::run_posted_or_defer(f)));
});
day_reactive::install_scheduler(|| {
P::post(Box::new(|| {
tree::run_posted_or_defer(Box::new(day_reactive::flush_sync));
}))
});
day_reactive::install_spawner(|fut| {
let handle = present::task(fut);
Box::new(move || handle.abort())
});
frame::install_frame_requester(|root, cb| tree::with_tree(|t| t.request_frame(root, cb)));
#[cfg(not(target_arch = "wasm32"))]
if toolkit_key() != Some("mock") {
deliver_exit_at_process_exit();
}
lifecycle::note_launch(
options.app_name.clone().unwrap_or_default(),
options.version.clone(),
);
lifecycle::dispatch_lifecycle(day_spec::Lifecycle::WillLaunch);
P::run(
backend,
options,
Box::new(move |mut toolkit, root_handle, size| {
day_spec::Toolkit::set_event_sink(&mut toolkit, Box::new(tree::enqueue_native_event));
let tree = Tree::new(toolkit, root_handle, size);
let root = tree.root();
tree::install_tree(Box::new(tree));
note_motion_changed();
ambient::set_window_size_class(
root,
day_spec::SizeClass::from_size(size.width, size.height),
);
lifecycle::warn_unsupported_registrations();
with_tree(|t| {
let rn = root;
t.on_event(
rn,
std::rc::Rc::new(move |ev| match ev {
day_spec::Event::WindowResized(size) => {
let s = *size;
with_tree(|t| t.set_window_size(s));
ambient::set_window_size_class(
rn,
day_spec::SizeClass::from_size(s.width, s.height),
);
}
day_spec::Event::RouteRequested(route) => handle_route_request(route),
day_spec::Event::Undo { redo } => dispatch_undo_invoke(*redo),
day_spec::Event::Edit(op) => dispatch_edit_invoke(*op),
_ => {}
}),
);
});
let win_scope = day_reactive::Scope::root().enter(day_reactive::Scope::child);
windows::adopt_initial_window(root, win_scope);
let piece = root_piece();
win_scope.enter(|| {
let mut cx = BuildCx::new(root);
let _ = piece.build(&mut cx);
});
with_tree(|t| {
t.mark_layout_dirty();
t.layout_if_needed();
});
day_reactive::on_turn_end(|| with_tree(|t| t.layout_if_needed()));
menu::install_dock_menu();
status::boot_sync();
#[cfg(not(any(
target_arch = "wasm32",
target_os = "ios",
target_os = "android",
target_env = "ohos"
)))]
documents::request_open_files(documents::take_launch_files());
day_reactive::on_main(documents::drain_open_files);
if let Ok(url) = std::env::var("DAY_OPEN_URL") {
urls::request_open_url(&url);
}
day_reactive::on_main(urls::drain);
lifecycle::dispatch_lifecycle(day_spec::Lifecycle::DidLaunch);
day_reactive::on_main(move || {
if let Some(route) = nav::launch_deeplink()
&& !nav::navigate(&route)
{
log::warn!("launch deep link {route:?} did not match a route");
}
let _ = nav::take_requested_route();
let route = nav::current_route().unwrap_or_default();
with_tree(|t| t.set_route(&route));
let last = std::cell::RefCell::new(Some(route));
day_reactive::on_turn_end(move || {
let route = nav::current_route().unwrap_or_default();
if last.borrow().as_deref() != Some(route.as_str()) {
*last.borrow_mut() = Some(route.clone());
with_tree(|t| t.set_route(&route));
}
});
});
if let Ok(spec) = std::env::var("DAY_AUTODRIVE") {
day_reactive::on_main_delayed(800, move || autodrive(&spec));
}
}),
);
if P::RUN_ENDS_APP {
lifecycle::dispatch_lifecycle(day_spec::Lifecycle::DidExit);
}
}
#[cfg(not(target_arch = "wasm32"))]
fn deliver_exit_at_process_exit() {
unsafe extern "C" {
fn atexit(f: extern "C" fn()) -> i32;
}
extern "C" fn at_exit() {
lifecycle::dispatch_lifecycle(day_spec::Lifecycle::DidExit);
}
unsafe {
atexit(at_exit);
}
}
fn autodrive(spec: &str) {
use day_spec::{DragPhase, Event, Point};
for step in spec.split(';').filter(|s| !s.is_empty()) {
let parts: Vec<&str> = step.splitn(3, ':').collect();
if parts[0] == "shot" {
let path = parts[1..].join(":");
let png = with_tree(|t| t.snapshot());
match png {
Ok(bytes) => {
let _ = std::fs::write(&path, bytes);
}
Err(e) => log::warn!("day autodrive: snapshot failed: {e}"),
}
continue;
}
let node = with_tree(|t| t.find_by_id(parts[0]));
let Some(node) = node else {
log::warn!("day autodrive: id {:?} not found", parts[0]);
continue;
};
if parts.get(1) == Some(&"tap") {
if let Some(f) = with_tree(|t| t.node_frame(node)) {
let c = Point::new(f.size.width / 2.0, f.size.height / 2.0);
tree::enqueue_event(tree::rnode_to_id(node), Event::Tap(c));
}
continue;
}
if parts.get(1) == Some(&"drag") {
if let Some(f) = with_tree(|t| t.node_frame(node)) {
let c = Point::new(f.size.width / 2.0, f.size.height / 2.0);
let (dx, dy) = parts
.get(2)
.and_then(|s| s.split_once(':'))
.and_then(|(a, b)| Some((a.parse::<f64>().ok()?, b.parse::<f64>().ok()?)))
.unwrap_or((0.0, 0.0));
let end = Point::new(c.x + dx, c.y + dy);
let id = tree::rnode_to_id(node);
tree::enqueue_event(
id,
Event::Drag {
phase: DragPhase::Began,
location: c,
translation: Point::ZERO,
},
);
tree::enqueue_event(
id,
Event::Drag {
phase: DragPhase::Changed,
location: end,
translation: Point::new(dx, dy),
},
);
tree::enqueue_event(
id,
Event::Drag {
phase: DragPhase::Ended,
location: end,
translation: Point::new(dx, dy),
},
);
}
continue;
}
if let (Some("text"), Some(v)) = (parts.get(1).copied(), parts.get(2).copied()) {
synthesize_text(node, v.to_string());
continue;
}
let ev = match (parts.get(1).copied(), parts.get(2).copied()) {
(Some("press"), _) => Event::Pressed,
(Some("toggle"), Some(v)) => Event::ToggleChanged(v == "true"),
(Some("value"), Some(v)) => Event::ValueChanged(v.parse().unwrap_or(0.0)),
(Some("select"), Some(v)) => Event::SelectionChanged(v.parse().unwrap_or(-1)),
_ => continue,
};
tree::enqueue_event(tree::rnode_to_id(node), ev);
}
}
pub fn dark_mode() -> bool {
dark_signal().get()
}
pub fn font_families() -> std::rc::Rc<[day_spec::FontFamilyInfo]> {
if !tree::has_tree() {
return std::rc::Rc::from(Vec::new());
}
FONT_FAMILIES.with(|c| {
c.get_or_init(|| {
let mut list = tree::with_tree(|t| t.font_families());
for path in day_spec::fonts::bundled_fonts() {
let Some(names) = std::fs::read(&path)
.ok()
.and_then(|bytes| day_spec::fonts::parse_font_names(&bytes))
else {
continue;
};
if list
.iter()
.any(|f| f.family.eq_ignore_ascii_case(&names.family))
{
continue;
}
list.push(day_spec::FontFamilyInfo {
family: names.family,
faces: vec![day_spec::FontFace {
name: "Regular".to_string(),
weight: day_spec::FontWeight::Regular,
italic: false,
}],
});
}
list.sort_by_key(|f| f.family.to_lowercase());
list.dedup_by(|a, b| a.family.eq_ignore_ascii_case(&b.family));
list.into()
})
.clone()
})
}
const TEXT_METRICS_CAP: usize = 512;
struct TextMetricsCache {
key: String,
current: std::collections::HashMap<String, day_spec::TextMetrics>,
previous: std::collections::HashMap<String, day_spec::TextMetrics>,
hits: u64,
misses: u64,
}
impl TextMetricsCache {
fn new() -> Self {
TextMetricsCache {
key: String::new(),
current: std::collections::HashMap::new(),
previous: std::collections::HashMap::new(),
hits: 0,
misses: 0,
}
}
fn build_key(&mut self, text: &str, size: f64, font: &day_spec::CanvasFont) {
use std::fmt::Write;
let family = font.family_str();
self.key.clear();
let _ = write!(
self.key,
"{:x}:{}:{}:{}:{family}{text}",
size.to_bits(),
font.css_weight(),
u8::from(font.italic),
family.len(),
);
}
fn lookup(&mut self) -> Option<day_spec::TextMetrics> {
if let Some(m) = self.current.get(&self.key) {
self.hits += 1;
return Some(*m);
}
let m = *self.previous.get(&self.key)?;
self.hits += 1;
self.current.insert(self.key.clone(), m);
Some(m)
}
fn insert(&mut self, m: day_spec::TextMetrics) {
self.misses += 1;
if self.current.len() >= TEXT_METRICS_CAP {
self.previous = std::mem::take(&mut self.current);
}
self.current.insert(self.key.clone(), m);
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TextMetricsCacheStats {
pub hits: u64,
pub misses: u64,
pub entries: usize,
}
pub fn text_metrics_cache_stats() -> TextMetricsCacheStats {
TEXT_METRICS.with(|c| {
let c = c.borrow();
TextMetricsCacheStats {
hits: c.hits,
misses: c.misses,
entries: c.current.len() + c.previous.len(),
}
})
}
pub fn clear_text_metrics_cache() {
TEXT_METRICS.with(|c| {
let mut c = c.borrow_mut();
c.current.clear();
c.previous.clear();
});
}
pub fn measure_text(text: &str, size: f64, font: &day_spec::CanvasFont) -> day_spec::TextMetrics {
if let Some(m) = TEXT_METRICS.with(|c| {
let mut c = c.borrow_mut();
c.build_key(text, size, font);
c.lookup()
}) {
return m;
}
let Some(m) = tree::try_with_tree(|t| t.measure_text(text, size, font)).flatten() else {
return day_spec::TextMetrics::approximate(text, size);
};
TEXT_METRICS.with(|c| {
let mut c = c.borrow_mut();
c.build_key(text, size, font);
c.insert(m);
});
m
}
pub fn reduce_motion() -> bool {
reduce_motion_signal().get()
}
pub fn reduce_motion_now() -> bool {
testing::forced_reduce_motion() || REDUCE_MOTION.with(|c| c.get())
}
fn reduce_motion_signal() -> day_reactive::Signal<bool> {
REDUCE_MOTION_SIGNAL.with(|c| {
*c.get_or_init(|| {
let seed = reduce_motion_now();
day_reactive::Scope::detached().enter(|| day_reactive::Signal::new(seed))
})
})
}
pub fn note_motion_changed() {
if let Some(system) = tree::try_with_tree(|t| t.reduce_motion()) {
REDUCE_MOTION.with(|c| c.set(system));
let now = reduce_motion_now();
REDUCE_MOTION_SIGNAL.with(|c| {
if let Some(s) = c.get() {
s.set(now);
}
});
}
}
fn dark_signal() -> day_reactive::Signal<bool> {
DARK_SIGNAL.with(|c| {
*c.get_or_init(|| {
let seed = tree::with_tree(|t| t.dark_mode());
day_reactive::Scope::detached().enter(|| day_reactive::Signal::new(seed))
})
})
}
pub fn note_appearance_changed() {
if let Some(d) = tree::try_with_tree(|t| t.dark_mode()) {
dark_signal().set(d);
}
}
pub fn note_text_size_changed() {
let _ = tree::try_with_tree(|t| {
t.invalidate_all_measurements();
t.layout_if_needed();
});
}
pub fn register_search_host(host: RNode) {
let window = toolbar::current_page_window();
SEARCH_HOSTS.with(|h| {
let mut h = h.borrow_mut();
h.retain(|(_, node)| *node != host);
h.push((window, host));
});
}
pub fn unregister_search_host(host: RNode) {
SEARCH_HOSTS.with(|h| h.borrow_mut().retain(|(_, node)| *node != host));
}
pub fn inline_search_host() -> Option<RNode> {
let window = windows::focused_root();
SEARCH_HOSTS.with(|h| {
h.borrow()
.iter()
.rev()
.find(|(root, _)| *root == window)
.map(|(_, host)| *host)
})
}
pub fn focus_search() {
if toolbar::focus_toolbar_search() {
return;
}
if let Some(host) = inline_search_host() {
tree::with_tree(|t| t.patch(host, Box::new(day_spec::props::SearchPatch::Focus), false));
}
}
pub fn synthesize_text(node: RNode, text: String) {
tree::with_tree(|t| match t.node_kind(node) {
Some(day_spec::kinds::TEXT_FIELD) => t.patch(
node,
Box::new(day_spec::props::TextFieldPatch::Text {
text: text.clone(),
from_native: false,
}),
false,
),
Some(day_spec::kinds::TEXT_AREA) => t.patch(
node,
Box::new(day_spec::props::TextAreaPatch::SetText(text.clone())),
false,
),
_ => tree::enqueue_event(
tree::rnode_to_id(node),
day_spec::Event::custom(SYNTHESIZED_TEXT, text.clone()),
),
});
tree::enqueue_event(tree::rnode_to_id(node), day_spec::Event::TextChanged(text));
}
pub const SYNTHESIZED_TEXT: &str = "day:synthesized-text";
mod piece_ops;
pub use piece_ops::{
PieceOperationDone, PieceOperationFn, piece_operation, register_piece_operation,
};
pub fn set_appearance(dark: Option<bool>) {
tree::with_tree(|t| t.set_appearance(dark));
note_appearance_changed();
}
pub fn set_app_badge(badge: &day_spec::AppBadge) {
tree::with_tree(|t| t.set_app_badge(badge));
}
#[cfg(test)]
mod posted_panic_tests {
#[test]
fn posted_panic_is_contained() {
super::contain_posted_panic(Box::new(|| panic!("boom in a posted task")));
let s = day_reactive::Signal::new(1i32);
s.set(2);
assert_eq!(s.get_untracked(), 2);
}
}