use std::any::Any;
use std::collections::HashMap;
pub use day_geometry::*;
pub mod controls;
pub use controls::datetime::{DayDate, DayTime};
pub mod applications;
pub mod capture;
pub mod markdown;
pub mod resource;
pub mod styled;
pub mod styled_codec;
pub mod transfer;
pub use resource::{
AssetDir, AssetName, FontFamily, ImageName, Resource, ResourceOpener, VectorName,
resolve_asset_dir, resolve_image_file, resource, set_resource_opener,
};
pub use styled::{
ListStyle, ParagraphAlign, ParagraphRun, ParagraphStyle, RunStyle, StyledText, Underline,
coalesce_runs, paragraph_bounds, paragraphs_are_valid,
};
pub use styled_codec::{
DocStyle, html_to_styled, markdown_to_styled, rtf_to_styled, styled_to_html,
styled_to_markdown, styled_to_rtf,
};
pub use day_fonts as fonts;
pub fn route_of_url(url: &str) -> String {
match url.split_once("://") {
Some((_, rest)) => rest.to_string(),
None => url.to_string(),
}
}
pub type PieceKind = &'static str;
macro_rules! builtin_kinds {
($( $(#[$meta:meta])* $variant:ident = $konst:ident => $key:literal ),+ $(,)?) => {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Builtin {
$( $(#[$meta])* $variant, )+
}
impl Builtin {
pub const fn key(self) -> PieceKind {
match self { $( Builtin::$variant => $key, )+ }
}
pub fn from_key(kind: PieceKind) -> Option<Builtin> {
match kind {
$( $key => Some(Builtin::$variant), )+
_ => None,
}
}
pub const ALL: &'static [Builtin] = &[ $( Builtin::$variant, )+ ];
}
pub mod kinds {
$( $(#[$meta])* pub const $konst: super::PieceKind = super::Builtin::$variant.key(); )+
}
};
}
builtin_kinds! {
Container = CONTAINER => "day.container",
Label = LABEL => "day.label",
Button = BUTTON => "day.button",
Toggle = TOGGLE => "day.toggle",
Slider = SLIDER => "day.slider",
TextField = TEXT_FIELD => "day.text_field",
TextArea = TEXT_AREA => "day.text_area",
Picker = PICKER => "day.picker",
Divider = DIVIDER => "day.divider",
Scroll = SCROLL => "day.scroll",
Image = IMAGE => "day.image",
Progress = PROGRESS => "day.progress",
Canvas = CANVAS => "day.canvas",
Nav = NAV => "day.nav",
NavPage = NAV_PAGE => "day.nav_page",
NavMenu = NAV_MENU => "day.nav_menu",
List = LIST => "day.list",
ListCell = LIST_CELL => "day.list_cell",
Tree = TREE => "day.tree",
Cover = COVER => "day.cover",
Inspector = INSPECTOR => "day.inspector",
InspectorPane = INSPECTOR_PANE => "day.inspector_pane",
Split = SPLIT => "day.split",
SplitPane = SPLIT_PANE => "day.split_pane",
ListItem = LIST_ITEM => "day.list_item",
}
pub fn kind_has_baseline(kind: PieceKind) -> bool {
!matches!(
Builtin::from_key(kind),
Some(
Builtin::Container
| Builtin::Scroll
| Builtin::Divider
| Builtin::Image
| Builtin::Canvas
| Builtin::Slider
| Builtin::Toggle
| Builtin::Progress
| Builtin::Nav
| Builtin::NavPage
| Builtin::NavMenu
| Builtin::Split
| Builtin::SplitPane
| Builtin::List
| Builtin::ListCell
| Builtin::Cover
| Builtin::Inspector
| Builtin::InspectorPane
)
)
}
pub mod placeholder {
use super::PieceKind;
use std::collections::BTreeSet;
use std::sync::{Mutex, OnceLock};
fn table() -> &'static Mutex<BTreeSet<PieceKind>> {
static TABLE: OnceLock<Mutex<BTreeSet<PieceKind>>> = OnceLock::new();
TABLE.get_or_init(|| Mutex::new(BTreeSet::new()))
}
pub fn report(kind: PieceKind, toolkit: &str) {
let Ok(mut seen) = table().lock() else { return };
if seen.insert(kind) {
log::warn!(
"no renderer for piece kind \"{kind}\" on {toolkit} — is the piece's \
{toolkit} feature enabled? (rendering a placeholder)"
);
}
}
pub fn seen() -> Vec<PieceKind> {
table()
.lock()
.map(|s| s.iter().copied().collect())
.unwrap_or_default()
}
}
pub fn props_of<'a, T: 'static>(
kind: PieceKind,
toolkit: &str,
any: &'a dyn std::any::Any,
) -> Option<&'a T> {
let p = any.downcast_ref::<T>();
if p.is_none() {
placeholder::report(kind, toolkit);
}
p
}
pub mod sidetable {
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
type Sweeper = Rc<dyn Fn(usize)>;
thread_local! {
static SWEEPERS: RefCell<Vec<Sweeper>> = const { RefCell::new(Vec::new()) };
}
pub struct SideTable<V: 'static> {
map: Rc<RefCell<HashMap<usize, V>>>,
teardown: Option<Rc<dyn Fn(V)>>,
}
impl<V> Default for SideTable<V> {
fn default() -> Self {
Self::new()
}
}
impl<V> SideTable<V> {
pub fn new() -> Self {
Self::build(None)
}
pub fn with_teardown(teardown: impl Fn(V) + 'static) -> Self {
Self::build(Some(Rc::new(teardown)))
}
fn build(teardown: Option<Rc<dyn Fn(V)>>) -> Self {
let map: Rc<RefCell<HashMap<usize, V>>> = Rc::default();
let (m, t) = (map.clone(), teardown.clone());
SWEEPERS.with(|s| {
s.borrow_mut().push(Rc::new(move |key| {
let v = m.borrow_mut().remove(&key);
if let (Some(v), Some(t)) = (v, &t) {
t(v);
}
}));
});
SideTable { map, teardown }
}
pub fn insert(&self, key: usize, value: V) -> Option<V> {
self.map.borrow_mut().insert(key, value)
}
pub fn remove(&self, key: usize) -> Option<V> {
let v = self.map.borrow_mut().remove(&key)?;
match &self.teardown {
Some(t) => {
t(v);
None
}
None => Some(v),
}
}
pub fn take(&self, key: usize) -> Option<V> {
self.map.borrow_mut().remove(&key)
}
pub fn contains(&self, key: usize) -> bool {
self.map.borrow().contains_key(&key)
}
pub fn get(&self, key: usize) -> Option<V>
where
V: Clone,
{
self.map.borrow().get(&key).cloned()
}
pub fn with<R>(&self, key: usize, f: impl FnOnce(&mut V) -> R) -> Option<R> {
self.map.borrow_mut().get_mut(&key).map(f)
}
pub fn for_each(&self, mut f: impl FnMut(usize, &V)) {
for (k, v) in self.map.borrow().iter() {
f(*k, v);
}
}
}
pub fn sweep(key: usize) {
let sweepers: Vec<Sweeper> = SWEEPERS.with(|s| s.borrow().clone());
for f in sweepers {
f(key);
}
}
}
pub mod ffi_guard {
use std::sync::OnceLock;
static RECOVERY: OnceLock<fn()> = OnceLock::new();
pub fn set_recovery(f: fn()) {
let _ = RECOVERY.set(f);
}
pub fn contain<R>(default: R, f: impl FnOnce() -> R) -> R {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(r) => r,
Err(payload) => {
let msg = payload
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "<non-string panic>".into());
log::error!("panic contained at an FFI boundary: {msg}");
if let Some(recover) = RECOVERY.get() {
recover();
}
default
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct NodeId(pub u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SurfaceRole {
SectionCard,
Window,
Scrim,
}
pub const NAV_SIDEBAR_WIDTH: f64 = 240.0;
pub const NAV_SIDEBAR_MIN_W: f64 = 160.0;
pub const NAV_SIDEBAR_MAX_W: f64 = 400.0;
pub const NAV_LIST_WIDTH: f64 = 300.0;
pub const NAV_LIST_MIN_W: f64 = 220.0;
pub const NAV_LIST_MAX_W: f64 = 560.0;
pub const WINDOW_NODE: NodeId = NodeId(u64::MAX);
pub mod keys {
use super::NodeId;
use std::cell::RefCell;
use std::collections::HashSet;
thread_local! {
static HANDLED: RefCell<HashSet<NodeId>> = RefCell::new(HashSet::new());
}
pub fn mark(node: NodeId) {
HANDLED.with(|h| h.borrow_mut().insert(node));
}
pub fn handled(node: NodeId) -> bool {
HANDLED.with(|h| h.borrow().contains(&node))
}
}
pub type RawHandle = *mut std::ffi::c_void;
pub mod bridge {
#[repr(i32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BridgeKind {
Pressed = 0,
TextChanged = 1,
ToggleChanged = 2,
ValueChanged = 3,
SelectionChanged = 4,
NavBack = 5,
FrameChanged = 6,
Deeplink = 7,
PresentButton = 8,
PresentText = 9,
PresentDismissed = 10,
Gesture = 11,
Custom = 12,
MenuAction = 13,
Lifecycle = 14,
PresentFile = 15,
FocusChanged = 16,
Submitted = 17,
WindowResized = 18,
SafeArea = 19,
WindowClosed = 20,
WindowFocused = 21,
ValueCommitted = 22,
SearchChanged = 23,
NavPresentation = 24,
AppearanceChanged = 25,
CoverHidden = 26,
LinkActivated = 27,
UndoInvoked = 28,
ToolbarChanged = 30,
ListActivated = 31,
DocumentOpened = 32,
ReduceMotionChanged = 33,
Key = 29,
WindowStateChanged = 34,
RefreshRequested = 35,
}
impl BridgeKind {
pub const ALL: [BridgeKind; 36] = [
BridgeKind::Pressed,
BridgeKind::TextChanged,
BridgeKind::ToggleChanged,
BridgeKind::ValueChanged,
BridgeKind::SelectionChanged,
BridgeKind::NavBack,
BridgeKind::FrameChanged,
BridgeKind::Deeplink,
BridgeKind::PresentButton,
BridgeKind::PresentText,
BridgeKind::PresentDismissed,
BridgeKind::Gesture,
BridgeKind::Custom,
BridgeKind::MenuAction,
BridgeKind::Lifecycle,
BridgeKind::PresentFile,
BridgeKind::FocusChanged,
BridgeKind::Submitted,
BridgeKind::WindowResized,
BridgeKind::SafeArea,
BridgeKind::WindowClosed,
BridgeKind::WindowFocused,
BridgeKind::ValueCommitted,
BridgeKind::SearchChanged,
BridgeKind::NavPresentation,
BridgeKind::AppearanceChanged,
BridgeKind::CoverHidden,
BridgeKind::LinkActivated,
BridgeKind::UndoInvoked,
BridgeKind::Key,
BridgeKind::ToolbarChanged,
BridgeKind::ListActivated,
BridgeKind::DocumentOpened,
BridgeKind::ReduceMotionChanged,
BridgeKind::WindowStateChanged,
BridgeKind::RefreshRequested,
];
}
#[cfg(test)]
mod tests {
use super::BridgeKind;
#[test]
fn discriminants_are_unique() {
let mut seen = std::collections::BTreeSet::new();
for k in BridgeKind::ALL {
assert!(
seen.insert(k as i32),
"duplicate bridge kind number {} ({k:?})",
k as i32
);
}
assert_eq!(seen.len(), BridgeKind::ALL.len());
}
#[test]
fn table_covers_every_wire_number() {
let mut nums: Vec<i32> = BridgeKind::ALL.iter().map(|k| *k as i32).collect();
nums.sort_unstable();
let dense: Vec<i32> = (0..BridgeKind::ALL.len() as i32).collect();
assert_eq!(
nums, dense,
"bridge kinds must be dense from 0 — a missing entry means a variant no test sees"
);
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
Pressed,
TextChanged(String),
Submitted,
ToggleChanged(bool),
ValueChanged(f64),
ValueCommitted(f64),
SelectionChanged(i64),
ListActivated(usize),
Undo {
redo: bool,
},
Edit(EditOp),
SelectionSet(Vec<i64>),
FocusChanged(bool),
Tap(Point),
LinkActivated(String),
LongPress(Point),
ContextMenu {
local: Point,
window: Point,
},
Drag {
phase: DragPhase,
location: Point,
translation: Point,
},
Pinch {
phase: DragPhase,
scale: f64,
location: Point,
},
Pan {
phase: DragPhase,
delta: Point,
location: Point,
},
Hover {
phase: DragPhase,
location: Point,
},
ScrollChanged(Point),
FrameChanged(Size),
NavBack {
already_popped: bool,
},
NavPresentationChanged(props::NavPresentation),
Key(KeyEvent),
Pointer(PointerEvent),
WindowResized(Size),
RouteRequested(String),
PresentResult {
req: u64,
result: present::PresentResult,
},
ImageDecoded {
req: u64,
result: Result<BitmapInfo, ImageError>,
},
ImageEncoded {
req: u64,
result: Result<Vec<u8>, ImageError>,
},
BitmapRendered {
req: u64,
result: Result<BitmapInfo, ExportError>,
},
PdfRendered {
req: u64,
result: Result<Vec<u8>, ExportError>,
},
Custom {
tag: &'static str,
num: f64,
text: String,
},
MenuAction(u64),
ToolbarChanged {
action: u64,
value: ToolbarValue,
},
SearchChanged(String),
SearchScopeChanged(usize),
SearchSuggestionChosen(usize),
SearchSubmitted(String),
Lifecycle(Lifecycle),
WindowClosed,
WindowFocused(bool),
WindowStateChanged(WindowState),
ListReorder {
from: usize,
to: usize,
},
NavRegroup {
from: usize,
section: usize,
before: Option<usize>,
},
ListDelete(usize),
RefreshRequested,
ListSwipe {
index: usize,
edge: SwipeEdge,
action: usize,
},
CoverHidden,
InspectorChanged(bool),
TreeExpanded {
token: u64,
expanded: bool,
},
TreeMove {
token: u64,
parent: Option<u64>,
index: Option<usize>,
},
TreeSelection(Vec<u64>),
}
impl Event {
pub fn custom(tag: &'static str, text: impl Into<String>) -> Event {
Event::Custom {
tag,
num: 0.0,
text: text.into(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Lifecycle {
WillLaunch,
DidLaunch,
DidBecomeActive,
WillResignActive,
WillEnterForeground,
DidEnterBackground,
DidReceiveMemoryWarning,
WillTerminate,
DidExit,
}
impl Lifecycle {
pub const ALL: [Lifecycle; 9] = [
Lifecycle::WillLaunch,
Lifecycle::DidLaunch,
Lifecycle::DidBecomeActive,
Lifecycle::WillResignActive,
Lifecycle::WillEnterForeground,
Lifecycle::DidEnterBackground,
Lifecycle::DidReceiveMemoryWarning,
Lifecycle::WillTerminate,
Lifecycle::DidExit,
];
pub const fn is_universal(self) -> bool {
matches!(
self,
Lifecycle::WillLaunch
| Lifecycle::DidLaunch
| Lifecycle::DidBecomeActive
| Lifecycle::WillResignActive
| Lifecycle::WillTerminate
| Lifecycle::DidExit
)
}
pub const fn name(self) -> &'static str {
match self {
Lifecycle::WillLaunch => "WillLaunch",
Lifecycle::DidLaunch => "DidLaunch",
Lifecycle::DidBecomeActive => "DidBecomeActive",
Lifecycle::WillResignActive => "WillResignActive",
Lifecycle::WillEnterForeground => "WillEnterForeground",
Lifecycle::DidEnterBackground => "DidEnterBackground",
Lifecycle::DidReceiveMemoryWarning => "DidReceiveMemoryWarning",
Lifecycle::WillTerminate => "WillTerminate",
Lifecycle::DidExit => "DidExit",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DragPhase {
Began,
Changed,
Ended,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum GestureKind {
Tap,
LongPress,
Drag,
Hover,
Pinch,
Pan,
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Shortcut {
pub key: String,
pub primary: bool,
pub shift: bool,
pub alt: bool,
pub control: bool,
}
impl Shortcut {
pub fn primary(key: impl Into<String>) -> Shortcut {
Shortcut {
key: key.into(),
primary: true,
..Default::default()
}
}
pub fn plain(key: impl Into<String>) -> Shortcut {
Shortcut {
key: key.into(),
..Default::default()
}
}
pub fn shift(mut self) -> Shortcut {
self.shift = true;
self
}
pub fn alt(mut self) -> Shortcut {
self.alt = true;
self
}
pub fn control(mut self) -> Shortcut {
self.control = true;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MenuRole {
Cut,
Copy,
Paste,
SelectAll,
Undo,
Redo,
Delete,
About,
Quit,
Preferences,
Minimize,
CloseWindow,
Fullscreen,
NewWindow,
CheckForUpdates,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Modifiers {
pub shift: bool,
pub primary: bool,
pub alt: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EditOp {
Cut,
Copy,
Paste,
SelectAll,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct EditState {
pub can_cut: bool,
pub can_copy: bool,
pub can_paste: bool,
pub can_select_all: bool,
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct UndoState {
pub can_undo: bool,
pub can_redo: bool,
pub undo_label: String,
pub redo_label: String,
}
#[derive(Clone, Debug, PartialEq)]
pub enum MenuItem {
Action {
id: Option<String>,
action: u64,
label: String,
shortcut: Option<Shortcut>,
enabled: bool,
checked: Option<bool>,
role: Option<MenuRole>,
icon: Option<Icon>,
},
Submenu {
label: String,
items: Vec<MenuItem>,
role: Option<MenuBarRole>,
},
Separator,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum MenuBarRole {
File,
Edit,
View,
Settings,
Window,
Help,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Symbol {
Add,
Remove,
Delete,
Edit,
New,
Open,
Save,
Print,
Refresh,
Search,
Share,
Settings,
Info,
Star,
Bookmark,
Back,
Forward,
Up,
Down,
Home,
Sidebar,
Filter,
Sort,
More,
Play,
Pause,
Stop,
Camera,
Code,
Light,
Dark,
Auto,
ZoomIn,
ZoomOut,
ZoomReset,
Undo,
Redo,
Copy,
Cut,
Paste,
Mail,
Folder,
Document,
Check,
Close,
Warning,
Rectangle,
Oval,
Circle,
CircleFilled,
Line,
Group,
Ungroup,
Text,
}
pub fn sf_symbol_name(s: Symbol) -> &'static str {
match s {
Symbol::Add => "plus",
Symbol::Remove => "minus",
Symbol::Delete => "trash",
Symbol::Edit => "pencil",
Symbol::New => "square.and.pencil",
Symbol::Open => "folder",
Symbol::Save => "square.and.arrow.down",
Symbol::Print => "printer",
Symbol::Refresh => "arrow.clockwise",
Symbol::Search => "magnifyingglass",
Symbol::Share => "square.and.arrow.up",
Symbol::Settings => "gearshape",
Symbol::Info => "info.circle",
Symbol::Star => "star",
Symbol::Bookmark => "bookmark",
Symbol::Back => "chevron.backward",
Symbol::Forward => "chevron.forward",
Symbol::Up => "chevron.up",
Symbol::Down => "chevron.down",
Symbol::Home => "house",
Symbol::Sidebar => "sidebar.leading",
Symbol::Filter => "line.3.horizontal.decrease",
Symbol::Sort => "arrow.up.arrow.down",
Symbol::More => "ellipsis",
Symbol::Play => "play.fill",
Symbol::Pause => "pause.fill",
Symbol::Stop => "stop.fill",
Symbol::Camera => "camera",
Symbol::Code => "chevron.left.forwardslash.chevron.right",
Symbol::Light => "sun.max",
Symbol::Dark => "moon",
Symbol::Auto => "circle.lefthalf.filled",
Symbol::ZoomIn => "plus.magnifyingglass",
Symbol::ZoomReset => "1.magnifyingglass",
Symbol::ZoomOut => "minus.magnifyingglass",
Symbol::Undo => "arrow.uturn.backward",
Symbol::Redo => "arrow.uturn.forward",
Symbol::Copy => "doc.on.doc",
Symbol::Cut => "scissors",
Symbol::Paste => "doc.on.clipboard",
Symbol::Mail => "envelope",
Symbol::Folder => "folder",
Symbol::Document => "doc",
Symbol::Check => "checkmark",
Symbol::Close => "xmark",
Symbol::Warning => "exclamationmark.triangle",
Symbol::Rectangle => "rectangle",
Symbol::Oval => "oval",
Symbol::Circle => "circle",
Symbol::CircleFilled => "circle.fill",
Symbol::Line => "line.diagonal",
Symbol::Group => "square.on.square",
Symbol::Ungroup => "square.on.square.dashed",
Symbol::Text => "textformat",
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Icon {
Symbol(Symbol),
Image(String),
}
#[derive(Clone, Debug, PartialEq)]
pub enum ToolbarItemKind {
Button,
Toggle { on: bool },
Menu { items: Vec<MenuItem> },
Search {
text: String,
placeholder: String,
suggestions: Vec<String>,
},
Segmented {
segments: Vec<ToolbarSegment>,
selected: usize,
},
Label,
Separator,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ToolbarSegment {
pub title: String,
pub icon: Option<Icon>,
}
pub const SIDEBAR_TOGGLE_ID: &str = "day::sidebar_toggle";
pub const SEARCH_ITEM_ID: &str = "day::search";
#[derive(Clone, Debug, PartialEq)]
pub struct ToolbarItem {
pub id: String,
pub kind: ToolbarItemKind,
pub label: String,
pub tooltip: Option<String>,
pub icon: Option<Icon>,
pub enabled: bool,
pub action: u64,
pub placement: ToolbarPlacement,
pub label_style: LabelStyle,
pub prominent: bool,
pub column: ToolbarColumn,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ToolbarColumn {
#[default]
Window,
Sidebar,
List,
Detail,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ToolbarPlacement {
#[default]
Automatic,
Navigation,
Principal,
Primary,
Secondary,
Bottom,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LabelStyle {
#[default]
Automatic,
IconOnly,
TitleOnly,
TitleAndIcon,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ToolbarValue {
Text(String),
Submit(String),
On(bool),
Selected(usize),
}
#[derive(Clone, Debug, PartialEq)]
pub enum ToolbarPatch {
Focus { item: String },
Text { item: String, text: String },
On { item: String, on: bool },
Selected { item: String, index: usize },
Enabled { item: String, on: bool },
Suggestions { item: String, list: Vec<String> },
Label {
item: String,
label: String,
tooltip: Option<String>,
},
Placeholder { item: String, text: String },
}
impl ToolbarPatch {
pub fn item(&self) -> &str {
match self {
ToolbarPatch::Focus { item }
| ToolbarPatch::Text { item, .. }
| ToolbarPatch::On { item, .. }
| ToolbarPatch::Selected { item, .. }
| ToolbarPatch::Enabled { item, .. }
| ToolbarPatch::Suggestions { item, .. }
| ToolbarPatch::Label { item, .. }
| ToolbarPatch::Placeholder { item, .. } => item,
}
}
pub fn apply_to(&self, items: &mut [ToolbarItem]) -> bool {
let Some(it) = items.iter_mut().find(|i| i.id == self.item()) else {
return false;
};
match (self, &mut it.kind) {
(ToolbarPatch::Focus { .. }, ToolbarItemKind::Search { .. }) => (),
(ToolbarPatch::Text { text, .. }, ToolbarItemKind::Search { text: t, .. }) => {
*t = text.clone()
}
(
ToolbarPatch::Suggestions { list, .. },
ToolbarItemKind::Search { suggestions, .. },
) => *suggestions = list.clone(),
(ToolbarPatch::On { on, .. }, ToolbarItemKind::Toggle { on: o }) => *o = *on,
(
ToolbarPatch::Selected { index, .. },
ToolbarItemKind::Segmented { segments, selected },
) if *index < segments.len() => *selected = *index,
(ToolbarPatch::Enabled { on, .. }, _) => it.enabled = *on,
(ToolbarPatch::Label { label, tooltip, .. }, _) => {
it.label = label.clone();
it.tooltip = tooltip.clone();
}
(
ToolbarPatch::Placeholder { text, .. },
ToolbarItemKind::Search { placeholder, .. },
) => *placeholder = text.clone(),
_ => return false,
}
true
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ToolbarOp {
Remove { id: String },
Insert { index: usize, item: ToolbarItem },
}
impl ToolbarOp {
pub fn apply_to(&self, items: &mut Vec<ToolbarItem>) {
match self {
ToolbarOp::Remove { id } => items.retain(|i| i.id != *id),
ToolbarOp::Insert { index, item } => {
items.insert((*index).min(items.len()), item.clone())
}
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ToolbarMirror {
items: Vec<ToolbarItem>,
}
impl ToolbarMirror {
pub fn items(&self) -> &[ToolbarItem] {
&self.items
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn get(&self, id: &str) -> Option<&ToolbarItem> {
self.items.iter().find(|i| i.id == id)
}
pub fn by_action(&self, action: u64) -> Option<&ToolbarItem> {
self.items
.iter()
.find(|i| i.action == action && action != 0)
}
pub fn remove(&mut self, id: &str) -> Option<ToolbarItem> {
let at = self.items.iter().position(|i| i.id == id)?;
Some(self.items.remove(at))
}
pub fn insert(&mut self, index: usize, item: ToolbarItem) {
let at = index.min(self.items.len());
self.items.insert(at, item);
}
pub fn apply(&mut self, op: &ToolbarOp) {
op.apply_to(&mut self.items);
}
pub fn patch(&mut self, patch: &ToolbarPatch) -> bool {
patch.apply_to(&mut self.items)
}
pub fn prev_where(
&self,
index: usize,
same_group: impl Fn(&ToolbarItem) -> bool,
) -> Option<&ToolbarItem> {
self.items[..index.min(self.items.len())]
.iter()
.rev()
.find(|i| same_group(i))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct KeyEvent {
pub key: String,
pub modifiers: u8,
}
impl KeyEvent {
pub const SHIFT: u8 = 1;
pub const PRIMARY: u8 = 2;
pub const ALT: u8 = 4;
pub const DIGITS: [&'static str; 10] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
pub fn digit_name(c: char) -> Option<&'static str> {
c.to_digit(10).map(|d| Self::DIGITS[d as usize])
}
pub fn digit(&self) -> Option<u8> {
match self.key.as_bytes() {
[d @ b'0'..=b'9'] => Some(d - b'0'),
_ => None,
}
}
pub fn shift(&self) -> bool {
self.modifiers & Self::SHIFT != 0
}
pub fn primary(&self) -> bool {
self.modifiers & Self::PRIMARY != 0
}
pub fn alt(&self) -> bool {
self.modifiers & Self::ALT != 0
}
}
#[cfg(test)]
mod key_event_tests {
use super::KeyEvent;
#[test]
fn digit_names_read_back_as_values() {
for (value, c) in ('0'..='9').enumerate() {
let name = KeyEvent::digit_name(c).expect("an ASCII digit names itself");
let key = KeyEvent {
key: name.to_string(),
modifiers: 0,
};
assert_eq!(key.digit(), Some(value as u8));
}
assert_eq!(KeyEvent::digit_name('a'), None);
assert_eq!(KeyEvent::digit_name('٣'), None);
let arrow = KeyEvent {
key: "ArrowUp".to_string(),
modifiers: 0,
};
assert_eq!(arrow.digit(), None);
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PointerEvent {
pub position: Point,
pub down: bool,
}
pub type EventSink = Box<dyn Fn(NodeId, Event)>;
#[derive(Clone)]
pub struct ListSource {
pub first_visible: Option<std::rc::Rc<dyn Fn(usize)>>,
pub len: std::rc::Rc<dyn Fn() -> usize>,
pub is_header: Option<std::rc::Rc<dyn Fn(usize) -> bool>>,
pub token_at: std::rc::Rc<dyn Fn(usize) -> u64>,
pub bind_row: std::rc::Rc<dyn Fn(usize, RawHandle)>,
pub bind_pinned: std::rc::Rc<dyn Fn(usize, RawHandle)>,
pub recycle: std::rc::Rc<dyn Fn(RawHandle)>,
pub release: std::rc::Rc<dyn Fn(RawHandle)>,
pub layout_cell: std::rc::Rc<dyn Fn(RawHandle, f64)>,
pub cell_height: std::rc::Rc<dyn Fn(RawHandle) -> f64>,
pub reorder: Option<ListReorder>,
pub delete: Option<ListDelete>,
pub swipe: Option<ListSwipe>,
}
#[derive(Clone)]
pub struct ListReorder {
pub can_move: std::rc::Rc<dyn Fn(usize, usize) -> i64>,
pub move_row: std::rc::Rc<dyn Fn(usize, usize)>,
}
#[derive(Clone)]
pub struct ListDelete {
pub can_delete: std::rc::Rc<dyn Fn(usize) -> bool>,
pub delete_row: std::rc::Rc<dyn Fn(usize)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SwipeEdge {
Leading,
Trailing,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ListSwipeAction {
pub label: String,
pub destructive: bool,
pub tint: Option<Color>,
pub symbol: Option<Symbol>,
}
#[derive(Clone)]
pub struct ListSwipe {
pub actions_at: std::rc::Rc<dyn Fn(usize, SwipeEdge) -> Vec<ListSwipeAction>>,
pub perform: std::rc::Rc<dyn Fn(usize, SwipeEdge, usize)>,
}
#[derive(Clone)]
pub struct TreeSource {
pub children_len: std::rc::Rc<dyn Fn(Option<u64>) -> usize>,
pub child_token: std::rc::Rc<dyn Fn(Option<u64>, usize) -> u64>,
pub expandable: std::rc::Rc<dyn Fn(u64) -> bool>,
pub section_header: std::rc::Rc<dyn Fn(u64) -> bool>,
pub expanded: std::rc::Rc<dyn Fn(u64) -> bool>,
pub bind_row: std::rc::Rc<dyn Fn(u64, RawHandle)>,
pub recycle: std::rc::Rc<dyn Fn(RawHandle)>,
pub layout_cell: std::rc::Rc<dyn Fn(RawHandle, f64)>,
pub layout_cell_size: std::rc::Rc<dyn Fn(RawHandle, Size)>,
pub cell_height: std::rc::Rc<dyn Fn(RawHandle) -> f64>,
pub type_select_text: std::rc::Rc<dyn Fn(u64) -> String>,
pub row_menu: Option<std::rc::Rc<dyn Fn(u64) -> Vec<MenuItem>>>,
pub moves: Option<TreeMoves>,
}
#[derive(Clone)]
pub struct TreeMoves {
#[allow(clippy::type_complexity)]
pub can_move: std::rc::Rc<dyn Fn(u64, Option<u64>, Option<usize>) -> MoveVerdict>,
#[allow(clippy::type_complexity)]
pub move_node: std::rc::Rc<dyn Fn(u64, Option<u64>, Option<usize>)>,
}
pub type ContextMenuFn = std::rc::Rc<dyn Fn(Point) -> Vec<MenuItem>>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MoveVerdict {
Allow,
Deny,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Subcontrol {
Host,
Content,
Header,
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub enum AppBadge {
#[default]
None,
Count(u32),
Text(String),
Dot,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
pub enum WidthClass {
#[default]
Compact,
Medium,
Expanded,
Large,
ExtraLarge,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
pub enum HeightClass {
#[default]
Compact,
Medium,
Expanded,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct SizeClass {
pub width: WidthClass,
pub height: HeightClass,
}
impl SizeClass {
pub const SPLIT_MIN_WIDTH: f64 = 600.0;
pub fn from_size(width: f64, height: f64) -> Self {
SizeClass {
width: match width {
w if w >= 1600.0 => WidthClass::ExtraLarge,
w if w >= 1200.0 => WidthClass::Large,
w if w >= 840.0 => WidthClass::Expanded,
w if w >= Self::SPLIT_MIN_WIDTH => WidthClass::Medium,
_ => WidthClass::Compact,
},
height: match height {
h if h >= 900.0 => HeightClass::Expanded,
h if h >= 480.0 => HeightClass::Medium,
_ => HeightClass::Compact,
},
}
}
pub fn prefers_split(self) -> bool {
self.width > WidthClass::Compact
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Cap {
SearchField,
ComboBox,
Stepper,
DateTimePicker,
ColorPicker,
PullRefresh,
Pager,
PullRefreshGesture,
DragDrop,
DragExternalImport,
DragExternalExport,
DragMultipleItems,
DragFileReferences,
DragDeferredReceipt,
DragFilePromises,
DragExternalMove,
ListRecycling,
UndoBridge,
EditBridge,
ListReorder,
NavReorder,
ListDelete,
ListSwipeActions,
ListEditing,
ListItem,
Tree,
TreeMove,
TreeSections,
BaselineAlignment,
TextRuns,
TextLinks,
Lottie,
NativeSymbols,
Snapshot,
NavSplit,
NavTabs,
NavTabsAdaptive,
NavRepresent,
NavContentList,
NavHeader,
Appearance,
AppMenu,
Dialogs,
FileDialogs,
FolderDialog,
Animation,
Cover,
CoverSheet,
StatusBarHidden,
TextEditable,
TextSelectable,
TextSpellCheck,
MultiWindow,
AppBadgeCount,
AppBadgeText,
AppBadgeDot,
Toolbar,
ToolbarSearch,
Inspector,
Split,
Cursor,
FontList,
ReduceMotion,
Announce,
ImageDecode,
ImageEncode,
ImageProperties,
CanvasBitmap,
CanvasPdf,
WindowStates,
WindowFullscreen,
ContentProtection,
WindowTabbing,
DockMenu,
WindowGeometry,
WindowPosition,
WindowLevel,
WindowAppearance,
RequestAttention,
FramelessWindow,
OverlayTitleBar,
TransparentWindow,
WindowMaterial,
DragRegion,
StatusItem,
AppProgress,
DockVisibility,
Monitors,
DynamicShortcuts,
ListSections,
}
impl Cap {
pub const ALL: [Cap; 90] = [
Cap::DragDrop,
Cap::DragExternalImport,
Cap::DragExternalExport,
Cap::DragMultipleItems,
Cap::DragFileReferences,
Cap::DragDeferredReceipt,
Cap::DragFilePromises,
Cap::DragExternalMove,
Cap::ListRecycling,
Cap::UndoBridge,
Cap::EditBridge,
Cap::ListReorder,
Cap::NavReorder,
Cap::ListDelete,
Cap::ListSwipeActions,
Cap::Tree,
Cap::TreeMove,
Cap::BaselineAlignment,
Cap::TextRuns,
Cap::TextLinks,
Cap::Lottie,
Cap::NativeSymbols,
Cap::Snapshot,
Cap::NavSplit,
Cap::NavTabs,
Cap::NavTabsAdaptive,
Cap::NavRepresent,
Cap::NavContentList,
Cap::NavHeader,
Cap::Appearance,
Cap::AppMenu,
Cap::Dialogs,
Cap::FileDialogs,
Cap::Animation,
Cap::Cover,
Cap::StatusBarHidden,
Cap::TextEditable,
Cap::TextSelectable,
Cap::TextSpellCheck,
Cap::MultiWindow,
Cap::AppBadgeCount,
Cap::AppBadgeText,
Cap::AppBadgeDot,
Cap::Toolbar,
Cap::ToolbarSearch,
Cap::Inspector,
Cap::Cursor,
Cap::FontList,
Cap::ReduceMotion,
Cap::Announce,
Cap::ImageDecode,
Cap::ImageEncode,
Cap::ImageProperties,
Cap::WindowStates,
Cap::WindowFullscreen,
Cap::ContentProtection,
Cap::WindowTabbing,
Cap::DockMenu,
Cap::WindowGeometry,
Cap::WindowPosition,
Cap::WindowLevel,
Cap::WindowAppearance,
Cap::RequestAttention,
Cap::FramelessWindow,
Cap::OverlayTitleBar,
Cap::TransparentWindow,
Cap::WindowMaterial,
Cap::DragRegion,
Cap::StatusItem,
Cap::AppProgress,
Cap::DockVisibility,
Cap::Monitors,
Cap::DynamicShortcuts,
Cap::Split,
Cap::TreeSections,
Cap::CoverSheet,
Cap::ListEditing,
Cap::ListItem,
Cap::CanvasBitmap,
Cap::CanvasPdf,
Cap::SearchField,
Cap::ComboBox,
Cap::Stepper,
Cap::DateTimePicker,
Cap::ColorPicker,
Cap::PullRefresh,
Cap::Pager,
Cap::PullRefreshGesture,
Cap::FolderDialog,
Cap::ListSections,
];
pub const fn ordinal(self) -> usize {
match self {
Cap::DragDrop => 0,
Cap::DragExternalImport => 1,
Cap::DragExternalExport => 2,
Cap::DragMultipleItems => 3,
Cap::DragFileReferences => 4,
Cap::DragDeferredReceipt => 5,
Cap::DragFilePromises => 6,
Cap::DragExternalMove => 7,
Cap::ListRecycling => 8,
Cap::UndoBridge => 9,
Cap::EditBridge => 10,
Cap::ListReorder => 11,
Cap::NavReorder => 12,
Cap::ListDelete => 13,
Cap::ListSwipeActions => 14,
Cap::Tree => 15,
Cap::TreeMove => 16,
Cap::BaselineAlignment => 17,
Cap::TextRuns => 18,
Cap::TextLinks => 19,
Cap::Lottie => 20,
Cap::NativeSymbols => 21,
Cap::Snapshot => 22,
Cap::NavSplit => 23,
Cap::NavTabs => 24,
Cap::NavTabsAdaptive => 25,
Cap::NavRepresent => 26,
Cap::NavContentList => 27,
Cap::NavHeader => 28,
Cap::Appearance => 29,
Cap::AppMenu => 30,
Cap::Dialogs => 31,
Cap::FileDialogs => 32,
Cap::Animation => 33,
Cap::Cover => 34,
Cap::StatusBarHidden => 35,
Cap::TextEditable => 36,
Cap::TextSelectable => 37,
Cap::TextSpellCheck => 38,
Cap::MultiWindow => 39,
Cap::AppBadgeCount => 40,
Cap::AppBadgeText => 41,
Cap::AppBadgeDot => 42,
Cap::Toolbar => 43,
Cap::ToolbarSearch => 44,
Cap::Inspector => 45,
Cap::Cursor => 46,
Cap::FontList => 47,
Cap::ReduceMotion => 48,
Cap::Announce => 49,
Cap::ImageDecode => 50,
Cap::ImageEncode => 51,
Cap::ImageProperties => 52,
Cap::WindowStates => 53,
Cap::WindowFullscreen => 54,
Cap::ContentProtection => 55,
Cap::WindowTabbing => 56,
Cap::DockMenu => 57,
Cap::WindowGeometry => 58,
Cap::WindowPosition => 59,
Cap::WindowLevel => 60,
Cap::WindowAppearance => 61,
Cap::RequestAttention => 62,
Cap::FramelessWindow => 63,
Cap::OverlayTitleBar => 64,
Cap::TransparentWindow => 65,
Cap::WindowMaterial => 66,
Cap::DragRegion => 67,
Cap::StatusItem => 68,
Cap::AppProgress => 69,
Cap::DockVisibility => 70,
Cap::Monitors => 71,
Cap::DynamicShortcuts => 72,
Cap::Split => 73,
Cap::TreeSections => 74,
Cap::CoverSheet => 75,
Cap::ListEditing => 76,
Cap::ListItem => 77,
Cap::CanvasBitmap => 78,
Cap::CanvasPdf => 79,
Cap::SearchField => 80,
Cap::ComboBox => 81,
Cap::Stepper => 82,
Cap::DateTimePicker => 83,
Cap::ColorPicker => 84,
Cap::PullRefresh => 85,
Cap::Pager => 86,
Cap::PullRefreshGesture => 87,
Cap::FolderDialog => 88,
Cap::ListSections => 89,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BitmapId(pub u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum ImageFormat {
#[default]
Png,
Jpeg,
Webp,
Gif,
Bmp,
Tiff,
Heif,
Unknown,
}
impl ImageFormat {
pub fn mime(self) -> &'static str {
match self {
ImageFormat::Png => "image/png",
ImageFormat::Jpeg => "image/jpeg",
ImageFormat::Webp => "image/webp",
ImageFormat::Gif => "image/gif",
ImageFormat::Bmp => "image/bmp",
ImageFormat::Tiff => "image/tiff",
ImageFormat::Heif => "image/heif",
ImageFormat::Unknown => "application/octet-stream",
}
}
pub fn dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
let be16 = |i: usize| -> Option<u32> {
Some(u32::from(u16::from_be_bytes([
*bytes.get(i)?,
*bytes.get(i + 1)?,
])))
};
let le16 = |i: usize| -> Option<u32> {
Some(u32::from(u16::from_le_bytes([
*bytes.get(i)?,
*bytes.get(i + 1)?,
])))
};
let be32 = |i: usize| -> Option<u32> {
Some(u32::from_be_bytes([
*bytes.get(i)?,
*bytes.get(i + 1)?,
*bytes.get(i + 2)?,
*bytes.get(i + 3)?,
]))
};
let le32 = |i: usize| -> Option<u32> {
Some(u32::from_le_bytes([
*bytes.get(i)?,
*bytes.get(i + 1)?,
*bytes.get(i + 2)?,
*bytes.get(i + 3)?,
]))
};
match Self::sniff(bytes)? {
ImageFormat::Png => Some((be32(16)?, be32(20)?)),
ImageFormat::Gif => Some((le16(6)?, le16(8)?)),
ImageFormat::Bmp => Some((
le32(18)?.cast_signed().unsigned_abs(),
le32(22)?.cast_signed().unsigned_abs(),
)),
ImageFormat::Jpeg => {
let mut i = 2usize;
while i + 9 <= bytes.len() {
if bytes[i] != 0xFF {
i += 1;
continue;
}
let marker = bytes[i + 1];
if marker == 0xFF || (0xD0..=0xD9).contains(&marker) || marker == 0x01 {
i += 1;
continue;
}
let is_sof = (0xC0..=0xCF).contains(&marker)
&& marker != 0xC4
&& marker != 0xC8
&& marker != 0xCC;
if is_sof {
return Some((be16(i + 7)?, be16(i + 5)?));
}
i += 2 + be16(i + 2)? as usize;
}
None
}
ImageFormat::Webp => match bytes.get(12..16)? {
b"VP8X" => {
let w = u32::from(*bytes.get(24)?)
| u32::from(*bytes.get(25)?) << 8
| u32::from(*bytes.get(26)?) << 16;
let h = u32::from(*bytes.get(27)?)
| u32::from(*bytes.get(28)?) << 8
| u32::from(*bytes.get(29)?) << 16;
Some((w + 1, h + 1))
}
b"VP8 " => Some((le16(26)? & 0x3FFF, le16(28)? & 0x3FFF)),
b"VP8L" => {
let bits = le32(21)?;
Some(((bits & 0x3FFF) + 1, ((bits >> 14) & 0x3FFF) + 1))
}
_ => None,
},
ImageFormat::Tiff | ImageFormat::Heif | ImageFormat::Unknown => None,
}
}
pub fn sniff(bytes: &[u8]) -> Option<ImageFormat> {
let starts = |sig: &[u8]| bytes.len() >= sig.len() && &bytes[..sig.len()] == sig;
if starts(&[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]) {
return Some(ImageFormat::Png);
}
if starts(&[0xff, 0xd8, 0xff]) {
return Some(ImageFormat::Jpeg);
}
if starts(b"GIF87a") || starts(b"GIF89a") {
return Some(ImageFormat::Gif);
}
if starts(b"BM") {
return Some(ImageFormat::Bmp);
}
if starts(&[0x49, 0x49, 0x2a, 0x00]) || starts(&[0x4d, 0x4d, 0x00, 0x2a]) {
return Some(ImageFormat::Tiff);
}
if starts(b"RIFF") && bytes.len() >= 12 && &bytes[8..12] == b"WEBP" {
return Some(ImageFormat::Webp);
}
if bytes.len() >= 12 && &bytes[4..8] == b"ftyp" {
let brand = &bytes[8..12];
if brand == b"heic" || brand == b"heix" || brand == b"mif1" || brand == b"msf1" {
return Some(ImageFormat::Heif);
}
}
None
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct BitmapInfo {
pub pixels: Size,
pub scale: f64,
pub format: Option<ImageFormat>,
pub has_alpha: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EncodeSpec {
pub format: ImageFormat,
pub quality: Option<f64>,
pub fit: Option<Size>,
}
impl Default for EncodeSpec {
fn default() -> Self {
EncodeSpec {
format: ImageFormat::Png,
quality: None,
fit: None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ImageProperties {
pub orientation: Option<u16>,
pub dpi: Option<f64>,
pub created: Option<String>,
pub extra: Vec<(String, String)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ImageError {
Decode,
Encode,
Unsupported,
Gone,
}
impl std::fmt::Display for ImageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImageError::Decode => write!(f, "the bytes are not a decodable image"),
ImageError::Encode => write!(f, "this backend cannot encode that format"),
ImageError::Unsupported => write!(f, "image support is unavailable on this backend"),
ImageError::Gone => write!(f, "the image was released"),
}
}
}
impl std::error::Error for ImageError {}
pub const EXPORT_MAX_SIDE: u32 = 16_384;
pub const EXPORT_MAX_PIXELS: u64 = 64 * 1024 * 1024;
pub fn export_pixels(size: Size, scale: f64) -> (u32, u32) {
let side = |v: f64| (v * scale).ceil().clamp(0.0, u32::MAX as f64) as u32;
(side(size.width), side(size.height))
}
#[derive(Clone, Debug, PartialEq)]
pub struct PdfPage {
pub size: Size,
pub ops: Vec<DrawOp>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PdfInfo {
pub title: Option<String>,
pub author: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ExportError {
Unsupported,
TooLarge { width: u32, height: u32 },
Invalid,
Failed(String),
}
impl std::fmt::Display for ExportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExportError::Unsupported => write!(f, "this export is unavailable on this backend"),
ExportError::TooLarge { width, height } => {
write!(
f,
"a {width} × {height} pixel bitmap is too large to render"
)
}
ExportError::Invalid => write!(f, "the drawing has no size or the PDF has no pages"),
ExportError::Failed(why) => write!(f, "rendering failed: {why}"),
}
}
}
impl std::error::Error for ExportError {}
#[derive(Clone)]
pub enum ImageSource {
Named(String),
Bytes(std::sync::Arc<Vec<u8>>),
Decoded(BitmapId),
}
impl Default for ImageSource {
fn default() -> Self {
ImageSource::Named(String::new())
}
}
impl PartialEq for ImageSource {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(ImageSource::Named(a), ImageSource::Named(b)) => a == b,
(ImageSource::Bytes(a), ImageSource::Bytes(b)) => std::sync::Arc::ptr_eq(a, b),
(ImageSource::Decoded(a), ImageSource::Decoded(b)) => a == b,
_ => false,
}
}
}
impl std::fmt::Debug for ImageSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImageSource::Named(n) => f.debug_tuple("Named").field(n).finish(),
ImageSource::Bytes(b) => f.debug_tuple("Bytes").field(&b.len()).finish(),
ImageSource::Decoded(id) => f.debug_tuple("Decoded").field(&id.0).finish(),
}
}
}
impl From<BitmapId> for ImageSource {
fn from(id: BitmapId) -> Self {
ImageSource::Decoded(id)
}
}
impl From<resource::ImageName> for ImageSource {
fn from(name: resource::ImageName) -> Self {
ImageSource::Named(name.as_str().to_owned())
}
}
impl From<resource::VectorName> for ImageSource {
fn from(name: resource::VectorName) -> Self {
ImageSource::Named(name.as_str().to_owned())
}
}
impl From<std::sync::Arc<Vec<u8>>> for ImageSource {
fn from(bytes: std::sync::Arc<Vec<u8>>) -> Self {
ImageSource::Bytes(bytes)
}
}
impl From<String> for ImageSource {
fn from(name: String) -> Self {
ImageSource::Named(name)
}
}
impl From<Vec<u8>> for ImageSource {
fn from(bytes: Vec<u8>) -> Self {
ImageSource::Bytes(std::sync::Arc::new(bytes))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Support {
Native,
Emulated,
#[default]
Unsupported,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum InputPurpose {
#[default]
Text,
Name,
Email,
Url,
Phone,
Number,
Decimal,
Username,
Password,
NewPassword,
OneTimeCode,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SubmitLabel {
#[default]
Return,
Done,
Go,
Next,
Search,
Send,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct InputTraits {
pub secure: bool,
pub read_only: bool,
pub purpose: InputPurpose,
pub submit_label: SubmitLabel,
pub max_length: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
pub enum Cursor {
#[default]
Default,
Pointer,
Text,
VerticalText,
Crosshair,
Move,
Grab,
Grabbing,
NotAllowed,
Wait,
Progress,
Help,
ContextMenu,
Copy,
Alias,
Cell,
ZoomIn,
ZoomOut,
None,
NsResize,
EwResize,
NeswResize,
NwseResize,
ColResize,
RowResize,
Native(std::borrow::Cow<'static, str>),
}
impl Cursor {
pub const fn native(name: &'static str) -> Self {
Cursor::Native(std::borrow::Cow::Borrowed(name))
}
pub fn css_name(&self) -> &str {
match self {
Cursor::Default => "default",
Cursor::Pointer => "pointer",
Cursor::Text => "text",
Cursor::VerticalText => "vertical-text",
Cursor::Crosshair => "crosshair",
Cursor::Move => "move",
Cursor::Grab => "grab",
Cursor::Grabbing => "grabbing",
Cursor::NotAllowed => "not-allowed",
Cursor::Wait => "wait",
Cursor::Progress => "progress",
Cursor::Help => "help",
Cursor::ContextMenu => "context-menu",
Cursor::Copy => "copy",
Cursor::Alias => "alias",
Cursor::Cell => "cell",
Cursor::ZoomIn => "zoom-in",
Cursor::ZoomOut => "zoom-out",
Cursor::None => "none",
Cursor::NsResize => "ns-resize",
Cursor::EwResize => "ew-resize",
Cursor::NeswResize => "nesw-resize",
Cursor::NwseResize => "nwse-resize",
Cursor::ColResize => "col-resize",
Cursor::RowResize => "row-resize",
Cursor::Native(name) => name,
}
}
pub const NAMED: [Cursor; 25] = [
Cursor::Default,
Cursor::Pointer,
Cursor::Text,
Cursor::VerticalText,
Cursor::Crosshair,
Cursor::Move,
Cursor::Grab,
Cursor::Grabbing,
Cursor::NotAllowed,
Cursor::Wait,
Cursor::Progress,
Cursor::Help,
Cursor::ContextMenu,
Cursor::Copy,
Cursor::Alias,
Cursor::Cell,
Cursor::ZoomIn,
Cursor::ZoomOut,
Cursor::None,
Cursor::NsResize,
Cursor::EwResize,
Cursor::NeswResize,
Cursor::NwseResize,
Cursor::ColResize,
Cursor::RowResize,
];
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Edges(pub u8);
impl Edges {
pub const NONE: Edges = Edges(0);
pub const TOP: Edges = Edges(1);
pub const BOTTOM: Edges = Edges(2);
pub const LEADING: Edges = Edges(4);
pub const TRAILING: Edges = Edges(8);
pub const ALL: Edges = Edges(15);
pub fn contains(self, other: Edges) -> bool {
self.0 & other.0 == other.0
}
pub fn union(self, other: Edges) -> Edges {
Edges(self.0 | other.0)
}
pub fn is_empty(self) -> bool {
self.0 == 0
}
}
impl std::ops::BitOr for Edges {
type Output = Edges;
fn bitor(self, rhs: Edges) -> Edges {
self.union(rhs)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Curve {
Linear,
EaseIn,
EaseOut,
EaseInOut,
Spring {
response: f64,
damping: f64,
},
}
impl Curve {
pub fn fraction(self, elapsed: f64, duration: f64) -> f64 {
match self {
Curve::Spring { response, damping } => spring_step(response, damping, elapsed),
_ => {
let t = if duration <= 0.0 {
1.0
} else {
(elapsed / duration).clamp(0.0, 1.0)
};
self.ease(t)
}
}
}
#[inline]
pub fn ease(self, t: f64) -> f64 {
match self {
Curve::Linear | Curve::Spring { .. } => t,
Curve::EaseIn => t * t,
Curve::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
Curve::EaseInOut => t * t * (3.0 - 2.0 * t), }
}
pub fn is_settled(self, elapsed: f64, duration: f64) -> bool {
match self {
Curve::Spring { response, damping } => {
let cap = response.max(0.0) * 4.0 + 0.2;
if elapsed >= cap {
return true;
}
(spring_step(response, damping, elapsed) - 1.0).abs() < 0.001
&& elapsed > response * 0.5
}
_ => elapsed >= duration.max(0.0),
}
}
}
fn spring_step(response: f64, damping: f64, t: f64) -> f64 {
if response <= 0.0 || t <= 0.0 {
return if t <= 0.0 { 0.0 } else { 1.0 };
}
let omega0 = std::f64::consts::TAU / response;
let zeta = damping.max(0.0);
if zeta < 1.0 {
let omega_d = omega0 * (1.0 - zeta * zeta).sqrt();
let e = (-zeta * omega0 * t).exp();
1.0 - e * ((omega_d * t).cos() + (zeta * omega0 / omega_d) * (omega_d * t).sin())
} else {
let e = (-omega0 * t).exp();
1.0 - e * (1.0 + omega0 * t)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnimSpec {
pub duration_ms: u32,
pub delay_ms: u32,
pub curve: Curve,
pub repeat: u32,
pub autoreverse: bool,
}
impl Default for AnimSpec {
fn default() -> Self {
AnimSpec {
duration_ms: 350,
delay_ms: 0,
curve: Curve::Spring {
response: 0.4,
damping: 0.8,
},
repeat: 0,
autoreverse: false,
}
}
}
impl AnimSpec {
pub fn spring(response: f64, damping: f64) -> Self {
AnimSpec {
duration_ms: (response * 1000.0).max(50.0) as u32,
delay_ms: 0,
curve: Curve::Spring { response, damping },
repeat: 0,
autoreverse: false,
}
}
pub fn linear(duration_ms: u32) -> Self {
Self::timed(duration_ms, Curve::Linear)
}
pub fn ease_in(duration_ms: u32) -> Self {
Self::timed(duration_ms, Curve::EaseIn)
}
pub fn ease_out(duration_ms: u32) -> Self {
Self::timed(duration_ms, Curve::EaseOut)
}
pub fn ease_in_out(duration_ms: u32) -> Self {
Self::timed(duration_ms, Curve::EaseInOut)
}
fn timed(duration_ms: u32, curve: Curve) -> Self {
AnimSpec {
duration_ms,
delay_ms: 0,
curve,
repeat: 0,
autoreverse: false,
}
}
pub fn delay(mut self, ms: u32) -> Self {
self.delay_ms = ms;
self
}
pub fn repeat(mut self, count: u32, autoreverse: bool) -> Self {
self.repeat = count;
self.autoreverse = autoreverse;
self
}
pub fn repeat_forever(mut self, autoreverse: bool) -> Self {
self.repeat = u32::MAX;
self.autoreverse = autoreverse;
self
}
#[inline]
pub fn duration_secs(&self) -> f64 {
self.duration_ms as f64 / 1000.0
}
#[inline]
pub fn delay_secs(&self) -> f64 {
self.delay_ms as f64 / 1000.0
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Role {
#[default]
None,
Button,
Toggle,
Slider,
TextInput,
Heading(u8),
Image,
Meter,
Group,
Tree,
TreeItem,
Tab,
TabList,
}
impl Role {
pub fn for_kind(kind: PieceKind) -> Role {
match kind {
kinds::BUTTON => Role::Button,
kinds::TOGGLE => Role::Toggle,
kinds::SLIDER => Role::Slider,
kinds::TEXT_FIELD => Role::TextInput,
kinds::IMAGE => Role::Image,
kinds::PROGRESS => Role::Meter,
kinds::TREE => Role::Tree,
_ => Role::None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct A11yProps {
pub label: Option<String>,
pub hint: Option<String>,
pub value: Option<String>,
pub role: Role,
pub identifier: Option<String>,
pub hidden: bool,
pub decorative: bool,
pub selected: Option<bool>,
}
impl A11yProps {
pub fn merge(&mut self, other: &A11yProps) {
if other.label.is_some() {
self.label = other.label.clone();
}
if other.hint.is_some() {
self.hint = other.hint.clone();
}
if other.value.is_some() {
self.value = other.value.clone();
}
if other.role != Role::None {
self.role = other.role;
}
if other.identifier.is_some() {
self.identifier = other.identifier.clone();
}
self.hidden |= other.hidden;
self.decorative |= other.decorative;
if other.selected.is_some() {
self.selected = other.selected;
}
}
pub fn resolved_role(&self, kind: PieceKind) -> Role {
if self.role != Role::None {
self.role
} else {
Role::for_kind(kind)
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct NativeSnapshot {
pub found: bool,
pub role: Role,
pub label: Option<String>,
pub value: Option<String>,
pub identifier: Option<String>,
pub text: Option<String>,
pub number: Option<f64>,
pub checked: Option<bool>,
pub enabled: Option<bool>,
pub visible: Option<bool>,
pub frame: Option<Rect>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Shape {
Rect(Rect),
RoundedRect(Rect, f64),
Ellipse(Rect),
Arc {
rect: Rect,
start_deg: f64,
sweep_deg: f64,
},
Line(Point, Point),
Polygon(Vec<Point>),
Path(Path),
}
impl Shape {
pub fn rounded_rect(rect: Rect, radii: CornerRadii) -> Shape {
let r = radii.fitted(rect.size);
if r.is_zero() || rect.size.width <= 0.0 || rect.size.height <= 0.0 {
Shape::Rect(rect)
} else if r.is_uniform() {
Shape::RoundedRect(rect, r.top_left)
} else {
Shape::Path(Path {
segs: rounded_rect_segs(rect, r),
rule: FillRule::NonZero,
})
}
}
pub fn translated(&self, dx: f64, dy: f64) -> Shape {
let r = |r: &Rect| {
Rect::new(
r.origin.x + dx,
r.origin.y + dy,
r.size.width,
r.size.height,
)
};
let p = |p: &Point| Point::new(p.x + dx, p.y + dy);
match self {
Shape::Rect(rect) => Shape::Rect(r(rect)),
Shape::RoundedRect(rect, rad) => Shape::RoundedRect(r(rect), *rad),
Shape::Ellipse(rect) => Shape::Ellipse(r(rect)),
Shape::Arc {
rect,
start_deg,
sweep_deg,
} => Shape::Arc {
rect: r(rect),
start_deg: *start_deg,
sweep_deg: *sweep_deg,
},
Shape::Line(a, b) => Shape::Line(p(a), p(b)),
Shape::Polygon(pts) => Shape::Polygon(pts.iter().map(p).collect()),
Shape::Path(path) => Shape::Path(Path {
segs: path
.segs
.iter()
.map(|seg| match seg {
PathSeg::Move(a) => PathSeg::Move(p(a)),
PathSeg::Line(a) => PathSeg::Line(p(a)),
PathSeg::Quad(c, a) => PathSeg::Quad(p(c), p(a)),
PathSeg::Cubic(c1, c2, a) => PathSeg::Cubic(p(c1), p(c2), p(a)),
PathSeg::Close => PathSeg::Close,
})
.collect(),
rule: path.rule,
}),
}
}
pub fn bounds(&self) -> Rect {
match self {
Shape::Rect(r) | Shape::RoundedRect(r, _) | Shape::Ellipse(r) => *r,
Shape::Arc { rect, .. } => *rect,
Shape::Line(a, b) => points_bounds(&[*a, *b]),
Shape::Polygon(pts) => points_bounds(pts),
Shape::Path(p) => p.bounds(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PathSeg {
Move(Point),
Line(Point),
Quad(Point, Point),
Cubic(Point, Point, Point),
Close,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum FillRule {
#[default]
NonZero,
EvenOdd,
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct Path {
pub segs: Vec<PathSeg>,
pub rule: FillRule,
}
impl Path {
pub fn bounds(&self) -> Rect {
let mut pts: Vec<Point> = Vec::with_capacity(self.segs.len() * 3);
for seg in &self.segs {
match seg {
PathSeg::Move(p) | PathSeg::Line(p) => pts.push(*p),
PathSeg::Quad(c, p) => pts.extend_from_slice(&[*c, *p]),
PathSeg::Cubic(c1, c2, p) => pts.extend_from_slice(&[*c1, *c2, *p]),
PathSeg::Close => {}
}
}
points_bounds(&pts)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct CornerRadii {
pub top_left: f64,
pub top_right: f64,
pub bottom_right: f64,
pub bottom_left: f64,
}
impl CornerRadii {
pub fn uniform(r: f64) -> Self {
CornerRadii {
top_left: r,
top_right: r,
bottom_right: r,
bottom_left: r,
}
}
pub fn top(r: f64) -> Self {
CornerRadii {
top_left: r,
top_right: r,
..Default::default()
}
}
pub fn bottom(r: f64) -> Self {
CornerRadii {
bottom_right: r,
bottom_left: r,
..Default::default()
}
}
pub fn left(r: f64) -> Self {
CornerRadii {
top_left: r,
bottom_left: r,
..Default::default()
}
}
pub fn right(r: f64) -> Self {
CornerRadii {
top_right: r,
bottom_right: r,
..Default::default()
}
}
pub fn to_array(self) -> [f64; 4] {
[
self.top_left,
self.top_right,
self.bottom_right,
self.bottom_left,
]
}
pub fn is_zero(self) -> bool {
self.to_array().iter().all(|r| *r <= 0.0)
}
pub fn is_uniform(self) -> bool {
let [a, b, c, d] = self.to_array();
a == b && b == c && c == d
}
pub fn fitted(self, size: Size) -> Self {
let clean = |r: f64| if r.is_finite() && r > 0.0 { r } else { 0.0 };
let [tl, tr, br, bl] = self.to_array().map(clean);
let (w, h) = (size.width.max(0.0), size.height.max(0.0));
let factor = [(w, tl + tr), (w, bl + br), (h, tl + bl), (h, tr + br)]
.into_iter()
.filter(|(_, sum)| *sum > 0.0)
.map(|(side, sum)| side / sum)
.fold(1.0f64, f64::min);
CornerRadii {
top_left: tl * factor,
top_right: tr * factor,
bottom_right: br * factor,
bottom_left: bl * factor,
}
}
}
pub fn arc_point(center: Point, radius: f64, angle_deg: f64) -> Point {
let a = angle_deg.to_radians();
Point::new(center.x + radius * a.cos(), center.y + radius * a.sin())
}
pub fn arc_cubics(center: Point, radius: f64, start_deg: f64, sweep_deg: f64) -> Vec<PathSeg> {
let (start, sweep) = (start_deg.to_radians(), sweep_deg.to_radians());
if !radius.is_finite() || radius <= 0.0 || !sweep.is_finite() || sweep == 0.0 {
return Vec::new();
}
let at = |a: f64| Point::new(center.x + radius * a.cos(), center.y + radius * a.sin());
let steps = (sweep.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize;
let delta = sweep / steps as f64;
let k = 4.0 / 3.0 * (delta / 4.0).tan() * radius;
(0..steps)
.map(|i| {
let a = start + delta * i as f64;
let b = a + delta;
let (p0, p1) = (at(a), at(b));
PathSeg::Cubic(
Point::new(p0.x - k * a.sin(), p0.y + k * a.cos()),
Point::new(p1.x + k * b.sin(), p1.y - k * b.cos()),
p1,
)
})
.collect()
}
pub fn rounded_rect_segs(rect: Rect, radii: CornerRadii) -> Vec<PathSeg> {
let r = radii.fitted(rect.size);
let (x0, y0) = (rect.origin.x, rect.origin.y);
let (x1, y1) = (x0 + rect.size.width, y0 + rect.size.height);
let corners = [
(
Point::new(x0 + r.top_left, y0 + r.top_left),
r.top_left,
180.0,
),
(
Point::new(x1 - r.top_right, y0 + r.top_right),
r.top_right,
270.0,
),
(
Point::new(x1 - r.bottom_right, y1 - r.bottom_right),
r.bottom_right,
0.0,
),
(
Point::new(x0 + r.bottom_left, y1 - r.bottom_left),
r.bottom_left,
90.0,
),
];
let mut segs = Vec::with_capacity(14);
for (i, (center, radius, start)) in corners.into_iter().enumerate() {
let first = arc_point(center, radius, start);
segs.push(if i == 0 {
PathSeg::Move(first)
} else {
PathSeg::Line(first)
});
segs.extend(arc_cubics(center, radius, start, 90.0));
}
segs.push(PathSeg::Close);
segs
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum LineJoin {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Clone, Debug, PartialEq)]
pub struct StrokeStyle {
pub width: f64,
pub cap: LineCap,
pub join: LineJoin,
pub miter_limit: f64,
pub dash: Vec<f64>,
pub dash_phase: f64,
}
impl Default for StrokeStyle {
fn default() -> Self {
StrokeStyle {
width: 1.0,
cap: LineCap::Butt,
join: LineJoin::Miter,
miter_limit: 10.0,
dash: Vec::new(),
dash_phase: 0.0,
}
}
}
impl StrokeStyle {
pub fn width(width: f64) -> Self {
StrokeStyle {
width,
..Default::default()
}
}
pub fn dashed(width: f64, dash: Vec<f64>) -> Self {
StrokeStyle {
width,
dash,
..Default::default()
}
}
pub fn round(width: f64) -> Self {
StrokeStyle {
width,
cap: LineCap::Round,
join: LineJoin::Round,
..Default::default()
}
}
pub fn is_plain(&self) -> bool {
self.cap == LineCap::Butt
&& self.join == LineJoin::Miter
&& self.dash.is_empty()
&& self.miter_limit == 10.0
}
}
impl From<f64> for StrokeStyle {
fn from(width: f64) -> Self {
StrokeStyle::width(width)
}
}
fn points_bounds(pts: &[Point]) -> Rect {
let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for p in pts {
x0 = x0.min(p.x);
y0 = y0.min(p.y);
x1 = x1.max(p.x);
y1 = y1.max(p.y);
}
if pts.is_empty() {
return Rect::ZERO;
}
Rect::new(x0, y0, x1 - x0, y1 - y0)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UnitPoint {
pub x: f64,
pub y: f64,
}
impl UnitPoint {
pub const fn new(x: f64, y: f64) -> Self {
UnitPoint { x, y }
}
pub const TOP: UnitPoint = UnitPoint::new(0.5, 0.0);
pub const BOTTOM: UnitPoint = UnitPoint::new(0.5, 1.0);
pub const LEADING: UnitPoint = UnitPoint::new(0.0, 0.5);
pub const TRAILING: UnitPoint = UnitPoint::new(1.0, 0.5);
pub const TOP_LEADING: UnitPoint = UnitPoint::new(0.0, 0.0);
pub const TOP_TRAILING: UnitPoint = UnitPoint::new(1.0, 0.0);
pub const BOTTOM_LEADING: UnitPoint = UnitPoint::new(0.0, 1.0);
pub const BOTTOM_TRAILING: UnitPoint = UnitPoint::new(1.0, 1.0);
pub const CENTER: UnitPoint = UnitPoint::new(0.5, 0.5);
pub fn resolve(&self, rect: Rect) -> Point {
Point::new(
rect.origin.x + self.x * rect.size.width,
rect.origin.y + self.y * rect.size.height,
)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LinearGradient {
pub start: UnitPoint,
pub end: UnitPoint,
pub stops: Vec<(f64, Color)>,
}
impl LinearGradient {
pub fn new(start: UnitPoint, end: UnitPoint, stops: Vec<(f64, Color)>) -> Self {
LinearGradient { start, end, stops }
}
pub fn vertical(top: Color, bottom: Color) -> Self {
LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
vec![(0.0, top), (1.0, bottom)],
)
}
pub fn horizontal(leading: Color, trailing: Color) -> Self {
LinearGradient::new(
UnitPoint::LEADING,
UnitPoint::TRAILING,
vec![(0.0, leading), (1.0, trailing)],
)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct RadialGradient {
pub center: UnitPoint,
pub radius: f64,
pub stops: Vec<(f64, Color)>,
}
impl RadialGradient {
pub fn new(center: UnitPoint, radius: f64, stops: Vec<(f64, Color)>) -> Self {
RadialGradient {
center,
radius,
stops,
}
}
pub fn centered(inner: Color, outer: Color) -> Self {
RadialGradient::new(UnitPoint::CENTER, 0.5, vec![(0.0, inner), (1.0, outer)])
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Stamp {
pub shape: Shape,
pub at: Vec<Point>,
pub paint: Paint,
pub stroke: Option<StrokeStyle>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Paint {
Solid(Color),
Linear(LinearGradient),
Radial(RadialGradient),
}
impl From<Color> for Paint {
fn from(c: Color) -> Self {
Paint::Solid(c)
}
}
impl From<LinearGradient> for Paint {
fn from(g: LinearGradient) -> Self {
Paint::Linear(g)
}
}
impl From<RadialGradient> for Paint {
fn from(g: RadialGradient) -> Self {
Paint::Radial(g)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum TextVAlign {
#[default]
Top,
Middle,
Baseline,
Bottom,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct TextAnchor {
pub h: props::TextAlign,
pub v: TextVAlign,
}
impl TextAnchor {
pub const LEADING: TextAnchor = TextAnchor {
h: props::TextAlign::Leading,
v: TextVAlign::Top,
};
pub const CENTERED: TextAnchor = TextAnchor {
h: props::TextAlign::Center,
v: TextVAlign::Middle,
};
pub const TRAILING: TextAnchor = TextAnchor {
h: props::TextAlign::Trailing,
v: TextVAlign::Top,
};
pub fn offset(self, width: f64, height: f64, ascent: f64) -> (f64, f64) {
let dx = match self.h {
props::TextAlign::Leading => 0.0,
props::TextAlign::Center => -width / 2.0,
props::TextAlign::Trailing => -width,
};
let dy = match self.v {
TextVAlign::Top => 0.0,
TextVAlign::Middle => -height / 2.0,
TextVAlign::Baseline => -ascent,
TextVAlign::Bottom => -height,
};
(dx, dy)
}
pub fn pack(self) -> f64 {
(self.h as u8 * 4 + self.v as u8) as f64
}
pub fn unpack(code: f64) -> TextAnchor {
let c = code.round().clamp(0.0, 11.0) as u8;
let h = match c / 4 {
1 => props::TextAlign::Center,
2 => props::TextAlign::Trailing,
_ => props::TextAlign::Leading,
};
let v = match c % 4 {
1 => TextVAlign::Middle,
2 => TextVAlign::Baseline,
3 => TextVAlign::Bottom,
_ => TextVAlign::Top,
};
TextAnchor { h, v }
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum DrawOp {
Fill(Shape, Paint),
Stroke(Shape, Paint, StrokeStyle),
Text {
text: String,
at: Point,
size: f64,
color: Color,
anchor: TextAnchor,
font: CanvasFont,
},
Stamp(Box<Stamp>),
Image {
image: BitmapId,
rect: Rect,
opacity: f64,
},
Clip(Shape),
Save,
Restore,
Concat(day_geometry::Affine),
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum Font {
LargeTitle,
Title,
Title2,
Title3,
Headline,
Subheadline,
#[default]
Body,
Callout,
Footnote,
Caption,
Caption2,
System(f64),
Custom(&'static str, f64),
}
impl Font {
pub const fn custom(family: FontFamily, size: f64) -> Font {
Font::Custom(family.as_str(), size)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum FontWeight {
UltraLight,
Thin,
Light,
Regular,
Medium,
Semibold,
Bold,
Heavy,
Black,
}
impl FontWeight {
pub const ALL: [FontWeight; 9] = [
FontWeight::UltraLight,
FontWeight::Thin,
FontWeight::Light,
FontWeight::Regular,
FontWeight::Medium,
FontWeight::Semibold,
FontWeight::Bold,
FontWeight::Heavy,
FontWeight::Black,
];
pub const fn css(self) -> u16 {
match self {
FontWeight::UltraLight => 100,
FontWeight::Thin => 200,
FontWeight::Light => 300,
FontWeight::Regular => 400,
FontWeight::Medium => 500,
FontWeight::Semibold => 600,
FontWeight::Bold => 700,
FontWeight::Heavy => 800,
FontWeight::Black => 900,
}
}
pub fn from_css(n: u16) -> FontWeight {
let n = n.clamp(100, 900);
let idx = (usize::from(n) + 50) / 100; FontWeight::ALL[idx.clamp(1, 9) - 1]
}
pub const fn name(self) -> &'static str {
match self {
FontWeight::UltraLight => "Ultra Light",
FontWeight::Thin => "Thin",
FontWeight::Light => "Light",
FontWeight::Regular => "Regular",
FontWeight::Medium => "Medium",
FontWeight::Semibold => "Semibold",
FontWeight::Bold => "Bold",
FontWeight::Heavy => "Heavy",
FontWeight::Black => "Black",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FontFace {
pub name: String,
pub weight: FontWeight,
pub italic: bool,
}
impl FontFace {
pub fn synthesized_name(weight: FontWeight, italic: bool) -> String {
if italic {
format!("{} Italic", weight.name())
} else {
weight.name().to_string()
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FontFamilyInfo {
pub family: String,
pub faces: Vec<FontFace>,
}
impl FontFamilyInfo {
pub fn has_bold(&self) -> bool {
self.faces.iter().any(|f| f.weight >= FontWeight::Bold)
}
pub fn has_italic(&self) -> bool {
self.faces.iter().any(|f| f.italic)
}
pub fn face_for(&self, weight: FontWeight, italic: bool) -> Option<&FontFace> {
let want = i32::from(weight.css());
self.faces.iter().min_by_key(|f| {
let slant = i32::from(f.italic != italic);
let dist = (i32::from(f.weight.css()) - want).abs();
(slant, dist, i32::from(i32::from(f.weight.css()) < want))
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextMetrics {
pub width: f64,
pub height: f64,
pub ascent: f64,
pub cap_height: f64,
pub ink: Rect,
}
impl TextMetrics {
pub fn approximate(text: &str, size: f64) -> TextMetrics {
let (width, height) = (0.6 * size * text.chars().count() as f64, 1.2 * size);
TextMetrics {
width,
height,
ascent: 0.9 * size,
cap_height: 0.7 * size,
ink: Rect::new(0.0, 0.0, width, height),
}
}
pub fn from_slots(out: &[f64; 8]) -> TextMetrics {
TextMetrics {
width: out[0],
height: out[1],
ascent: out[2],
cap_height: out[3],
ink: Rect::new(out[4], out[5], out[6], out[7]),
}
}
pub fn from_baseline(
width: f64,
ascent: f64,
descent: f64,
cap_height: f64,
ink: Rect,
) -> TextMetrics {
TextMetrics {
width,
height: ascent + descent,
ascent,
cap_height,
ink: Rect::new(
ink.origin.x,
ink.origin.y + ascent,
ink.size.width,
ink.size.height,
),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct CanvasFont {
pub family: Option<String>,
pub weight: Option<FontWeight>,
pub italic: bool,
}
impl CanvasFont {
pub fn family_str(&self) -> &str {
self.family.as_deref().unwrap_or("")
}
pub fn css_weight(&self) -> u16 {
self.weight.map(FontWeight::css).unwrap_or(0)
}
}
pub const FONT_LIST_FIELD: char = '\u{1f}';
pub const FONT_LIST_FAMILY: char = '\u{1e}';
pub fn parse_font_list(s: &str) -> Vec<FontFamilyInfo> {
let mut out = Vec::new();
for fam in s.split(FONT_LIST_FAMILY) {
let mut fields = fam.split(FONT_LIST_FIELD);
let Some(family) = fields.next().map(str::trim).filter(|f| !f.is_empty()) else {
continue;
};
let rest: Vec<&str> = fields.collect();
let mut faces = Vec::new();
for face in rest.chunks(3) {
let [name, weight, italic] = face else {
continue;
};
let Ok(weight) = weight.trim().parse::<u16>() else {
continue;
};
let weight = FontWeight::from_css(weight);
let italic = matches!(italic.trim(), "1" | "true");
let name = name.trim();
let name = if name.is_empty() {
FontFace::synthesized_name(weight, italic)
} else {
name.to_string()
};
if !faces
.iter()
.any(|f: &FontFace| f.weight == weight && f.italic == italic)
{
faces.push(FontFace {
name,
weight,
italic,
});
}
}
out.push(FontFamilyInfo {
family: family.to_string(),
faces,
});
}
out
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FontSpec {
pub style: Font,
pub weight: Option<FontWeight>,
pub italic: bool,
pub tabular: bool,
pub monospace: bool,
pub scale: f64,
}
impl Default for FontSpec {
fn default() -> Self {
FontSpec {
style: Font::Body,
weight: None,
italic: false,
tabular: false,
monospace: false,
scale: 1.0,
}
}
}
impl From<Font> for FontSpec {
fn from(style: Font) -> Self {
FontSpec {
style,
..FontSpec::default()
}
}
}
impl FontSpec {
pub fn new(style: Font) -> Self {
FontSpec::from(style)
}
pub fn scaled(mut self, scale: f64) -> Self {
self.scale = scale;
self
}
pub fn resolved_points(&self, style_points: f64) -> f64 {
let base = match self.style {
Font::System(pt) | Font::Custom(_, pt) => pt,
_ => style_points,
};
(base * self.scale).max(1.0)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextRun {
pub range: std::ops::Range<usize>,
pub font: FontSpec,
pub color: Option<Color>,
pub background: Option<Color>,
pub underline: Underline,
pub strikethrough: bool,
pub link: Option<String>,
}
impl Default for TextRun {
fn default() -> Self {
TextRun {
range: 0..0,
font: FontSpec::default(),
color: None,
background: None,
underline: Underline::None,
strikethrough: false,
link: None,
}
}
}
impl TextRun {
pub fn font(range: std::ops::Range<usize>, font: FontSpec) -> Self {
TextRun {
range,
font,
..TextRun::default()
}
}
pub fn style(&self) -> RunStyle {
RunStyle {
font: self.font,
color: self.color,
background: self.background,
underline: self.underline,
strikethrough: self.strikethrough,
link: self.link.clone(),
}
}
pub fn styled(range: std::ops::Range<usize>, style: RunStyle) -> Self {
TextRun {
range,
font: style.font,
color: style.color,
background: style.background,
underline: style.underline,
strikethrough: style.strikethrough,
link: style.link,
}
}
}
pub fn runs_to_markup(
text: &str,
runs: &[TextRun],
dialect: MarkupDialect,
base_points: f64,
) -> String {
let mut out = String::with_capacity(text.len() + runs.len() * 24);
let mut at = 0usize;
for r in runs {
let Some(styled) = text.get(r.range.clone()) else {
continue;
};
if r.range.start > at
&& let Some(plain) = text.get(at..r.range.start)
{
escape_markup_in(plain, &mut out, dialect);
}
open_run(r, dialect, base_points, &mut out);
escape_markup_in(styled, &mut out, dialect);
close_run(r, dialect, &mut out);
at = r.range.end;
}
if let Some(tail) = text.get(at..) {
escape_markup_in(tail, &mut out, dialect);
}
out
}
impl Symbol {
pub fn outline_path(self) -> Option<&'static str> {
use Symbol as S;
#[allow(unreachable_patterns)]
Some(match self {
S::Add => "M11 5h2v6h6v2h-6v6h-2v-6H5v-2h6z",
S::Remove => "M5 11h14v2H5z",
S::Delete => "M9 3h6l1 2h4v2H4V5h4zM6 8h12l-1 13H7z",
S::Edit => {
"M3 17.3V21h3.7L17.8 9.9l-3.7-3.7zM20.7 7a1 1 0 0 0 0-1.4l-2.3-2.3a1 1 0 0 0-1.4 0l-1.8 1.8 3.7 3.7z"
}
S::New => "M13 2H6v20h12V7h-5zm2 .5L19.5 7H15z",
S::Open => "M2 5h7l2 2h11v12H2z",
S::Save => "M3 3h13l5 5v13H3zm5 0h7v6H8zm-1 11h10v7H7z",
S::Print => "M7 3h10v4H7zM4 8h16v8h-3v6H7v-6H4z",
S::Refresh => "M12 4V1L8 5l4 4V6a6 6 0 1 1-6 6H4a8 8 0 1 0 8-8z",
S::Search => {
"M10 4a6 6 0 1 0 3.5 10.9l4.8 4.8 1.4-1.4-4.8-4.8A6 6 0 0 0 10 4m0 2a4 4 0 1 1 0 8 4 4 0 0 1 0-8"
}
S::Share => "M12 3l5 5h-3v7h-4V8H7zM5 17h14v4H5z",
S::Settings => {
"M9.9 4.7L10.2 2.2L13.8 2.2L14.1 4.7A7.6 7.6 0 0 1 15.7 5.4L17.7 3.8L20.2 6.3L18.6 8.3A7.6 7.6 0 0 1 19.3 9.9L21.8 10.2L21.8 13.8L19.3 14.1A7.6 7.6 0 0 1 18.6 15.7L20.2 17.7L17.7 20.2L15.7 18.6A7.6 7.6 0 0 1 14.1 19.3L13.8 21.8L10.2 21.8L9.9 19.3A7.6 7.6 0 0 1 8.3 18.6L6.3 20.2L3.8 17.7L5.4 15.7A7.6 7.6 0 0 1 4.7 14.1L2.2 13.8L2.2 10.2L4.7 9.9A7.6 7.6 0 0 1 5.4 8.3L3.8 6.3L6.3 3.8L8.3 5.4A7.6 7.6 0 0 1 9.9 4.7zM12 8.8a3.2 3.2 0 1 0 0 6.4a3.2 3.2 0 1 0 0-6.4z"
}
S::Info => "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20m-1 5h2v2h-2zm0 4h2v6h-2z",
S::Star => {
"M12 2l2.9 6.3 6.9.8-5.1 4.7 1.4 6.8L12 17.3 5.9 20.6l1.4-6.8L2.2 9.1l6.9-.8z"
}
S::Bookmark => "M6 2h12v20l-6-4-6 4z",
S::Back => "M15.4 5.4L14 4l-8 8 8 8 1.4-1.4L8.8 12z",
S::Forward => "M8.6 5.4L10 4l8 8-8 8-1.4-1.4L15.2 12z",
S::Up => "M5.4 15.4L4 14l8-8 8 8-1.4 1.4L12 8.8z",
S::Down => "M5.4 8.6L4 10l8 8 8-8-1.4-1.4L12 15.2z",
S::Home => "M12 3l9 8h-3v10h-5v-6h-2v6H6V11H3z",
S::Sidebar => "M3 4h18v16H3zm2 2v12h4V6zm6 0v12h8V6z",
S::Filter => "M3 5h18l-7 8v6l-4 2v-8z",
S::Sort => "M8 4v12H4l4 4 4-4H10V4zM14 6h7v2h-7zm0 4h5v2h-5zm0 4h3v2h-3z",
S::More => {
"M6 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4m6 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4m6 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4"
}
S::Play => "M8 5l11 7-11 7z",
S::Pause => "M7 5h3v14H7zm7 0h3v14h-3z",
S::Stop => "M6 6h12v12H6z",
S::Camera => {
"M9 3h6l1.5 2H20a2 2 0 0 1 2 2v12H2V7a2 2 0 0 1 2-2h3.5zm3 6a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9m0 1.6a2.9 2.9 0 1 1 0 5.8 2.9 2.9 0 0 1 0-5.8"
}
S::Code => {
"M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6zm5.2 0l4.6-4.6L14.6 7.4 16 6l6 6-6 6z"
}
S::Light => {
"M12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10M11 1h2v3h-2zm0 19h2v3h-2zM1 11h3v2H1zm19 0h3v2h-3zM3.5 4.9l1.4-1.4 2.1 2.1-1.4 1.4zm13.5 13.5l1.4-1.4 2.1 2.1-1.4 1.4zM4.9 20.5l-1.4-1.4 2.1-2.1 1.4 1.4zm13.5-13.5l-1.4-1.4 2.1-2.1 1.4 1.4z"
}
S::Dark => "M12 3a9 9 0 1 0 9 9 7 7 0 0 1-9-9",
S::Auto => "M12 2a10 10 0 0 0 0 20zM12 4a8 8 0 0 1 0 16v-1.6a6.4 6.4 0 0 0 0-12.8z",
S::ZoomIn => {
"M10 4a6 6 0 1 0 3.5 10.9l4.8 4.8 1.4-1.4-4.8-4.8A6 6 0 0 0 10 4M9 7h2v2h2v2h-2v2H9v-2H7V9h2z"
}
S::ZoomOut => {
"M10 4a6 6 0 1 0 3.5 10.9l4.8 4.8 1.4-1.4-4.8-4.8A6 6 0 0 0 10 4M7 9h6v2H7z"
}
S::ZoomReset => {
"M10 4a6 6 0 1 0 3.5 10.9l4.8 4.8 1.4-1.4-4.8-4.8A6 6 0 0 0 10 4M12 13h-1.7V9.3l-1.2.8-.9-1.3 2.4-1.8H12z"
}
S::Undo => "M12 5V2L7 7l5 5V9a5 5 0 1 1 0 10H8v2h4a7 7 0 0 0 0-14z",
S::Redo => "M12 5V2l5 5-5 5V9a5 5 0 1 0 0 10h4v2h-4a7 7 0 0 1 0-14z",
S::Copy => "M8 2h10v14H8zM4 6h2v14h12v2H4z",
S::Cut => {
"M9 4l5.5 9.5-1.2 2L7.8 6zM15 4L9.5 13.5l1.2 2L16.2 6zM6 16a3 3 0 1 0 0 6 3 3 0 0 0 0-6m12 0a3 3 0 1 0 0 6 3 3 0 0 0 0-6"
}
S::Paste => "M9 2h6v2h3v18H6V4h3zm0 2v2h6V4z",
S::Mail => "M2 5h20v14H2zm2 3.2V17h16V8.2l-8 5z",
S::Folder => "M2 5h7l2 2h11v12H2z",
S::Document => "M14 2H6v20h12V6zm.5.8L17.2 6H14.5z",
S::Check => "M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z",
S::Close => {
"M19 6.4L17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12z"
}
S::Warning => "M12 2l10 19H2zm-1 7h2v6h-2zm0 8h2v2h-2z",
S::Rectangle => "M3 5h18v14H3zm2 2v10h14V7z",
S::Oval => {
"M12 5c5 0 9 3.1 9 7s-4 7-9 7-9-3.1-9-7 4-7 9-7zm0 2c-3.9 0-7 2.2-7 5s3.1 5 7 5 7-2.2 7-5-3.1-5-7-5z"
}
S::Circle => "M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16zm0 2a6 6 0 1 1 0 12 6 6 0 0 1 0-12z",
S::CircleFilled => "M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16z",
S::Line => "M4.7 17.9l13.2-13.2 1.4 1.4L6.1 19.3z",
S::Group => "M8 4h12v12h-4V8H8zM4 8h12v12H4z",
S::Ungroup => "M3 3h8v8H3zM13 13h8v8h-8z",
_ => return None,
})
}
pub fn outline_svg(self) -> Option<String> {
self.outline_path().map(|d| {
format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">\
<path fill-rule=\"evenodd\" d=\"{d}\"/></svg>"
)
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MarkupDialect {
Pango,
QtHtml,
}
fn escape_markup_in(s: &str, out: &mut String, dialect: MarkupDialect) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
'\n' if dialect == MarkupDialect::QtHtml => out.push_str("<br>"),
_ => out.push(c),
}
}
}
fn escape_markup(s: &str, out: &mut String) {
escape_markup_in(s, out, MarkupDialect::Pango);
}
fn hex(c: Color) -> String {
format!(
"#{:02x}{:02x}{:02x}",
(c.r.clamp(0.0, 1.0) * 255.0) as u8,
(c.g.clamp(0.0, 1.0) * 255.0) as u8,
(c.b.clamp(0.0, 1.0) * 255.0) as u8
)
}
fn needs_span(r: &TextRun, dialect: MarkupDialect) -> bool {
let extras = r.color.is_some() || r.background.is_some() || r.font.scale != 1.0;
match dialect {
MarkupDialect::Pango => extras || r.font.monospace,
MarkupDialect::QtHtml => extras,
}
}
fn open_run(r: &TextRun, dialect: MarkupDialect, base_points: f64, out: &mut String) {
if let Some(url) = r.link.as_deref() {
out.push_str("<a href=\"");
escape_markup(url, out);
out.push_str("\">");
}
let color = r.color.map(hex);
let background = r.background.map(hex);
let mono = r.font.monospace;
if needs_span(r, dialect) {
match dialect {
MarkupDialect::Pango => {
out.push_str("<span");
if let Some(c) = &color {
out.push_str(&format!(" foreground=\"{c}\""));
}
if let Some(c) = &background {
out.push_str(&format!(" background=\"{c}\""));
}
if mono {
out.push_str(" font_family=\"monospace\"");
}
if r.font.scale != 1.0 {
let pts = r.font.resolved_points(base_points);
out.push_str(&format!(" size=\"{}\"", (pts * 1024.0).round() as i64));
}
out.push('>');
}
MarkupDialect::QtHtml => {
out.push_str("<span style=\"");
if let Some(c) = &color {
out.push_str(&format!("color:{c};"));
}
if let Some(c) = &background {
out.push_str(&format!("background-color:{c};"));
}
if r.font.scale != 1.0 {
let pt = r.font.resolved_points(base_points);
out.push_str(&format!("font-size:{pt}pt;"));
}
out.push_str("\">");
}
}
}
if dialect == MarkupDialect::QtHtml && mono {
out.push_str("<code>");
}
if r.font.weight.is_some_and(|w| w >= FontWeight::Semibold) {
out.push_str("<b>");
}
if r.font.italic {
out.push_str("<i>");
}
if r.underline.is_on() {
out.push_str("<u>");
}
if r.strikethrough {
out.push_str("<s>");
}
}
fn close_run(r: &TextRun, dialect: MarkupDialect, out: &mut String) {
if r.strikethrough {
out.push_str("</s>");
}
if r.underline.is_on() {
out.push_str("</u>");
}
if r.font.italic {
out.push_str("</i>");
}
if r.font.weight.is_some_and(|w| w >= FontWeight::Semibold) {
out.push_str("</b>");
}
if dialect == MarkupDialect::QtHtml && r.font.monospace {
out.push_str("</code>");
}
if needs_span(r, dialect) {
out.push_str("</span>");
}
if r.link.is_some() {
out.push_str("</a>");
}
}
pub fn runs_are_valid(text: &str, runs: &[TextRun]) -> Result<(), String> {
let mut prev_end = 0usize;
for (i, r) in runs.iter().enumerate() {
if r.range.start < prev_end {
return Err(format!(
"run {i} starts at {} but the previous run ends at {prev_end} — runs must be \
ascending and non-overlapping",
r.range.start
));
}
if r.range.end > text.len() {
return Err(format!(
"run {i} ends at {} but the text is {} bytes",
r.range.end,
text.len()
));
}
if r.range.start > r.range.end {
return Err(format!("run {i} has an inverted range {:?}", r.range));
}
if !text.is_char_boundary(r.range.start) || !text.is_char_boundary(r.range.end) {
return Err(format!(
"run {i} range {:?} splits a multi-byte character",
r.range
));
}
prev_end = r.range.end;
}
Ok(())
}
pub mod props {
use super::*;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ContainerProps {
pub background: Option<Color>,
pub corner_radius: f64,
pub clips: bool,
pub role: Option<super::SurfaceRole>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ContainerPatch {
Background(Option<Color>),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ScrollProps {
pub horizontal: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LabelProps {
pub text: String,
pub font: FontSpec,
pub color: Option<Color>,
pub role: TextRole,
pub wraps: bool,
pub max_lines: u32,
pub align: TextAlign,
pub runs: Vec<crate::TextRun>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextRole {
#[default]
Primary,
Secondary,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextAlign {
#[default]
Leading,
Center,
Trailing,
}
#[derive(Clone, Debug, PartialEq)]
pub enum LabelPatch {
Text(String),
Color(Option<Color>),
Font(FontSpec),
Runs(String, Vec<crate::TextRun>),
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum ButtonStyleSpec {
#[default]
Automatic,
Bordered,
Prominent,
Tinted(Color),
Compact,
Plain,
}
impl ButtonStyleSpec {
pub fn on_tint(fill: Color) -> Color {
let lin = |c: f64| {
let c = c.clamp(0.0, 1.0);
if c <= 0.04045 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
};
let l = 0.2126 * lin(fill.r) + 0.7152 * lin(fill.g) + 0.0722 * lin(fill.b);
let against_white = 1.05 / (l + 0.05);
let against_black = (l + 0.05) / 0.05;
if against_black >= against_white {
Color::BLACK
} else {
Color::WHITE
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ButtonContent {
pub title: String,
pub icon: Option<Icon>,
pub icon_only: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ButtonProps {
pub title: String,
pub icon: Option<Icon>,
pub icon_only: bool,
pub enabled: bool,
pub style: ButtonStyleSpec,
pub selected: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ButtonPatch {
Title(String),
Content(ButtonContent),
Enabled(bool),
Selected(bool),
Style(ButtonStyleSpec),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ToggleProps {
pub on: bool,
pub enabled: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum TogglePatch {
On(bool),
Enabled(bool),
}
#[derive(Clone, Debug, PartialEq)]
pub struct SliderProps {
pub value: f64,
pub min: f64,
pub max: f64,
pub step: Option<f64>,
pub enabled: bool,
}
impl Default for SliderProps {
fn default() -> Self {
SliderProps {
value: 0.0,
min: 0.0,
max: 1.0,
step: None,
enabled: true,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum SliderPatch {
Value(f64),
Enabled(bool),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PickerStyle {
#[default]
Menu,
Segmented,
Inline,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PickerProps {
pub separators_before: Vec<usize>,
pub options: Vec<String>,
pub selected: usize,
pub style: PickerStyle,
pub enabled: bool,
}
impl Default for PickerProps {
fn default() -> Self {
PickerProps {
separators_before: Vec::new(),
options: Vec::new(),
selected: 0,
style: PickerStyle::default(),
enabled: true,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum PickerPatch {
Selected(usize),
Options(Vec<String>),
Enabled(bool),
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextAreaProps {
pub text: String,
pub placeholder: String,
pub min_lines: u32,
pub max_lines: u32,
pub editable: bool,
pub selectable: bool,
pub spellcheck: bool,
pub submit_on_enter: bool,
}
impl Default for TextAreaProps {
fn default() -> Self {
TextAreaProps {
text: String::new(),
placeholder: String::new(),
min_lines: 1,
max_lines: 0,
editable: true,
selectable: true,
spellcheck: true,
submit_on_enter: false,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum TextAreaPatch {
SetText(String),
SetEditable(bool),
SetSelectable(bool),
SetSpellCheck(bool),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TextFieldProps {
pub text: String,
pub placeholder: String,
pub enabled: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum TextFieldPatch {
Text {
text: String,
from_native: bool,
},
Placeholder(String),
Enabled(bool),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ContentMode {
#[default]
Fit,
Fill,
Stretch,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ImageProps {
pub source: ImageSource,
pub decorative: bool,
pub content_mode: ContentMode,
pub aspect_ratio: Option<f64>,
pub tint: Option<Color>,
pub template: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ListItemProps {
pub title: String,
pub subtitle: String,
pub detail: String,
pub icon: Option<crate::Icon>,
pub icon_tint: Option<Color>,
pub checked: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ListItemPatch {
Title(String),
Subtitle(String),
Detail(String),
Icon(Option<crate::Icon>),
IconTint(Option<Color>),
Checked(bool),
}
#[derive(Clone, Debug, PartialEq)]
pub enum ImagePatch {
Tint(Option<Color>),
Source(ImageSource),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CanvasProps {
pub ops: Vec<DrawOp>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProgressProps {
pub value: Option<f64>,
pub animating: bool,
pub large: bool,
}
impl Default for ProgressProps {
fn default() -> Self {
Self {
value: None,
animating: true,
large: false,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ProgressPatch {
Animating(bool),
Value(Option<f64>),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct DividerProps {
pub vertical: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum NavPresentation {
Split,
#[default]
Stack,
Tabs,
Rail,
}
impl NavPresentation {
pub fn is_split(self) -> bool {
self == NavPresentation::Split
}
pub fn rows_are_chrome(self) -> bool {
matches!(self, NavPresentation::Tabs | NavPresentation::Rail)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct NavProps {
pub documents: Option<DocumentTabsConfig>,
pub title: String,
pub presentation: NavPresentation,
pub adaptive: bool,
pub sidebar_toggle: bool,
pub search: Option<SearchProps>,
pub list_width: Option<f64>,
pub list_visible: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DocumentTabsPatch {
pub native: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct DocumentTabsConfig {
pub new_label: String,
pub close_label: String,
pub can_add: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SearchPlacement {
#[default]
Automatic,
Toolbar,
Inline,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SearchProps {
pub text: String,
pub prompt: String,
pub placement: SearchPlacement,
pub hides_on_scroll: bool,
pub initially_hidden: bool,
pub scopes: Vec<String>,
pub scope: usize,
pub suggestions: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum SearchPatch {
Focus,
Text(String),
Scope(usize),
Suggestions(Vec<String>),
}
#[derive(Clone, Debug, PartialEq)]
pub enum NavPatch {
Pushed { title: String, immersive: bool },
Popped,
Title(String),
GuardTop(bool),
Presentation(NavPresentation),
Select(usize),
Reorder(Vec<usize>),
ListVisible(bool),
ListInStack(bool),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Pane {
Sidebar,
List,
#[default]
Detail,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct NavPageProps {
pub title: String,
pub pane: Pane,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CoverProps {
pub sheet: bool,
pub half: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum CoverPatch {
Present {
background: Option<Color>,
dismiss_disabled: bool,
},
DismissDisabled(bool),
Dismiss,
}
#[derive(Clone, Debug, PartialEq)]
pub struct InspectorProps {
pub visible: bool,
pub width: f64,
pub edge: PaneEdge,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PaneEdge {
#[default]
Trailing,
Leading,
}
#[derive(Clone, Debug, PartialEq)]
pub enum InspectorPatch {
Visible(bool),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct InspectorPaneProps {
pub panel: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SplitAxis {
#[default]
Horizontal,
Vertical,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SplitProps {
pub axis: SplitAxis,
pub fraction: f64,
pub min_pane: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub enum SplitPatch {
Axis(SplitAxis),
Fraction(f64),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SplitPaneProps {
pub second: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct NavMenuProps {
pub document_keys: Vec<String>,
pub items: Vec<String>,
pub icons: Vec<Option<String>>,
pub badges: Vec<Option<String>>,
pub badge_icons: Vec<Option<String>>,
pub badge_tints: Vec<Option<Color>>,
pub tints: Vec<Option<Color>>,
pub sections: Vec<Option<String>>,
pub menus: Vec<Vec<crate::MenuItem>>,
pub movable: Vec<bool>,
pub regroup: bool,
pub sorted: Vec<bool>,
pub selected: Option<usize>,
}
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum NavMenuPatch {
IconProgress(Vec<(usize, Option<f64>)>),
Selected(Option<usize>),
Items {
rows_changed: Option<Box<[usize]>>,
document_keys: Box<[String]>,
items: Vec<String>,
icons: Vec<Option<String>>,
badges: Vec<Option<String>>,
badge_icons: Vec<Option<String>>,
badge_tints: Vec<Option<Color>>,
sections: Vec<Option<String>>,
tints: Vec<Option<Color>>,
menus: Vec<Vec<crate::MenuItem>>,
movable: Vec<bool>,
sorted: Vec<bool>,
selected: Option<usize>,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum RowHeight {
Uniform(f64),
#[default]
Automatic,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RefreshPatch {
pub refreshing: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ListProps {
pub activatable: bool,
pub row_height: RowHeight,
pub selectable: bool,
pub multi_select: bool,
pub reorderable: bool,
pub deletable: bool,
pub delete_label: String,
pub swipe_actions: bool,
pub separators: Option<bool>,
pub editing: bool,
pub sections: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RowDelta {
Insert(usize),
Remove(usize),
Move(usize, usize),
}
#[derive(Clone, Debug, PartialEq)]
pub struct TreeProps {
pub section_headers: bool,
pub row_height: RowHeight,
pub selectable: bool,
pub multi_select: bool,
pub movable: bool,
pub indent: Option<f64>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum TreePatch {
Reload,
Expand(u64, bool),
Selected(Vec<u64>),
Reveal(u64),
}
#[derive(Clone, Debug, PartialEq)]
pub enum ListPatch {
Reload,
Splice(Vec<RowDelta>),
RowSizeInvalidated { cell: usize, height: f64 },
ScrollToEnd,
ScrollToRow(usize),
Selected(Vec<usize>),
Editing(bool),
}
}
pub mod present {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ButtonRole {
#[default]
Default,
Cancel,
Destructive,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PresentButton {
pub label: String,
pub role: ButtonRole,
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct FileFilter {
pub name: String,
pub extensions: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum PresentSpec {
Dialog {
title: String,
message: Option<String>,
buttons: Vec<PresentButton>,
sheet: bool,
},
Prompt {
title: String,
message: Option<String>,
placeholder: String,
initial: String,
ok: String,
cancel: String,
},
OpenFile {
title: String,
filters: Vec<FileFilter>,
multiple: bool,
},
OpenFolder { title: String },
SaveFile {
title: String,
suggested_name: String,
src_path: String,
filters: Vec<FileFilter>,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum PresentResult {
Button(i64),
Text(String),
Files(Vec<String>),
Dismissed,
}
pub const UNIT_SEP: char = '\u{1f}';
std::thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static APP_TEMP_DIR: std::cell::RefCell<Option<std::path::PathBuf>> =
const { std::cell::RefCell::new(None) };
}
pub fn set_app_temp_dir(dir: impl Into<std::path::PathBuf>) {
APP_TEMP_DIR.with(|d| *d.borrow_mut() = Some(dir.into()));
}
pub fn app_temp_dir() -> std::path::PathBuf {
APP_TEMP_DIR.with(|d| {
d.borrow().clone().unwrap_or_else(|| {
#[cfg(target_arch = "wasm32")]
{
std::path::PathBuf::from("/day-tmp")
}
#[cfg(not(target_arch = "wasm32"))]
std::env::temp_dir()
})
})
}
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
pub mod web_files {
std::thread_local! {
static FILES: std::cell::RefCell<std::collections::HashMap<String, Vec<u8>>> =
std::cell::RefCell::new(std::collections::HashMap::new());
}
pub fn write(path: &str, bytes: Vec<u8>) {
FILES.with(|f| f.borrow_mut().insert(path.to_string(), bytes));
}
pub fn read(path: &str) -> Option<Vec<u8>> {
FILES.with(|f| f.borrow().get(path).cloned())
}
pub fn remove(path: &str) {
FILES.with(|f| {
f.borrow_mut().remove(path);
});
}
}
impl PresentResult {
pub fn decode(tag: i32, index: i64, text: String) -> PresentResult {
match tag {
1 => PresentResult::Button(index),
2 => PresentResult::Text(text),
3 => PresentResult::Files(
text.split(UNIT_SEP)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect(),
),
_ => PresentResult::Dismissed,
}
}
}
impl PresentSpec {
pub fn title(&self) -> &str {
match self {
PresentSpec::Dialog { title, .. }
| PresentSpec::Prompt { title, .. }
| PresentSpec::OpenFile { title, .. }
| PresentSpec::OpenFolder { title }
| PresentSpec::SaveFile { title, .. } => title,
}
}
pub fn message(&self) -> Option<&str> {
match self {
PresentSpec::Dialog { message, .. } | PresentSpec::Prompt { message, .. } => {
message.as_deref()
}
_ => None,
}
}
pub fn buttons_joined(&self) -> String {
match self {
PresentSpec::Dialog { buttons, .. } => buttons
.iter()
.map(|b| b.label.as_str())
.collect::<Vec<_>>()
.join(&UNIT_SEP.to_string()),
PresentSpec::Prompt { ok, cancel, .. } => format!("{ok}{UNIT_SEP}{cancel}"),
_ => String::new(),
}
}
pub fn roles_joined(&self) -> String {
let roles: Vec<i32> = match self {
PresentSpec::Dialog { buttons, .. } => {
buttons.iter().map(|b| b.role as i32).collect()
}
PresentSpec::Prompt { .. } => {
vec![ButtonRole::Default as i32, ButtonRole::Cancel as i32]
}
_ => vec![],
};
roles
.iter()
.map(|r| r.to_string())
.collect::<Vec<_>>()
.join(",")
}
pub fn filters(&self) -> &[FileFilter] {
match self {
PresentSpec::OpenFile { filters, .. } | PresentSpec::SaveFile { filters, .. } => {
filters
}
_ => &[],
}
}
pub fn multiple(&self) -> bool {
matches!(self, PresentSpec::OpenFile { multiple: true, .. })
}
pub fn suggested_name(&self) -> &str {
match self {
PresentSpec::SaveFile { suggested_name, .. } => suggested_name,
_ => "",
}
}
pub fn src_path(&self) -> &str {
match self {
PresentSpec::SaveFile { src_path, .. } => src_path,
_ => "",
}
}
pub fn filters_joined(&self) -> String {
self.filters()
.iter()
.map(|f| format!("{}|{}", f.name, f.extensions.join(",")))
.collect::<Vec<_>>()
.join(&UNIT_SEP.to_string())
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FrameStamp {
pub timestamp: f64,
pub target_timestamp: Option<f64>,
}
impl FrameStamp {
pub fn new(timestamp: f64) -> Self {
Self {
timestamp,
target_timestamp: None,
}
}
}
pub type FrameCallback = Box<dyn FnOnce(FrameStamp)>;
pub type CancelFrame = Box<dyn FnOnce()>;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct RowMetrics {
pub insets: Insets,
pub min_height: f64,
}
pub trait Toolkit: Sized + 'static {
type Handle: Clone + 'static;
fn request_frame(&mut self, _host: &Self::Handle, _cb: FrameCallback) -> CancelFrame {
Box::new(|| {})
}
fn capability(&self, _cap: Cap) -> Support {
Support::Unsupported
}
fn realize(&mut self, kind: PieceKind, props: &dyn Any, id: NodeId) -> Self::Handle;
fn update(
&mut self,
h: &Self::Handle,
kind: PieceKind,
patch: &dyn Any,
anim: Option<&AnimSpec>,
);
fn release(&mut self, h: Self::Handle);
fn release_piece(&mut self, _kind: PieceKind, _h: &Self::Handle) {}
fn insert(&mut self, parent: &Self::Handle, child: &Self::Handle, index: usize);
fn remove(&mut self, parent: &Self::Handle, child: &Self::Handle);
fn move_child(&mut self, parent: &Self::Handle, child: &Self::Handle, to: usize);
fn measure(&mut self, h: &Self::Handle, kind: PieceKind, p: Proposal) -> Size;
fn first_baseline(&mut self, _h: &Self::Handle, _kind: PieceKind, _size: Size) -> Option<f64> {
None
}
fn set_frame(&mut self, h: &Self::Handle, frame: Rect, anim: Option<&AnimSpec>);
fn set_opacity(&mut self, _h: &Self::Handle, _opacity: f64, _anim: Option<&AnimSpec>) {}
fn set_transform(
&mut self,
_h: &Self::Handle,
_t: Transform,
_size: Size,
_anim: Option<&AnimSpec>,
) {
}
fn set_selectable(&mut self, _h: &Self::Handle, _selectable: bool) -> Option<Self::Handle> {
None
}
fn set_input_traits(&mut self, h: &Self::Handle, traits: &InputTraits) -> Option<Self::Handle>;
fn set_cursor(&mut self, _h: &Self::Handle, _cursor: Cursor) {}
fn set_presented(&mut self, _h: &Self::Handle, _visible: bool) {}
fn attach_refresh(&mut self, _h: &Self::Handle, _host: NodeId) {}
fn set_scroll_content(&mut self, _h: &Self::Handle, _content: Size) {}
fn scroll_bar_inset(&mut self, _h: &Self::Handle, _vertical: bool) -> f64 {
0.0
}
fn list_row_metrics(&mut self, _h: &Self::Handle) -> RowMetrics {
RowMetrics::default()
}
fn scroll_to(&mut self, _h: &Self::Handle, _target: Rect, _animated: bool) {}
fn nav_scroll_to_edge(&mut self, _h: &Self::Handle, _bottom: bool) -> bool {
false
}
fn scroll_offset(&mut self, _h: &Self::Handle) -> Point {
Point::ZERO
}
fn set_event_sink(&mut self, sink: EventSink);
fn enable_gesture(&mut self, _h: &Self::Handle, _node: NodeId, _kind: GestureKind) {}
fn focus(&mut self, _h: &Self::Handle, _node: NodeId, _focused: bool) {}
fn set_focusable(&mut self, _h: &Self::Handle, _node: NodeId, _focusable: bool) {}
fn attach_list(&mut self, _host: &Self::Handle, _source: ListSource) {}
fn attach_tree(&mut self, _host: &Self::Handle, _source: TreeSource) {}
fn set_route(&mut self, _route: &str) {}
fn set_undo_state(&mut self, _state: &UndoState) {}
fn set_edit_state(&mut self, _state: &EditState) {}
fn modifiers(&mut self) -> Modifiers {
Modifiers::default()
}
fn set_drag_source(&mut self, _h: &Self::Handle, _source: transfer::Source) {}
fn set_drop_target(&mut self, _h: &Self::Handle, _target: transfer::Target) {}
fn set_context_menu_fn(&mut self, _h: &Self::Handle, _node: NodeId, _f: ContextMenuFn) {}
fn set_app_menu(&mut self, _items: &[MenuItem]) {}
fn set_context_menu(&mut self, _h: &Self::Handle, _node: NodeId, _items: &[MenuItem]) {}
fn edit_toolbar(&mut self, _h: &Self::Handle, _ops: &[ToolbarOp]) -> bool {
true
}
fn update_toolbar(&mut self, _h: &Self::Handle, _patch: &ToolbarPatch) {}
fn supports_lifecycle(&self, phase: Lifecycle) -> bool {
phase.is_universal()
}
fn set_a11y(&mut self, _h: &Self::Handle, _a11y: &A11yProps) {}
fn announce(&mut self, _text: &str, _urgent: bool) {}
fn read_native(&self, _h: &Self::Handle) -> NativeSnapshot {
NativeSnapshot::default()
}
fn replay(&mut self, _h: &Self::Handle, _ops: &[DrawOp], _size: Size) {}
fn font_families(&mut self) -> Vec<FontFamilyInfo> {
Vec::new()
}
fn measure_text(&mut self, _text: &str, _size: f64, _font: &CanvasFont) -> Option<TextMetrics> {
None
}
fn decode_image(&mut self, _req: u64, _id: BitmapId, _bytes: &[u8]) {}
fn image_info(&mut self, _id: BitmapId) -> Option<BitmapInfo> {
None
}
fn image_properties(&mut self, _id: BitmapId) -> Option<ImageProperties> {
None
}
fn encode_image(&mut self, _req: u64, _id: BitmapId, _spec: &EncodeSpec) {}
fn encode_formats(&mut self) -> Vec<ImageFormat> {
Vec::new()
}
fn release_image(&mut self, _id: BitmapId) {}
fn render_bitmap(
&mut self,
_req: u64,
_id: BitmapId,
_ops: &[DrawOp],
_size: Size,
_scale: f64,
) {
}
fn render_pdf(&mut self, _req: u64, _pages: &[PdfPage], _info: &PdfInfo) {}
fn snapshot_window(&mut self) -> Result<Vec<u8>, String> {
Err("snapshot unsupported".into())
}
fn snapshot_origin(&mut self, _root: &Self::Handle) -> Option<Point> {
None
}
fn prepare_snapshot(
&mut self,
_host: Option<&Self::Handle>,
_revision: u32,
) -> Result<capture::Readiness, String> {
Ok(capture::Readiness::OnCapture)
}
fn snapshot_window_chrome(&mut self) -> Result<Vec<u8>, String> {
self.snapshot_window()
}
fn toggle_sidebar(&mut self, _host: &Self::Handle) -> bool {
false
}
fn native_back(&mut self) -> bool {
false
}
fn ui_idle(&mut self) -> bool {
true
}
fn present(&mut self, _req: u64, _spec: &present::PresentSpec) {}
fn dismiss(&mut self, _req: u64) {}
fn open_url(&mut self, _url: &str) {}
fn share_support(&self) -> Support {
Support::Unsupported
}
fn share_url(&mut self, _url: &str, _title: &str) -> bool {
false
}
fn application_handlers(
&mut self,
_query: &applications::HandlerQuery,
) -> Result<applications::ApplicationHandlers, applications::ApplicationError> {
Err(applications::ApplicationError::Unsupported)
}
fn open_url_with_application(
&mut self,
_url: &str,
_application: &str,
completion: applications::OpenApplicationCompletion,
) {
completion(Err(applications::ApplicationError::Unsupported));
}
fn defer_system_gestures(&mut self, _edges: Edges) {}
fn set_status_bar_hidden(&mut self, _hidden: bool) {}
fn dark_mode(&mut self) -> bool {
std::env::var("DAY_THEME").ok().as_deref() == Some("dark")
}
fn reduce_motion(&mut self) -> bool {
false
}
fn set_appearance(&mut self, _dark: Option<bool>) {}
fn set_app_badge(&mut self, _badge: &AppBadge) {}
fn on_suspend(&mut self) {}
fn on_resume(&mut self) {}
fn on_memory_warning(&mut self) {}
fn adopt(&mut self, _raw: RawHandle) -> Self::Handle {
unimplemented!("this toolkit does not adopt foreign handles yet")
}
fn open_window(
&mut self,
_id: NodeId,
_options: &WindowOptions,
_kind: WindowKind,
) -> WindowOpenReply<Self::Handle> {
WindowOpenReply::Unsupported
}
fn close_window(&mut self, _host: &Self::Handle) {}
fn quit_app(&mut self) {}
fn focus_window(&mut self, _host: &Self::Handle) {}
fn set_window_title(&mut self, _host: &Self::Handle, _title: &str) {}
fn fit_window(&mut self, _host: &Self::Handle, _size: Size) {}
fn snapshot_window_of(&mut self, _host: &Self::Handle) -> Result<Vec<u8>, String> {
self.snapshot_window()
}
fn apply_window(&mut self, _host: &Self::Handle, _change: &WindowChange) {}
fn native_window_tabs(&self) -> bool {
false
}
fn native_document_tabs(&self) -> bool {
false
}
fn group_windows(&mut self, _hosts: &[Self::Handle]) {}
fn window_tab_order(&self, _host: &Self::Handle) -> Vec<NodeId> {
Vec::new()
}
fn set_dock_menu(&mut self, _items: &[MenuItem]) {}
fn set_drag_region(&mut self, _h: &Self::Handle, _drag: bool) {}
fn set_status_items(&mut self, _items: &[StatusItemSpec]) {}
fn set_app_progress(&mut self, _progress: AppProgress) {}
fn set_dock_visible(&mut self, _visible: bool) {}
fn set_keep_running(&mut self, _keep: bool) {}
fn monitors(&self) -> Vec<Monitor> {
Vec::new()
}
fn window_frame(&self, _host: &Self::Handle) -> Option<Rect> {
None
}
fn set_launcher_shortcuts(&mut self, _shortcuts: &[LauncherShortcut]) {}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum WindowKind {
Normal,
Preferences,
About,
}
impl WindowKind {
pub fn is_panel(self) -> bool {
!matches!(self, WindowKind::Normal)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum WindowRole {
#[default]
Primary,
Secondary,
}
impl From<WindowKind> for WindowRole {
fn from(kind: WindowKind) -> WindowRole {
match kind {
WindowKind::Normal => WindowRole::Primary,
WindowKind::Preferences | WindowKind::About => WindowRole::Secondary,
}
}
}
pub enum WindowOpenReply<H> {
Open(H),
Pending,
Unsupported,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum WindowState {
#[default]
Normal,
Minimized,
Maximized,
Fullscreen,
}
impl WindowState {
pub const fn code(self) -> i32 {
match self {
WindowState::Normal => 0,
WindowState::Minimized => 1,
WindowState::Maximized => 2,
WindowState::Fullscreen => 3,
}
}
pub const fn from_code(code: i32) -> WindowState {
match code {
1 => WindowState::Minimized,
2 => WindowState::Maximized,
3 => WindowState::Fullscreen,
_ => WindowState::Normal,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum WindowLevel {
#[default]
Normal,
Floating,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Attention {
#[default]
Informational,
Critical,
}
#[derive(Clone, Debug, PartialEq)]
pub enum WindowChange {
State(WindowState),
ContentProtected(bool),
Frame {
origin: Option<Point>,
size: Option<Size>,
},
Limits {
min: Option<Size>,
max: Option<Size>,
},
Level(WindowLevel),
OnAllWorkspaces(bool),
SkipTaskbar(bool),
Resizable(bool),
Minimizable(bool),
Maximizable(bool),
Closable(bool),
Visible(bool),
Appearance(Option<bool>),
RequestAttention(Attention),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum WindowChrome {
#[default]
Standard,
Overlay,
Frameless,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum WindowBackground {
#[default]
Opaque,
Transparent,
Material(WindowMaterial),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum WindowMaterial {
Window,
Sidebar,
Transient,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum WindowPlacement {
#[default]
Automatic,
Centered,
At(Point),
}
#[derive(Clone, PartialEq)]
pub struct StatusImage {
width: u32,
height: u32,
scale: f64,
rgba: std::sync::Arc<[u8]>,
}
impl StatusImage {
pub fn rgba(
width: u32,
height: u32,
scale: f64,
pixels: impl Into<std::sync::Arc<[u8]>>,
) -> Option<Self> {
let rgba = pixels.into();
let expected = (width as usize)
.checked_mul(height as usize)?
.checked_mul(4)?;
if width == 0
|| height == 0
|| expected > 16 * 1024 * 1024
|| rgba.len() != expected
|| !scale.is_finite()
|| scale <= 0.0
|| f64::from(width) / scale > 4096.0
|| f64::from(height) / scale > 4096.0
{
return None;
}
Some(Self {
width,
height,
scale,
rgba,
})
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn scale(&self) -> f64 {
self.scale
}
pub fn pixels(&self) -> &[u8] {
&self.rgba
}
pub fn size(&self) -> Size {
Size::new(
f64::from(self.width) / self.scale,
f64::from(self.height) / self.scale,
)
}
}
impl std::fmt::Debug for StatusImage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StatusImage")
.field("width", &self.width)
.field("height", &self.height)
.field("scale", &self.scale)
.field("bytes", &self.rgba.len())
.finish()
}
}
#[cfg(test)]
mod status_image_tests {
use super::*;
#[test]
fn validates_pixel_layout_and_logical_size() {
let image = StatusImage::rgba(120, 36, 2.0, vec![255; 120 * 36 * 4]).unwrap();
assert_eq!(image.size(), Size::new(60.0, 18.0));
assert_eq!(image.clone(), image);
assert!(StatusImage::rgba(0, 1, 1.0, vec![]).is_none());
assert!(StatusImage::rgba(1, 1, 1.0, vec![0; 3]).is_none());
assert!(StatusImage::rgba(u32::MAX, u32::MAX, 1.0, vec![]).is_none());
for scale in [0.0, -1.0, f64::NAN, f64::INFINITY, 0.00001] {
assert!(StatusImage::rgba(1, 1, scale, vec![0; 4]).is_none());
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct StatusItemSpec {
pub id: String,
pub icon: Option<Icon>,
pub raster: Option<StatusImage>,
pub template: bool,
pub title: String,
pub tooltip: String,
pub menu: Vec<MenuItem>,
pub activate: u64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Monitor {
pub id: String,
pub name: String,
pub frame: Rect,
pub work_area: Rect,
pub scale: f64,
pub primary: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LauncherShortcut {
pub route: String,
pub label: String,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum AppProgress {
#[default]
None,
Indeterminate,
Value(f64),
Paused(f64),
Error(f64),
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub enum WindowTabbing {
#[default]
Automatic,
Group(String),
Preferred(String),
Disallowed,
}
pub type AppLocales = (&'static str, &'static [(&'static str, &'static str)]);
#[derive(Clone, Debug)]
pub struct WindowOptions {
pub title: String,
pub size: Size,
pub min_size: Option<Size>,
pub start_hidden: bool,
pub size_to_fit: bool,
pub app_name: Option<String>,
pub locales: Option<AppLocales>,
pub title_fn: Option<fn() -> String>,
pub version: Option<String>,
pub tabbing: WindowTabbing,
pub chrome: WindowChrome,
pub background: WindowBackground,
pub shadow: bool,
pub resizable: bool,
pub placement: WindowPlacement,
pub max_size: Option<Size>,
pub remember_frame: Option<String>,
pub key: Option<String>,
pub kind: WindowKind,
}
impl WindowOptions {
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self.title_fn = None;
self
}
pub fn size(mut self, width: f64, height: f64) -> Self {
self.size = Size::new(width, height);
self
}
pub fn min_size(mut self, width: f64, height: f64) -> Self {
self.min_size = Some(Size::new(width, height));
self
}
pub fn max_size(mut self, width: f64, height: f64) -> Self {
self.max_size = Some(Size::new(width, height));
self
}
pub fn start_hidden(mut self) -> Self {
self.start_hidden = true;
self
}
pub fn size_to_fit(mut self) -> Self {
self.size_to_fit = true;
self
}
pub fn app_name(mut self, name: impl Into<String>) -> Self {
self.app_name = Some(name.into());
self
}
pub fn locales(
mut self,
default: &'static str,
catalog: &'static [(&'static str, &'static str)],
) -> Self {
self.locales = Some((default, catalog));
self
}
pub fn title_fn(mut self, f: fn() -> String) -> Self {
self.title_fn = Some(f);
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn tabbing(mut self, tabbing: WindowTabbing) -> Self {
self.tabbing = tabbing;
self
}
pub fn chrome(mut self, chrome: WindowChrome) -> Self {
self.chrome = chrome;
self
}
pub fn background(mut self, background: WindowBackground) -> Self {
self.background = background;
self
}
pub fn no_shadow(mut self) -> Self {
self.shadow = false;
self
}
pub fn fixed_size(mut self) -> Self {
self.resizable = false;
self
}
pub fn placement(mut self, placement: WindowPlacement) -> Self {
self.placement = placement;
self
}
pub fn remember_frame(mut self, key: impl Into<String>) -> Self {
self.remember_frame = Some(key.into());
self
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn kind(mut self, kind: WindowKind) -> Self {
self.kind = kind;
self
}
}
impl Default for WindowOptions {
fn default() -> Self {
WindowOptions {
title: "Day".into(),
size: Size::new(480.0, 640.0),
min_size: None,
start_hidden: false,
size_to_fit: false,
app_name: None,
locales: None,
title_fn: None,
version: None,
tabbing: WindowTabbing::Automatic,
chrome: WindowChrome::Standard,
background: WindowBackground::Opaque,
shadow: true,
resizable: true,
placement: WindowPlacement::Automatic,
max_size: None,
remember_frame: None,
key: None,
kind: WindowKind::Normal,
}
}
}
pub trait Platform: Toolkit {
const TARGET: &'static str;
const TOOLKIT: &'static str;
const RUN_ENDS_APP: bool = true;
fn run(self, options: WindowOptions, ready: Box<dyn FnOnce(Self, Self::Handle, Size)>);
fn post(f: Box<dyn FnOnce() + Send>);
fn post_delayed(ms: u32, f: Box<dyn FnOnce() + Send>) {
static SAID: std::sync::Once = std::sync::Once::new();
SAID.call_once(|| {
log::warn!(
"day: {} has no native timer door; `day::sleep` falls back to a helper thread \
per delay (implement `Platform::post_delayed`)",
Self::TARGET
);
});
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(u64::from(ms)));
Self::post(f);
});
}
fn locale_hints(&self) -> Vec<String> {
Vec::new()
}
}
pub fn capture_scale() -> Option<f64> {
std::env::var("DAY_CAPTURE_SCALE")
.ok()
.and_then(|v| v.trim().parse::<f64>().ok())
.filter(|s| s.is_finite() && (0.5..=4.0).contains(s))
}
pub fn frame_timestamp() -> f64 {
static ORIGIN: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
ORIGIN
.get_or_init(std::time::Instant::now)
.elapsed()
.as_secs_f64()
}
pub fn posix_locale_hints() -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut push = |tag: &str| {
let tag = tag.split('.').next().unwrap_or(tag).replace('_', "-");
if tag.is_empty() || tag == "C" || tag == "POSIX" {
return;
}
if !out.contains(&tag) {
out.push(tag);
}
};
if let Ok(list) = std::env::var("LANGUAGE") {
for tag in list.split(':') {
push(tag);
}
}
for key in ["LC_ALL", "LC_MESSAGES", "LANG"] {
if let Ok(v) = std::env::var(key) {
push(&v);
}
}
out
}
pub type MeasureFn<B> = fn(&mut B, &<B as Toolkit>::Handle, Proposal) -> Size;
pub struct Renderer<B: Toolkit> {
pub kind: PieceKind,
pub make: fn(&mut B, &dyn Any, NodeId) -> B::Handle,
pub update: fn(&mut B, &B::Handle, &dyn Any),
pub measure: Option<MeasureFn<B>>,
pub release: Option<fn(&mut B, &B::Handle)>,
}
pub struct Registry<B: Toolkit> {
map: HashMap<PieceKind, Renderer<B>>,
}
impl<B: Toolkit> Default for Registry<B> {
fn default() -> Self {
Registry {
map: HashMap::new(),
}
}
}
impl<B: Toolkit> Registry<B> {
pub fn register(&mut self, r: Renderer<B>) {
let kind = r.kind;
if self.map.insert(kind, r).is_some() {
debug_assert!(
false,
"duplicate renderer registered for piece kind {kind:?}"
);
log::warn!("duplicate renderer for piece kind {kind:?} — later registration wins");
}
}
pub fn get(&self, kind: PieceKind) -> Option<&Renderer<B>> {
self.map.get(kind)
}
pub fn kinds(&self) -> impl Iterator<Item = PieceKind> + '_ {
self.map.keys().copied()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(i32)]
pub enum OpCode {
FillRect = 0,
StrokeRect = 1,
FillRrect = 2,
FillEllipse = 3,
StrokeEllipse = 4,
StrokeArc = 5,
Line = 6,
Text = 7,
Save = 8,
Restore = 9,
Concat = 10,
FillPolygon = 11,
StrokePolygon = 12,
StrokeRrect = 13,
SetGradient = 14,
FillPath = 15,
StrokePath = 16,
Clip = 17,
StrokeStyle = 18,
SetFont = 19,
Stamp = 20,
StampPoints = 21,
Image = 22,
}
impl OpCode {
pub const ALL: [OpCode; 23] = [
OpCode::FillRect,
OpCode::StrokeRect,
OpCode::FillRrect,
OpCode::FillEllipse,
OpCode::StrokeEllipse,
OpCode::StrokeArc,
OpCode::Line,
OpCode::Text,
OpCode::Save,
OpCode::Restore,
OpCode::Concat,
OpCode::FillPolygon,
OpCode::StrokePolygon,
OpCode::StrokeRrect,
OpCode::SetGradient,
OpCode::FillPath,
OpCode::StrokePath,
OpCode::Clip,
OpCode::StrokeStyle,
OpCode::SetFont,
OpCode::Stamp,
OpCode::StampPoints,
OpCode::Image,
];
pub fn from_wire(n: f64) -> Option<OpCode> {
OpCode::ALL.into_iter().find(|c| *c as i32 as f64 == n)
}
}
pub fn geometry_key(shape: &Shape) -> f64 {
fn mix(h: &mut u64, v: f64) {
for byte in v.to_bits().to_le_bytes() {
*h ^= u64::from(byte);
*h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
}
fn mix_point(h: &mut u64, p: Point) {
mix(h, p.x);
mix(h, p.y);
}
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
match shape {
Shape::Polygon(pts) => {
mix(&mut h, 11.0);
for p in pts {
mix_point(&mut h, *p);
}
}
Shape::Path(path) => {
mix(&mut h, 15.0);
mix(&mut h, rule_bits(path.rule));
for seg in &path.segs {
match seg {
PathSeg::Move(a) => {
mix(&mut h, 0.0);
mix_point(&mut h, *a);
}
PathSeg::Line(a) => {
mix(&mut h, 1.0);
mix_point(&mut h, *a);
}
PathSeg::Quad(c, a) => {
mix(&mut h, 2.0);
mix_point(&mut h, *c);
mix_point(&mut h, *a);
}
PathSeg::Cubic(c1, c2, a) => {
mix(&mut h, 3.0);
mix_point(&mut h, *c1);
mix_point(&mut h, *c2);
mix_point(&mut h, *a);
}
PathSeg::Close => mix(&mut h, 4.0),
}
}
}
_ => return 0.0,
}
let masked = h & ((1u64 << 53) - 1);
if masked == 0 { 1.0 } else { masked as f64 }
}
pub fn encode_path(path: &Path) -> String {
let mut out = String::with_capacity(path.segs.len() * 16);
for seg in &path.segs {
if !out.is_empty() {
out.push(' ');
}
match seg {
PathSeg::Move(p) => out.push_str(&format!("M {} {}", p.x, p.y)),
PathSeg::Line(p) => out.push_str(&format!("L {} {}", p.x, p.y)),
PathSeg::Quad(c, p) => out.push_str(&format!("Q {} {} {} {}", c.x, c.y, p.x, p.y)),
PathSeg::Cubic(a, b, p) => out.push_str(&format!(
"C {} {} {} {} {} {}",
a.x, a.y, b.x, b.y, p.x, p.y
)),
PathSeg::Close => out.push('Z'),
}
}
out
}
fn rule_bits(rule: FillRule) -> f64 {
match rule {
FillRule::NonZero => 0.0,
FillRule::EvenOdd => 1.0,
}
}
pub fn encode_ops(ops: &[DrawOp]) -> (Vec<f64>, Vec<String>) {
fn color_bits(c: Color) -> f64 {
let r = (c.r.clamp(0.0, 1.0) * 255.0) as u32;
let g = (c.g.clamp(0.0, 1.0) * 255.0) as u32;
let b = (c.b.clamp(0.0, 1.0) * 255.0) as u32;
let a = (c.a.clamp(0.0, 1.0) * 255.0) as u32;
((a << 24) | (r << 16) | (g << 8) | b) as f64
}
#[allow(clippy::too_many_arguments)]
fn push(
k: OpCode,
a: f64,
b: f64,
c: f64,
d: f64,
e: f64,
f: f64,
g: f64,
col: Color,
nums: &mut Vec<f64>,
) {
nums.extend_from_slice(&[k as i32 as f64, a, b, c, d, e, f, g, color_bits(col)]);
}
fn stroke_style_record(style: &StrokeStyle, nums: &mut Vec<f64>, texts: &mut Vec<String>) {
if style.is_plain() {
return;
}
push(
OpCode::StrokeStyle,
match style.cap {
LineCap::Butt => 0.0,
LineCap::Round => 1.0,
LineCap::Square => 2.0,
},
match style.join {
LineJoin::Miter => 0.0,
LineJoin::Round => 1.0,
LineJoin::Bevel => 2.0,
},
style.miter_limit,
style.dash_phase,
style.dash.len() as f64,
0.0,
0.0,
Color::CLEAR,
nums,
);
texts.push(
style
.dash
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(" "),
);
}
fn shape_record(
stroke: bool,
shape: &Shape,
w: f64,
col: Color,
nums: &mut Vec<f64>,
texts: &mut Vec<String>,
) {
match shape {
Shape::Rect(r) => push(
if stroke {
OpCode::StrokeRect
} else {
OpCode::FillRect
},
r.origin.x,
r.origin.y,
r.size.width,
r.size.height,
0.0,
0.0,
w,
col,
nums,
),
Shape::RoundedRect(r, rad) => push(
if stroke {
OpCode::StrokeRrect
} else {
OpCode::FillRrect
},
r.origin.x,
r.origin.y,
r.size.width,
r.size.height,
*rad,
0.0,
w,
col,
nums,
),
Shape::Ellipse(r) => push(
if stroke {
OpCode::StrokeEllipse
} else {
OpCode::FillEllipse
},
r.origin.x,
r.origin.y,
r.size.width,
r.size.height,
0.0,
0.0,
w,
col,
nums,
),
Shape::Arc {
rect,
start_deg,
sweep_deg,
} => push(
OpCode::StrokeArc,
rect.origin.x,
rect.origin.y,
rect.size.width,
rect.size.height,
*start_deg,
*sweep_deg,
w,
col,
nums,
),
Shape::Line(p1, p2) => {
push(OpCode::Line, p1.x, p1.y, p2.x, p2.y, 0.0, 0.0, w, col, nums)
}
Shape::Polygon(pts) => {
push(
if stroke {
OpCode::StrokePolygon
} else {
OpCode::FillPolygon
},
geometry_key(shape),
0.0,
0.0,
0.0,
0.0,
0.0,
w,
col,
nums,
);
texts.push(
pts.iter()
.map(|p| format!("{},{}", p.x, p.y))
.collect::<Vec<_>>()
.join(" "),
);
}
Shape::Path(path) => {
push(
if stroke {
OpCode::StrokePath
} else {
OpCode::FillPath
},
geometry_key(shape),
0.0,
0.0,
0.0,
0.0,
rule_bits(path.rule),
w,
col,
nums,
);
texts.push(encode_path(path));
}
}
}
fn gradient_record(
geo: [f64; 4],
kind: f64,
stops: &[(f64, Color)],
nums: &mut Vec<f64>,
texts: &mut Vec<String>,
) {
push(
OpCode::SetGradient,
geo[0],
geo[1],
geo[2],
geo[3],
stops.len() as f64,
kind,
0.0,
Color::CLEAR,
nums,
);
texts.push(
stops
.iter()
.map(|(o, c)| format!("{o},{:08x}", color_bits(*c) as u32))
.collect::<Vec<_>>()
.join(" "),
);
}
let mut nums = Vec::with_capacity(ops.len() * 9);
let mut texts = Vec::new();
for op in ops {
match op {
DrawOp::Fill(shape, paint) => {
let col = match paint {
Paint::Solid(c) => *c,
Paint::Linear(g) => {
gradient_record(
[g.start.x, g.start.y, g.end.x, g.end.y],
0.0,
&g.stops,
&mut nums,
&mut texts,
);
Color::WHITE
}
Paint::Radial(g) => {
gradient_record(
[g.center.x, g.center.y, g.radius, 0.0],
1.0,
&g.stops,
&mut nums,
&mut texts,
);
Color::WHITE
}
};
shape_record(false, shape, 0.0, col, &mut nums, &mut texts);
}
DrawOp::Stamp(st) => {
if st.at.is_empty() {
continue;
}
let col = match &st.paint {
Paint::Solid(c) => *c,
Paint::Linear(g) => {
gradient_record(
[g.start.x, g.start.y, g.end.x, g.end.y],
0.0,
&g.stops,
&mut nums,
&mut texts,
);
Color::WHITE
}
Paint::Radial(g) => {
gradient_record(
[g.center.x, g.center.y, g.radius, 0.0],
1.0,
&g.stops,
&mut nums,
&mut texts,
);
Color::WHITE
}
};
push(
OpCode::Stamp,
st.at.len() as f64,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
Color::CLEAR,
&mut nums,
);
for chunk in st.at.chunks(4) {
let mut rec = [
OpCode::StampPoints as i32 as f64,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
];
for (i, p) in chunk.iter().enumerate() {
rec[1 + i * 2] = p.x;
rec[2 + i * 2] = p.y;
}
nums.extend_from_slice(&rec);
}
match &st.stroke {
None => shape_record(false, &st.shape, 0.0, col, &mut nums, &mut texts),
Some(style) => {
stroke_style_record(style, &mut nums, &mut texts);
shape_record(true, &st.shape, style.width, col, &mut nums, &mut texts);
}
}
}
DrawOp::Stroke(shape, paint, style) => {
stroke_style_record(style, &mut nums, &mut texts);
let col = match paint {
Paint::Solid(c) => *c,
Paint::Linear(g) => {
gradient_record(
[g.start.x, g.start.y, g.end.x, g.end.y],
0.0,
&g.stops,
&mut nums,
&mut texts,
);
Color::WHITE
}
Paint::Radial(g) => {
gradient_record(
[g.center.x, g.center.y, g.radius, 0.0],
1.0,
&g.stops,
&mut nums,
&mut texts,
);
Color::WHITE
}
};
shape_record(true, shape, style.width, col, &mut nums, &mut texts);
}
DrawOp::Clip(shape) => {
let (kind, geo, extra, payload): (f64, [f64; 4], f64, Option<String>) = match shape
{
Shape::Rect(r) => (
0.0,
[r.origin.x, r.origin.y, r.size.width, r.size.height],
0.0,
None,
),
Shape::RoundedRect(r, rad) => (
1.0,
[r.origin.x, r.origin.y, r.size.width, r.size.height],
*rad,
None,
),
Shape::Ellipse(r) => (
2.0,
[r.origin.x, r.origin.y, r.size.width, r.size.height],
0.0,
None,
),
Shape::Path(p) => (
3.0,
[geometry_key(shape), 0.0, 0.0, 0.0],
rule_bits(p.rule),
Some(encode_path(p)),
),
Shape::Polygon(pts) => (
4.0,
[geometry_key(shape), 0.0, 0.0, 0.0],
0.0,
Some(
pts.iter()
.map(|p| format!("{},{}", p.x, p.y))
.collect::<Vec<_>>()
.join(" "),
),
),
Shape::Line(..) | Shape::Arc { .. } => {
let r = shape.bounds();
(
0.0,
[r.origin.x, r.origin.y, r.size.width, r.size.height],
0.0,
None,
)
}
};
push(
OpCode::Clip,
geo[0],
geo[1],
geo[2],
geo[3],
extra,
kind,
0.0,
Color::CLEAR,
&mut nums,
);
if let Some(s) = payload {
texts.push(s);
}
}
DrawOp::Text {
text,
at,
size,
color,
anchor,
font,
} => {
if *font != CanvasFont::default() {
push(
OpCode::SetFont,
f64::from(font.css_weight()),
f64::from(u8::from(font.italic)),
0.0,
0.0,
0.0,
0.0,
0.0,
Color::CLEAR,
&mut nums,
);
texts.push(
font.family_str()
.replace([FONT_LIST_FIELD, FONT_LIST_FAMILY], " "),
);
}
push(
OpCode::Text,
at.x,
at.y,
0.0,
0.0,
*size,
anchor.pack(),
0.0,
*color,
&mut nums,
);
texts.push(text.clone());
}
DrawOp::Image {
image,
rect,
opacity,
} => push(
OpCode::Image,
rect.origin.x,
rect.origin.y,
rect.size.width,
rect.size.height,
image.0 as f64,
*opacity,
0.0,
Color::CLEAR,
&mut nums,
),
DrawOp::Save => push(
OpCode::Save,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
Color::CLEAR,
&mut nums,
),
DrawOp::Restore => push(
OpCode::Restore,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
Color::CLEAR,
&mut nums,
),
DrawOp::Concat(m) => push(
OpCode::Concat,
m.a,
m.b,
m.c,
m.d,
m.tx,
m.ty,
0.0,
Color::CLEAR,
&mut nums,
),
}
}
(nums, texts)
}
#[cfg(test)]
mod font_list_tests {
use super::*;
#[test]
fn css_weights_round_trip_and_snap_to_the_nearest_rung() {
for w in FontWeight::ALL {
assert_eq!(FontWeight::from_css(w.css()), w);
}
assert_eq!(FontWeight::from_css(0), FontWeight::UltraLight);
assert_eq!(FontWeight::from_css(350), FontWeight::Regular); assert_eq!(FontWeight::from_css(649), FontWeight::Semibold);
assert_eq!(FontWeight::from_css(1200), FontWeight::Black);
}
fn sans() -> FontFamilyInfo {
FontFamilyInfo {
family: "Day Sans".into(),
faces: vec![
FontFace {
name: "Regular".into(),
weight: FontWeight::Regular,
italic: false,
},
FontFace {
name: "Bold".into(),
weight: FontWeight::Bold,
italic: false,
},
FontFace {
name: "Light Italic".into(),
weight: FontWeight::Light,
italic: true,
},
],
}
}
#[test]
fn face_for_prefers_the_slant_then_the_nearest_weight() {
let f = sans();
assert!(f.has_bold() && f.has_italic());
assert_eq!(f.face_for(FontWeight::Bold, false).unwrap().name, "Bold");
assert_eq!(
f.face_for(FontWeight::Semibold, false).unwrap().name,
"Bold"
);
assert_eq!(
f.face_for(FontWeight::Medium, false).unwrap().name,
"Regular"
);
let tie = FontFamilyInfo {
family: "Tie".into(),
faces: vec![
FontFace {
name: "Light".into(),
weight: FontWeight::Light,
italic: false,
},
FontFace {
name: "Bold".into(),
weight: FontWeight::Bold,
italic: false,
},
],
};
assert_eq!(
tie.face_for(FontWeight::Medium, false).unwrap().name,
"Bold"
);
assert_eq!(
f.face_for(FontWeight::Black, true).unwrap().name,
"Light Italic"
);
let none = FontFamilyInfo {
family: "Empty".into(),
faces: vec![],
};
assert!(none.face_for(FontWeight::Regular, false).is_none());
assert!(!none.has_bold());
}
#[test]
fn the_shim_list_format_decodes_tolerantly() {
let s = "Day Sans\u{1f}Regular\u{1f}400\u{1f}0\u{1f}\u{1f}700\u{1f}1\u{1f}Broken\u{1f}x\u{1f}0\
\u{1e}\u{1e}Day Mono\u{1f}Regular\u{1f}400\u{1f}0\u{1e}Faceless";
let list = parse_font_list(s);
assert_eq!(list.len(), 3);
assert_eq!(list[0].family, "Day Sans");
assert_eq!(list[0].faces.len(), 2, "the bad weight is skipped");
assert_eq!(list[0].faces[1].name, "Bold Italic"); assert_eq!(list[1].family, "Day Mono");
assert_eq!(list[2].family, "Faceless");
assert!(list[2].faces.is_empty());
}
#[test]
fn approximate_metrics_scale_with_size_and_length() {
let m = TextMetrics::approximate("abc", 10.0);
assert_eq!((m.width, m.height, m.ascent), (18.0, 12.0, 9.0));
assert_eq!(TextMetrics::approximate("", 10.0).width, 0.0);
}
#[test]
fn a_default_canvas_font_is_the_platform_face() {
let f = CanvasFont::default();
assert_eq!((f.family_str(), f.css_weight(), f.italic), ("", 0, false));
}
}
#[cfg(test)]
mod image_header_tests {
use super::*;
fn png(w: u32, h: u32) -> Vec<u8> {
let mut v = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
v.extend(13u32.to_be_bytes());
v.extend(b"IHDR");
v.extend(w.to_be_bytes());
v.extend(h.to_be_bytes());
v
}
fn jpeg(w: u16, h: u16) -> Vec<u8> {
let mut v = vec![0xFF, 0xD8]; v.extend([0xFF, 0xE0, 0x00, 0x04, 0x00, 0x00]); v.extend([0xFF, 0xC0, 0x00, 0x11, 0x08]); v.extend(h.to_be_bytes());
v.extend(w.to_be_bytes());
v
}
#[test]
fn a_header_states_the_size_before_anything_decodes() {
assert_eq!(ImageFormat::dimensions(&png(512, 512)), Some((512, 512)));
assert_eq!(ImageFormat::dimensions(&png(1, 65535)), Some((1, 65535)));
assert_eq!(ImageFormat::dimensions(&jpeg(640, 480)), Some((640, 480)));
let mut gif = b"GIF89a".to_vec();
gif.extend(320u16.to_le_bytes());
gif.extend(240u16.to_le_bytes());
assert_eq!(ImageFormat::dimensions(&gif), Some((320, 240)));
let mut bmp = b"BM".to_vec();
bmp.resize(18, 0);
bmp.extend(64i32.to_le_bytes());
bmp.extend((-32i32).to_le_bytes());
assert_eq!(ImageFormat::dimensions(&bmp), Some((64, 32)));
}
#[test]
fn a_truncated_or_unknown_header_answers_nothing_rather_than_a_guess() {
assert_eq!(ImageFormat::dimensions(&[]), None);
assert_eq!(ImageFormat::dimensions(&png(8, 8)[..12]), None);
assert_eq!(ImageFormat::dimensions(b"not an image"), None);
assert_eq!(
ImageFormat::dimensions(&[0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x04, 0, 0]),
None
);
}
}
#[cfg(test)]
mod encode_ops_tests {
use super::*;
#[test]
fn op_codes_are_dense_and_round_trip() {
for (i, code) in OpCode::ALL.into_iter().enumerate() {
assert_eq!(code as i32, i as i32, "{code:?} broke wire density");
assert_eq!(OpCode::from_wire(i as f64), Some(code));
}
assert_eq!(OpCode::from_wire(OpCode::ALL.len() as f64), None);
assert_eq!(OpCode::from_wire(-1.0), None);
assert_eq!(OpCode::from_wire(0.5), None);
}
#[test]
fn encode_ops_emits_whole_records_and_matched_text_payloads() {
let r = Rect::new(1.0, 2.0, 3.0, 4.0);
let red = Color::rgb(1.0, 0.0, 0.0);
let ops = vec![
DrawOp::Fill(Shape::Rect(r), Paint::Solid(red)),
DrawOp::Stroke(Shape::Rect(r), Paint::Solid(red), StrokeStyle::round(2.0)),
DrawOp::Fill(Shape::RoundedRect(r, 5.0), Paint::Solid(red)),
DrawOp::Fill(Shape::Ellipse(r), Paint::Solid(red)),
DrawOp::Fill(
Shape::Arc {
rect: r,
start_deg: 0.0,
sweep_deg: 90.0,
},
Paint::Solid(red),
),
DrawOp::Stroke(
Shape::Line(Point::new(0.0, 0.0), Point::new(9.0, 9.0)),
Paint::Solid(red),
StrokeStyle::width(1.0),
),
DrawOp::Fill(
Shape::Polygon(vec![
Point::new(0.0, 0.0),
Point::new(4.0, 0.0),
Point::new(2.0, 3.0),
]),
Paint::Solid(red),
),
DrawOp::Fill(
Shape::Path(Path {
segs: vec![
PathSeg::Move(Point::new(0.0, 0.0)),
PathSeg::Line(Point::new(5.0, 5.0)),
PathSeg::Close,
],
rule: FillRule::EvenOdd,
}),
Paint::Linear(LinearGradient::new(
UnitPoint::new(0.0, 0.0),
UnitPoint::new(1.0, 1.0),
vec![(0.0, red), (1.0, Color::WHITE)],
)),
),
DrawOp::Clip(Shape::Rect(r)),
DrawOp::Clip(Shape::Polygon(vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.0, 1.0),
])),
DrawOp::Text {
text: "hi".into(),
at: Point::new(7.0, 8.0),
size: 12.0,
color: red,
anchor: TextAnchor::LEADING,
font: CanvasFont::default(),
},
DrawOp::Text {
text: "bold".into(),
at: Point::new(7.0, 8.0),
size: 12.0,
color: red,
anchor: TextAnchor::CENTERED,
font: CanvasFont {
family: Some("Day Sans".into()),
weight: Some(FontWeight::Bold),
italic: true,
},
},
DrawOp::Save,
DrawOp::Concat(Affine::IDENTITY),
DrawOp::Restore,
DrawOp::Image {
image: BitmapId(7),
rect: r,
opacity: 0.5,
},
];
let (nums, texts) = encode_ops(&ops);
assert_eq!(nums.len() % 9, 0, "partial record on the wire");
let mut expected_texts = 0usize;
let mut decoded = Vec::new();
for rec in nums.chunks(9) {
let code = OpCode::from_wire(rec[0])
.unwrap_or_else(|| panic!("unknown op code {} on the wire", rec[0]));
decoded.push(code);
expected_texts += match code {
OpCode::Text
| OpCode::FillPolygon
| OpCode::StrokePolygon
| OpCode::SetGradient
| OpCode::FillPath
| OpCode::StrokePath
| OpCode::StrokeStyle
| OpCode::SetFont => 1,
OpCode::Clip => usize::from(rec[6] == 3.0 || rec[6] == 4.0),
_ => 0,
};
}
assert_eq!(
texts.len(),
expected_texts,
"texts channel out of step with the records that consume it"
);
assert!(texts.iter().all(|t| !t.contains('\u{1f}')));
let expected = {
use OpCode::*;
vec![
FillRect,
StrokeStyle, StrokeRect,
FillRrect,
FillEllipse,
StrokeArc, Line,
FillPolygon,
SetGradient, FillPath,
Clip,
Clip,
Text,
SetFont, Text,
Save,
Concat,
Restore,
Image,
]
};
assert_eq!(decoded, expected);
let img = nums
.chunks(9)
.find(|r| r[0] == OpCode::Image as i32 as f64)
.expect("Image record");
assert_eq!(&img[1..7], &[1.0, 2.0, 3.0, 4.0, 7.0, 0.5]);
let font_rec = nums
.chunks(9)
.find(|r| r[0] == OpCode::SetFont as i32 as f64)
.expect("SetFont record");
assert_eq!(&font_rec[1..3], &[700.0, 1.0]);
assert_eq!(texts.iter().filter(|t| *t == "Day Sans").count(), 1);
let arc = nums
.chunks(9)
.find(|r| r[0] == OpCode::StrokeArc as i32 as f64);
assert_eq!(arc.map(|r| r[7]), Some(0.0));
let path_rec = nums
.chunks(9)
.find(|r| r[0] == OpCode::FillPath as i32 as f64);
assert_eq!(
path_rec.map(|r| r[8] as u32),
Some(0xFFFF_FFFF),
"gradient shape record must carry opaque white"
);
}
#[test]
fn variable_length_geometry_carries_a_content_key() {
let path = |x: f64| {
Shape::Path(Path {
segs: vec![
PathSeg::Move(Point::new(x, 0.0)),
PathSeg::Cubic(
Point::new(1.0, 1.0),
Point::new(2.0, 2.0),
Point::new(3.0, 3.0),
),
PathSeg::Close,
],
rule: FillRule::NonZero,
})
};
let poly = Shape::Polygon(vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.0, 1.0),
]);
assert_eq!(geometry_key(&path(0.0)), geometry_key(&path(0.0)));
assert_ne!(geometry_key(&path(0.0)), geometry_key(&path(1.0)));
let even_odd = Shape::Path(Path {
segs: match path(0.0) {
Shape::Path(p) => p.segs,
_ => unreachable!(),
},
rule: FillRule::EvenOdd,
});
assert_ne!(geometry_key(&path(0.0)), geometry_key(&even_odd));
assert_ne!(geometry_key(&poly), geometry_key(&path(0.0)));
assert_eq!(
geometry_key(&Shape::Rect(Rect::new(0.0, 0.0, 1.0, 1.0))),
0.0
);
let k = geometry_key(&path(0.0));
assert!(
k > 0.0 && k < (1u64 << 53) as f64 && k.fract() == 0.0,
"{k}"
);
let (nums, _) = encode_ops(&[
DrawOp::Fill(path(0.0), Paint::Solid(Color::WHITE)),
DrawOp::Fill(poly.clone(), Paint::Solid(Color::WHITE)),
DrawOp::Clip(path(0.0)),
DrawOp::Clip(Shape::Rect(Rect::new(1.0, 2.0, 3.0, 4.0))),
]);
let slot_a = |code: OpCode| {
nums.chunks(9)
.find(|r| r[0] == code as i32 as f64)
.unwrap_or_else(|| panic!("no {code:?} record"))[1]
};
assert_eq!(slot_a(OpCode::FillPath), geometry_key(&path(0.0)));
assert_eq!(slot_a(OpCode::FillPolygon), geometry_key(&poly));
let clips: Vec<&[f64]> = nums
.chunks(9)
.filter(|r| r[0] == OpCode::Clip as i32 as f64)
.collect();
assert_eq!(clips[0][1], geometry_key(&path(0.0)), "path clip keys");
assert_eq!(
&clips[1][1..5],
&[1.0, 2.0, 3.0, 4.0],
"a rect clip still puts its geometry in a..d"
);
}
}
#[cfg(test)]
mod markup_tests {
use super::*;
fn bold(range: std::ops::Range<usize>) -> TextRun {
TextRun::font(
range,
FontSpec {
style: Font::Body,
weight: Some(FontWeight::Bold),
..Default::default()
},
)
}
#[test]
fn plain_text_is_escaped_in_both_dialects() {
let text = "5 < 6 & \"quoted\" <b>not bold</b>";
for d in [MarkupDialect::Pango, MarkupDialect::QtHtml] {
let m = runs_to_markup(text, &[], d, 16.0);
assert!(!m.contains("<b>"), "content tag survived into markup: {m}");
assert!(m.contains("<b>"), "{m}");
assert!(m.contains("&"), "{m}");
assert!(m.contains("5 < 6"), "{m}");
}
}
#[test]
fn a_styled_run_is_wrapped_and_its_text_escaped() {
let text = "a <b> c";
let m = runs_to_markup(text, &[bold(2..5)], MarkupDialect::Pango, 16.0);
assert_eq!(m, "a <b><b></b> c");
}
#[test]
fn color_and_monospace_differ_per_dialect() {
let text = "code";
let run = TextRun {
range: 0..4,
font: FontSpec {
style: Font::Body,
monospace: true,
..Default::default()
},
color: Some(Color::rgb(1.0, 0.0, 0.0)),
..TextRun::default()
};
let pango = runs_to_markup(text, std::slice::from_ref(&run), MarkupDialect::Pango, 16.0);
assert!(pango.contains("foreground=\"#ff0000\""), "{pango}");
assert!(pango.contains("font_family=\"monospace\""), "{pango}");
let qt = runs_to_markup(
text,
std::slice::from_ref(&run),
MarkupDialect::QtHtml,
16.0,
);
assert!(qt.contains("color:#ff0000"), "{qt}");
assert!(qt.contains("<code>") && qt.contains("</code>"), "{qt}");
}
#[test]
fn the_new_attributes_reach_both_dialects() {
let run = TextRun {
range: 0..2,
font: FontSpec::default().scaled(1.5),
background: Some(Color::rgb(0.0, 1.0, 0.0)),
underline: Underline::Single,
..TextRun::default()
};
let pango = runs_to_markup("hi", std::slice::from_ref(&run), MarkupDialect::Pango, 16.0);
assert!(pango.contains("background=\"#00ff00\""), "{pango}");
assert!(pango.contains("size=\"24576\""), "{pango}");
assert!(pango.contains("<u>hi</u>"), "{pango}");
let qt = runs_to_markup(
"hi",
std::slice::from_ref(&run),
MarkupDialect::QtHtml,
16.0,
);
assert!(qt.contains("background-color:#00ff00"), "{qt}");
assert!(qt.contains("font-size:24pt"), "{qt}");
assert!(qt.contains("<u>hi</u>"), "{qt}");
}
#[test]
fn a_link_target_is_escaped_too() {
let text = "docs";
let run = TextRun {
range: 0..4,
link: Some("https://x.dev/?a=1&b=2".into()),
..TextRun::default()
};
let m = runs_to_markup(text, &[run], MarkupDialect::Pango, 16.0);
assert!(m.contains("a=1&b=2"), "{m}");
}
#[test]
fn tags_nest_and_close_in_reverse() {
let run = TextRun {
range: 0..2,
font: FontSpec {
style: Font::Body,
weight: Some(FontWeight::Bold),
italic: true,
..Default::default()
},
strikethrough: true,
link: Some("u".into()),
..TextRun::default()
};
let m = runs_to_markup("hi", &[run], MarkupDialect::Pango, 16.0);
assert_eq!(m, "<a href=\"u\"><b><i><s>hi</s></i></b></a>");
}
#[test]
fn runs_out_of_bounds_are_skipped_not_panicked() {
let m = runs_to_markup("hi", &[bold(0..99)], MarkupDialect::Pango, 16.0);
assert_eq!(m, "hi");
}
#[test]
fn validation_catches_the_four_ways_runs_go_wrong() {
let text = "héllo";
assert!(runs_are_valid(text, &[bold(0..1)]).is_ok());
assert!(runs_are_valid(text, &[bold(0..3), bold(2..4)]).is_err());
assert!(runs_are_valid(text, &[bold(0..99)]).is_err());
let (hi, lo) = (3, 1);
assert!(runs_are_valid(text, &[bold(hi..lo)]).is_err());
assert!(runs_are_valid(text, &[bold(0..2)]).is_err());
}
}
#[cfg(test)]
mod route_of_url_tests {
use super::route_of_url;
#[test]
fn strips_the_scheme_and_keeps_route_and_params() {
assert_eq!(
route_of_url("notes://mail/inbox?hint=x"),
"mail/inbox?hint=x"
);
assert_eq!(route_of_url("notes://home"), "home");
assert_eq!(route_of_url("mail/inbox"), "mail/inbox");
assert_eq!(route_of_url(""), "");
assert_eq!(route_of_url("n://a/b%2Fc?q=1%202"), "a/b%2Fc?q=1%202");
}
}
#[cfg(test)]
mod builtin_kind_tests {
use super::*;
#[test]
fn keys_round_trip_and_are_unique() {
let mut seen = std::collections::HashSet::new();
for &b in Builtin::ALL {
assert_eq!(Builtin::from_key(b.key()), Some(b), "{b:?} key round-trip");
assert!(seen.insert(b.key()), "duplicate key {:?}", b.key());
assert!(
b.key().starts_with("day."),
"built-in {b:?} must use the reserved `day.` prefix"
);
}
}
#[test]
fn kinds_constants_match_the_enum() {
assert_eq!(kinds::LABEL, Builtin::Label.key());
assert_eq!(kinds::LIST_CELL, Builtin::ListCell.key());
assert_eq!(kinds::COVER, Builtin::Cover.key());
assert_eq!(kinds::INSPECTOR, Builtin::Inspector.key());
assert_eq!(kinds::TREE, Builtin::Tree.key());
assert_eq!(Builtin::ALL.len(), 25);
}
#[test]
fn extension_kinds_are_not_builtin() {
assert_eq!(Builtin::from_key("acme.combobox"), None);
assert_eq!(Builtin::from_key("day.not_a_real_kind"), None);
}
}
#[cfg(test)]
mod locale_tests {
#[test]
fn posix_hints_parse_and_drop_the_c_locale() {
let normalize = |v: &str| v.split('.').next().unwrap_or(v).replace('_', "-");
assert_eq!(normalize("fr_FR.UTF-8"), "fr-FR");
assert_eq!(normalize("en_US"), "en-US");
assert_eq!(normalize("C"), "C");
let hints = super::posix_locale_hints();
assert!(
hints
.iter()
.all(|h| !h.is_empty() && h != "C" && h != "POSIX")
);
}
}
#[cfg(test)]
mod sidetable_tests {
use super::sidetable::{SideTable, sweep};
use std::cell::Cell;
use std::rc::Rc;
#[test]
fn sweep_clears_every_registered_table_and_runs_teardowns() {
let torn = Rc::new(Cell::new(0u32));
let t2 = torn.clone();
let plain: SideTable<&'static str> = SideTable::new();
let hooked: SideTable<u32> = SideTable::with_teardown(move |_| t2.set(t2.get() + 1));
plain.insert(7, "a");
hooked.insert(7, 1);
hooked.insert(8, 2);
sweep(7);
assert!(!plain.contains(7));
assert!(!hooked.contains(7), "swept from every table");
assert!(hooked.contains(8), "other keys untouched");
assert_eq!(torn.get(), 1, "teardown ran for the swept value");
hooked.remove(8);
assert_eq!(torn.get(), 2);
hooked.insert(9, 3);
assert_eq!(hooked.take(9), Some(3));
assert_eq!(torn.get(), 2, "take() skips the hook");
}
#[test]
fn contain_returns_default_on_panic() {
let ok = super::ffi_guard::contain(0i32, || 5);
assert_eq!(ok, 5);
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let contained = super::ffi_guard::contain(-1i32, || panic!("boom"));
std::panic::set_hook(prev);
assert_eq!(contained, -1, "the panic must not escape the guard");
}
}
#[cfg(test)]
mod cursor_tests {
use super::Cursor;
#[test]
fn named_cursors_have_distinct_css_names() {
let mut seen = std::collections::BTreeSet::new();
for c in Cursor::NAMED.iter() {
assert!(
seen.insert(c.css_name().to_string()),
"duplicate: {}",
c.css_name()
);
assert!(!matches!(c, Cursor::Native(_)));
}
assert_eq!(seen.len(), Cursor::NAMED.len());
assert_eq!(Cursor::native("dragCopy").css_name(), "dragCopy");
assert_eq!(Cursor::default(), Cursor::Default);
}
}
#[cfg(test)]
mod text_anchor_tests {
#[test]
fn text_anchor_offsets_place_the_line_box() {
use crate::props::TextAlign;
use crate::{TextAnchor, TextVAlign};
let (w, h, asc) = (40.0, 20.0, 16.0);
let at = |ha, va| TextAnchor { h: ha, v: va }.offset(w, h, asc);
assert_eq!(at(TextAlign::Leading, TextVAlign::Top), (0.0, 0.0));
assert_eq!(TextAnchor::default(), TextAnchor::LEADING);
assert_eq!(at(TextAlign::Center, TextVAlign::Top).0, -20.0);
assert_eq!(at(TextAlign::Trailing, TextVAlign::Top).0, -40.0);
assert_eq!(at(TextAlign::Leading, TextVAlign::Middle).1, -10.0);
assert_eq!(at(TextAlign::Leading, TextVAlign::Baseline).1, -16.0);
assert_eq!(at(TextAlign::Leading, TextVAlign::Bottom).1, -20.0);
assert_eq!(TextAnchor::CENTERED.offset(w, h, asc), (-20.0, -10.0));
assert_eq!(TextAnchor::TRAILING.offset(w, h, asc), (-40.0, 0.0));
}
#[test]
fn text_anchor_packs_into_one_slot_and_round_trips() {
use crate::props::TextAlign;
use crate::{TextAnchor, TextVAlign};
let mut seen = std::collections::BTreeSet::new();
for h in [TextAlign::Leading, TextAlign::Center, TextAlign::Trailing] {
for v in [
TextVAlign::Top,
TextVAlign::Middle,
TextVAlign::Baseline,
TextVAlign::Bottom,
] {
let a = TextAnchor { h, v };
let code = a.pack();
assert!(
(0.0..=11.0).contains(&code),
"{code} outside the packed range"
);
assert!(seen.insert(code as u64), "two anchors share code {code}");
assert_eq!(TextAnchor::unpack(code), a);
}
}
assert_eq!(seen.len(), 12);
assert_eq!(TextAnchor::LEADING.pack(), 0.0);
assert_eq!(TextAnchor::CENTERED.pack(), 5.0);
assert_eq!(TextAnchor::unpack(-3.0), TextAnchor::LEADING);
assert_eq!(TextAnchor::unpack(99.0), TextAnchor::unpack(11.0));
}
#[test]
fn metrics_convert_from_a_backends_own_shape() {
use crate::{Rect, TextMetrics};
let m = TextMetrics::from_baseline(
40.0,
16.0,
4.0,
11.0,
Rect::new(1.0, -11.0, 38.0, 13.0),
);
assert_eq!(
(m.width, m.height, m.ascent, m.cap_height),
(40.0, 20.0, 16.0, 11.0)
);
assert_eq!(m.ink, Rect::new(1.0, 5.0, 38.0, 13.0));
assert_eq!(m.ascent - m.cap_height, m.ink.origin.y);
let w = TextMetrics::from_slots(&[40.0, 20.0, 16.0, 11.0, 1.0, 5.0, 38.0, 13.0]);
assert_eq!(w, m);
}
#[test]
fn the_approximation_is_internally_consistent() {
use crate::TextMetrics;
let m = TextMetrics::approximate("Agy", 20.0);
assert!(m.ascent < m.height, "the baseline sits inside the line box");
assert!(
m.cap_height < m.ascent,
"caps do not reach above the ascent"
);
assert_eq!(m.ink.origin.x, 0.0);
assert_eq!(m.ink.origin.y, 0.0);
assert_eq!(m.ink.size.width, m.width);
assert_eq!(m.ink.size.height, m.height);
let e = TextMetrics::approximate("", 20.0);
assert_eq!(e.width, 0.0);
assert!(e.height > 0.0);
}
}
#[cfg(test)]
mod cap_list_tests {
use super::Cap;
#[test]
fn every_capability_is_listed_once_in_its_place() {
for (i, cap) in Cap::ALL.iter().enumerate() {
assert_eq!(cap.ordinal(), i, "{cap:?} is out of place in Cap::ALL");
}
}
}