#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod anim;
pub mod cache;
pub mod color_space;
pub mod diff;
pub mod dispatch;
pub mod events;
pub mod focus;
pub mod geometry;
pub mod grid;
pub mod layout;
pub mod paint;
pub mod reactive;
pub mod response;
pub mod scheduler;
pub mod solver;
pub mod style;
pub mod text_style;
pub mod tree;
pub mod widget_ext;
pub use anim::{Animator, Easing, Spring, Transition};
pub use cache::LayoutCache;
pub use color_space::{
contrast_ratio, ContrastWarning, Hsla, LinearRgba, Oklcha, PaletteBuilder, WcagLevel,
};
pub use diff::{diff, DiffOp};
pub use dispatch::{DispatchEvent, EventDispatcher, EventHandler, HandlerCtx, Phase};
pub use events::{
GestureKind, Key, KeyboardEvent, Modifiers, MouseButton, MouseEvent, PhysicalKey, Propagation,
ScrollDelta, TouchEvent,
};
pub use focus::FocusManager;
pub use geometry::{Constraints, Insets, Point, Rect, Size};
pub use grid::{
compute_grid, GridItem, GridLine, GridPlacement, GridSpan, GridTemplate, TrackSizing,
};
pub use layout::{
AlignContent, AlignItems, FlexDirection, FlexItem, FlexLayout, FlexWrap, JustifyContent,
};
pub use paint::{DrawCommand, DrawList, RenderBackend};
pub use reactive::{Computed, ReactiveError, ReactiveRuntime, Signal};
pub use response::{
CheckboxResponse, DropdownResponse, SliderResponse, TextInputResponse, WidgetResponse,
};
pub use scheduler::{Debounce, Scheduler, Throttle, TimerId};
pub use solver::{Constraint, Expression, RelOp, Solver, SolverError, Strength, Term, Variable};
pub use style::{Border, BorderStyle, CursorShape, Margin, Padding};
pub use text_style::TextStyle;
pub use tree::{WidgetId, WidgetIdAllocator, WidgetNode, WidgetTree};
pub use widget_ext::{ClipboardProvider, DragData, DragSource, DropEffect, DropTarget, WidgetExt};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Color(pub u8, pub u8, pub u8, pub u8);
#[derive(Clone, Debug)]
pub struct Palette {
pub background: Color,
pub surface: Color,
pub primary: Color,
pub on_primary: Color,
pub text: Color,
pub muted: Color,
}
impl Palette {
pub fn new(
background: Color,
surface: Color,
primary: Color,
on_primary: Color,
text: Color,
muted: Color,
) -> Self {
Self {
background,
surface,
primary,
on_primary,
text,
muted,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum FontStyle {
#[default]
Normal,
Italic,
Oblique {
degrees: f32,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct FontFeature {
pub tag: String,
pub value: u32,
}
impl FontFeature {
pub fn on(tag: impl Into<String>) -> Self {
Self {
tag: tag.into(),
value: 1,
}
}
pub fn off(tag: impl Into<String>) -> Self {
Self {
tag: tag.into(),
value: 0,
}
}
pub fn value(tag: impl Into<String>, value: u32) -> Self {
Self {
tag: tag.into(),
value,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct FontSpec {
pub family: String,
pub size: f32,
pub weight: u16,
pub style: FontStyle,
pub letter_spacing: f32,
pub line_height: Option<f32>,
pub features: Vec<FontFeature>,
}
impl FontSpec {
pub fn new(family: impl Into<String>, size: f32, weight: u16) -> Self {
Self {
family: family.into(),
size,
weight,
style: FontStyle::Normal,
letter_spacing: 0.0,
line_height: None,
features: Vec::new(),
}
}
pub fn with_style(mut self, style: FontStyle) -> Self {
self.style = style;
self
}
pub fn with_letter_spacing(mut self, letter_spacing: f32) -> Self {
self.letter_spacing = letter_spacing;
self
}
pub fn with_line_height(mut self, line_height: f32) -> Self {
self.line_height = Some(line_height);
self
}
pub fn with_feature(mut self, feature: FontFeature) -> Self {
self.features.push(feature);
self
}
pub fn is_slanted(&self) -> bool {
!matches!(self.style, FontStyle::Normal)
}
}
impl Default for FontSpec {
fn default() -> Self {
Self::new("Inter", 14.0, 400)
}
}
#[derive(Clone, Debug)]
pub struct RichTextSpan {
pub text: String,
pub bold: bool,
pub italic: bool,
pub color: [u8; 4],
pub font_size: f32,
pub font_family: Option<String>,
}
impl RichTextSpan {
pub fn new(text: impl Into<String>) -> Self {
RichTextSpan {
text: text.into(),
bold: false,
italic: false,
color: [0, 0, 0, 255],
font_size: 16.0,
font_family: None,
}
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
pub fn color(mut self, c: [u8; 4]) -> Self {
self.color = c;
self
}
pub fn font_size(mut self, s: f32) -> Self {
self.font_size = s;
self
}
pub fn font_family(mut self, family: impl Into<String>) -> Self {
self.font_family = Some(family.into());
self
}
}
#[derive(Clone, Debug, Default)]
pub struct ButtonResponse {
pub clicked: bool,
pub hovered: bool,
}
#[derive(Clone, Debug)]
pub enum Axis {
Vertical,
Horizontal,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum UiEvent {
Resize(u32, u32),
CloseRequested,
KeyPress(String),
Mouse {
x: f32,
y: f32,
},
MouseDown {
button: events::MouseButton,
x: f32,
y: f32,
modifiers: events::Modifiers,
},
MouseUp {
button: events::MouseButton,
x: f32,
y: f32,
modifiers: events::Modifiers,
},
MouseMove {
x: f32,
y: f32,
},
Wheel(events::ScrollDelta),
KeyDown {
key: events::Key,
modifiers: events::Modifiers,
repeat: bool,
},
KeyUp {
key: events::Key,
modifiers: events::Modifiers,
},
ImePreedit {
text: String,
cursor: Option<(usize, usize)>,
},
ImeCommit(String),
}
pub trait UiCtx {
fn heading(&mut self, text: &str);
fn label(&mut self, text: &str);
fn button(&mut self, label: &str) -> ButtonResponse;
fn text_input(&mut self, _text: &str) -> response::TextInputResponse {
response::TextInputResponse::unsupported()
}
fn checkbox(&mut self, _label: &str, _checked: bool) -> response::CheckboxResponse {
response::CheckboxResponse::unsupported()
}
fn slider(
&mut self,
_value: f64,
_range: core::ops::RangeInclusive<f64>,
) -> response::SliderResponse {
response::SliderResponse::unsupported()
}
fn dropdown(&mut self, _options: &[&str], _selected: usize) -> response::DropdownResponse {
response::DropdownResponse::unsupported()
}
fn image(&mut self, _uri: &str, _size: Option<Size>) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn separator(&mut self) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn spacer(&mut self, _size: f32) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn scroll_area(
&mut self,
_content: &mut dyn FnMut(&mut dyn UiCtx),
) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn tooltip(&mut self, _text: &str) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn popup(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn modal(
&mut self,
_title: &str,
_content: &mut dyn FnMut(&mut dyn UiCtx),
) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn horizontal(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn vertical(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn grid(
&mut self,
_cols: usize,
_content: &mut dyn FnMut(&mut dyn UiCtx),
) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn menu_bar(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn rich_text(&mut self, _spans: &[RichTextSpan]) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn label_styled(&mut self, text: &str, _style: TextStyle) -> response::WidgetResponse {
self.label(text);
response::WidgetResponse::supported()
}
fn heading_styled(&mut self, text: &str, _style: TextStyle) -> response::WidgetResponse {
self.heading(text);
response::WidgetResponse::supported()
}
fn drag_source(
&mut self,
_id: u64,
_content: &mut dyn FnMut(&mut dyn UiCtx),
) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
fn drop_target(
&mut self,
_accept_ids: &[u64],
_content: &mut dyn FnMut(&mut dyn UiCtx),
) -> response::WidgetResponse {
response::WidgetResponse::unsupported()
}
}
pub trait Widget {
fn render(&mut self, ui: &mut dyn UiCtx);
}
pub trait Theme: Send + Sync {
fn palette(&self) -> &Palette;
fn font(&self) -> &FontSpec;
}
pub trait Layout: Send + Sync {
fn axis(&self) -> Axis;
fn spacing(&self) -> f32;
}
pub trait EventSink {
fn push(&mut self, event: UiEvent);
}
#[derive(Debug)]
#[non_exhaustive]
pub enum UiError {
Backend(String),
Render(String),
Window(String),
Unsupported(String),
Layout(String),
Focus(String),
Clipboard(String),
DragDrop(String),
Other(String),
}
impl std::fmt::Display for UiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UiError::Backend(s) => write!(f, "UI backend error: {s}"),
UiError::Render(s) => write!(f, "UI render error: {s}"),
UiError::Window(s) => write!(f, "UI window error: {s}"),
UiError::Unsupported(s) => write!(f, "UI unsupported: {s}"),
UiError::Layout(s) => write!(f, "UI layout error: {s}"),
UiError::Focus(s) => write!(f, "UI focus error: {s}"),
UiError::Clipboard(s) => write!(f, "UI clipboard error: {s}"),
UiError::DragDrop(s) => write!(f, "UI drag-and-drop error: {s}"),
UiError::Other(s) => write!(f, "UI error: {s}"),
}
}
}
impl std::error::Error for UiError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ime_preedit_event_roundtrip() {
let event = UiEvent::ImePreedit {
text: "日本語".to_string(),
cursor: Some((0, 9)),
};
match event {
UiEvent::ImePreedit { text, cursor } => {
assert_eq!(text, "日本語");
assert!(cursor.is_some());
let (start, end) = cursor.expect("cursor should be Some");
assert_eq!(start, 0);
assert_eq!(end, 9);
}
_ => panic!("expected ImePreedit variant"),
}
}
#[test]
fn ime_commit_event_roundtrip() {
let event = UiEvent::ImeCommit("確定".to_string());
match event {
UiEvent::ImeCommit(text) => {
assert_eq!(text, "確定");
}
_ => panic!("expected ImeCommit variant"),
}
}
#[test]
fn ime_preedit_no_cursor() {
let event = UiEvent::ImePreedit {
text: "abc".to_string(),
cursor: None,
};
match event {
UiEvent::ImePreedit { text, cursor } => {
assert_eq!(text, "abc");
assert!(cursor.is_none());
}
_ => panic!("expected ImePreedit variant"),
}
}
#[test]
fn font_spec_expansion_defaults_and_builders() {
let base = FontSpec::new("Inter", 16.0, 500);
assert_eq!(base.style, FontStyle::Normal);
assert_eq!(base.letter_spacing, 0.0);
assert!(base.line_height.is_none());
assert!(base.features.is_empty());
assert!(!base.is_slanted());
let rich = FontSpec::new("Inter", 16.0, 500)
.with_style(FontStyle::Italic)
.with_letter_spacing(0.5)
.with_line_height(20.0)
.with_feature(FontFeature::on("liga"))
.with_feature(FontFeature::value("ss01", 1));
assert!(rich.is_slanted());
assert_eq!(rich.letter_spacing, 0.5);
assert_eq!(rich.line_height, Some(20.0));
assert_eq!(rich.features.len(), 2);
assert_eq!(rich.features[0], FontFeature::on("liga"));
let obl = FontSpec::default().with_style(FontStyle::Oblique { degrees: 12.0 });
assert!(
matches!(obl.style, FontStyle::Oblique { degrees } if (degrees - 12.0).abs() < 1e-6)
);
}
#[test]
fn extended_uictx_defaults_report_unsupported() {
struct BareCtx;
impl UiCtx for BareCtx {
fn heading(&mut self, _t: &str) {}
fn label(&mut self, _t: &str) {}
fn button(&mut self, _l: &str) -> ButtonResponse {
ButtonResponse::default()
}
}
let mut ui = BareCtx;
assert!(!ui.text_input("x").supported);
assert!(!ui.checkbox("c", true).supported);
assert!(!ui.slider(0.5, 0.0..=1.0).supported);
assert!(!ui.dropdown(&["a", "b"], 0).supported);
assert!(!ui.image("u", None).supported);
assert!(!ui.separator().supported);
assert!(!ui.spacer(8.0).supported);
assert!(!ui.tooltip("t").supported);
let mut invoked = false;
let r = ui.scroll_area(&mut |_inner| invoked = true);
assert!(!r.supported);
assert!(!invoked, "unsupported scroll_area must not run content");
let mut popup_invoked = false;
assert!(!ui.popup(&mut |_| popup_invoked = true).supported);
assert!(!popup_invoked);
let mut modal_invoked = false;
assert!(!ui.modal("title", &mut |_| modal_invoked = true).supported);
assert!(!modal_invoked);
}
#[test]
fn ui_error_new_variants_display() {
assert!(UiError::Layout("x".into()).to_string().contains("layout"));
assert!(UiError::Focus("x".into()).to_string().contains("focus"));
assert!(UiError::Clipboard("x".into())
.to_string()
.contains("clipboard"));
assert!(UiError::DragDrop("x".into()).to_string().contains("drag"));
}
#[test]
fn uictx_extension_defaults_unsupported() {
struct Bare;
impl UiCtx for Bare {
fn heading(&mut self, _: &str) {}
fn label(&mut self, _: &str) {}
fn button(&mut self, _: &str) -> ButtonResponse {
ButtonResponse::default()
}
}
let mut b = Bare;
assert!(!b.horizontal(&mut |_| {}).supported);
assert!(!b.vertical(&mut |_| {}).supported);
assert!(!b.grid(2, &mut |_| {}).supported);
assert!(!b.menu_bar(&mut |_| {}).supported);
assert!(!b.rich_text(&[]).supported);
assert!(!b.drag_source(1, &mut |_| {}).supported);
assert!(!b.drop_target(&[], &mut |_| {}).supported);
}
#[test]
fn rich_text_span_builder() {
let span = RichTextSpan::new("Hello")
.bold()
.italic()
.color([255, 0, 0, 255])
.font_size(24.0);
assert!(span.bold);
assert!(span.italic);
assert_eq!(span.color, [255, 0, 0, 255]);
assert_eq!(span.font_size, 24.0);
assert_eq!(span.text, "Hello");
}
}