mod events;
mod pass;
mod reconcile;
mod restyle;
pub use pass::{LayoutStep, LeafData};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::sync::Arc;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use taffy::style::Direction;
use crate::catalog::{CatalogView, A2UI_BASIC_CATALOG_ID};
use crate::data::{get_pointer, set_pointer};
use crate::engine::Engine;
use crate::layout_tree::{LNode, LayerClass, LayerSpec, ListSpec, LocalState, NodeKind, ToastSpec};
use crate::measure::{ControlBox, MeasureMemo, TextStyle};
use crate::overlay::OverlayPlacement;
use crate::recipes::RecipeIndex;
use crate::reducer::{reduce_surface, ReduceOptions, ReduceResult};
use crate::strings::StringTable;
use crate::style::Visual;
use crate::theme::{apply_contrast, apply_density, resolve_mode, Density, Mode, ModeSetting, ResolvedTheme};
use crate::types::{ExtensionDef, FlatComponent, NestedNode, ReduceIssue, UiNode};
pub use crate::layout_tree::ToastSpec as Toast;
pub const HOVER_CLOSE_MS: u32 = 150;
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Frame {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlacedNode {
pub index: u32,
pub id: String,
pub component: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub part: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner_component: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub catalog_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extension_kind: Option<String>,
pub depth: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<u32>,
pub layer: u32,
pub kind: NodeKind,
pub props: Map<String, Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lines: Option<u32>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub pressable: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub hidden: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub accessibility: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_for: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub states: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub form: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub live: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub removed: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct PlacedFrame {
pub index: u32,
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Layer {
pub layer: u32,
pub kind: String,
pub owner: String,
pub root: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub anchor_frame: Option<Frame>,
#[serde(skip_serializing_if = "Option::is_none")]
pub placement: Option<OverlayPlacement>,
pub position: String,
pub class: LayerClass,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub modal: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub dismissible: bool,
pub frames: Vec<PlacedFrame>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListOutput {
pub id: String,
pub node: u32,
pub content_height: f32,
pub start: u32,
pub end: u32,
pub count: u32,
pub windowed: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub horizontal: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct StickyOutput {
pub index: u32,
pub dx: f32,
pub dy: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ScrollOutput {
pub index: u32,
pub offset_x: f32,
pub offset_y: f32,
pub content_width: f32,
pub content_height: f32,
pub scroll_x: bool,
pub scroll_y: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct NodeDelta {
pub added: Vec<u32>,
pub removed: Vec<u32>,
pub changed: Vec<u32>,
pub renumbered: bool,
}
impl NodeDelta {
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty() && !self.renumbered
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LayoutOutput {
pub frames: Vec<PlacedFrame>,
pub layers: Vec<Layer>,
pub lists: Vec<ListOutput>,
pub scrolls: Vec<ScrollOutput>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sticky: Vec<StickyOutput>,
pub toasts: Vec<ToastSpec>,
pub visual_changes: Vec<u32>,
pub delta: NodeDelta,
pub structure_version: u64,
pub surface_width: f32,
pub surface_height: f32,
pub overflow: bool,
pub direction: String,
pub breakpoint: Option<String>,
pub measure_rounds: u32,
pub upcalls: u32,
pub restyled: u32,
pub rebuilt: bool,
pub built_nodes: u32,
pub layout_ns: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
#[non_exhaustive]
pub enum OutEvent {
Action { name: String, component_id: String, event: String, context: Value, #[serde(skip_serializing_if = "Option::is_none")] payload: Option<Value> },
OpenUrl { url: String },
FunctionCall { component_id: String, name: String, args: Value },
DataChanged { path: String, value: Value },
Input { component_id: String, name: String, #[serde(skip_serializing_if = "Option::is_none")] path: Option<String>, value: Value, commit: bool },
Focus { id: String, index: u32 },
Announce { text: String, live: String },
Copy { text: String },
PickFiles { component_id: String, #[serde(skip_serializing_if = "Option::is_none")] accept: Option<String>, multiple: bool },
Relayout,
HoverTimer { owner: String, delay_ms: u32 },
ScrollSurface { x: f32, y: f32 },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SurfaceCommand {
Focus { id: String },
Announce { text: String, #[serde(default)] live: Option<String> },
ScrollIntoView { id: String },
ScrollToIndex {
id: String,
index: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
align: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Insets {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ContrastSetting {
Normal,
High,
#[default]
System,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct SurfaceSettings {
pub locale: String,
pub strings: IndexMap<String, String>,
pub mode: ModeSetting,
pub system_dark: bool,
pub density: Density,
pub contrast: ContrastSetting,
pub system_high_contrast: bool,
pub font_scale: f32,
pub hover: bool,
pub reduced_motion: bool,
pub insets: Insets,
pub today: Option<String>,
pub hover_close_ms: u32,
pub read_only: bool,
}
impl Default for SurfaceSettings {
fn default() -> Self {
SurfaceSettings {
locale: crate::locale::DEFAULT_LOCALE.to_string(),
strings: IndexMap::new(),
mode: ModeSetting::Light,
system_dark: false,
density: Density::Default,
contrast: ContrastSetting::Normal,
system_high_contrast: false,
font_scale: 1.0,
hover: true,
reduced_motion: false,
insets: Insets::default(),
today: None,
hover_close_ms: 0,
read_only: false,
}
}
}
#[derive(Debug, Clone)]
pub struct SurfaceOptions {
pub catalog_id: String,
pub theme: Option<Arc<ResolvedTheme>>,
pub mode: Mode,
pub extensions: Vec<ExtensionDef>,
pub rounding: bool,
pub expand_controls: Option<bool>,
}
impl Default for SurfaceOptions {
fn default() -> Self {
SurfaceOptions {
catalog_id: crate::catalog::CORE_CATALOG_ID.to_string(),
theme: Some(crate::themes::default_theme()),
mode: Mode::Light,
extensions: Vec::new(),
rounding: false,
expand_controls: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ApplyOutcome {
pub structure_changed: bool,
pub issues: Vec<ReduceIssue>,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
struct RadiusPercent {
all: Option<f32>,
corners: [Option<f32>; 4],
}
#[derive(Debug, Clone, Default)]
struct NodeState {
flat: Map<String, Value>,
visual: Visual,
text: TextStyle,
control: ControlBox,
content_version: u32,
conditional: bool,
radius_pct: Option<RadiusPercent>,
styled: bool,
direction: Direction,
line_height_set: bool,
}
#[derive(Debug, Clone)]
struct Carried {
intrinsics: crate::measure::Intrinsics,
heights: Vec<(i64, f32)>,
props: Map<String, Value>,
lines: Option<u32>,
}
pub struct Surface {
pub id: String,
pub catalog_id: String,
view: Arc<CatalogView>,
recipes: Arc<RecipeIndex>,
extensions: Vec<ExtensionDef>,
theme: Option<Arc<ResolvedTheme>>,
effective: Option<Arc<ResolvedTheme>>,
settings: SurfaceSettings,
strings: StringTable,
mode: Mode,
expand_controls: Option<bool>,
components: Vec<FlatComponent>,
nested: Option<NestedNode>,
data: Value,
root: Option<UiNode>,
pub(crate) issues: Vec<ReduceIssue>,
reduce_issues: Vec<ReduceIssue>,
pub(crate) build_issues: Vec<ReduceIssue>,
pub(crate) style_issues: Vec<ReduceIssue>,
local: LocalState,
states: HashMap<String, Vec<String>>,
nodes: Vec<LNode>,
live: Vec<bool>,
slot_of: HashMap<String, u32>,
free: Vec<u32>,
main_root: Option<u32>,
layers: Vec<LayerSpec>,
lists: Vec<ListSpec>,
toasts: Vec<ToastSpec>,
node_state: Vec<NodeState>,
engine: Engine,
memo: MeasureMemo,
memo_versions: HashMap<u32, u32>,
last_measurer: Option<u64>,
viewport: (f32, f32),
max_height: Option<f32>,
direction: Direction,
rounding: bool,
structure_version: u64,
needs_build: bool,
restyle_all: bool,
style_dirty: BTreeSet<u32>,
visuals_dirty: HashSet<u32>,
template_cache: HashMap<String, Option<UiNode>>,
reduce_pending: bool,
component_index: HashMap<String, usize>,
last_frames: Vec<Frame>,
archive: HashMap<String, Carried>,
responsive: bool,
breakpoint: Option<String>,
delta_added: BTreeSet<u32>,
delta_removed: BTreeSet<u32>,
delta_changed: BTreeSet<u32>,
renumbered: bool,
pending: Vec<OutEvent>,
scrolls: Vec<ScrollOutput>,
restyled: u32,
rebuilt: bool,
built_nodes: u32,
step_state: Option<pass::PassState>,
subtree_pending: BTreeSet<String>,
hover_closing: HashSet<String>,
seed: crate::layout_tree::BuildSeed,
formatter: Arc<dyn crate::format::Formatter>,
clock: Option<f64>,
data_version: u64,
}
unsafe impl Send for Surface {}
impl Surface {
pub fn new(id: &str, options: SurfaceOptions) -> Surface {
let view = CatalogView::with(&options.extensions);
let recipes = if options.extensions.is_empty() { RecipeIndex::core() } else { Arc::new(RecipeIndex::new(&view)) };
let mut engine = Engine::new();
engine.set_rounding(options.rounding);
let settings = SurfaceSettings { mode: if options.mode == Mode::Dark { ModeSetting::Dark } else { ModeSetting::Light }, ..SurfaceSettings::default() };
let mut s = Surface {
id: id.to_string(),
catalog_id: options.catalog_id,
view,
recipes,
extensions: options.extensions,
theme: options.theme,
effective: None,
strings: crate::strings::DEFAULT_STRINGS.clone(),
settings,
mode: options.mode,
expand_controls: options.expand_controls,
components: Vec::new(),
nested: None,
data: Value::Object(Map::new()),
root: None,
issues: Vec::new(),
reduce_issues: Vec::new(),
build_issues: Vec::new(),
style_issues: Vec::new(),
local: LocalState::default(),
states: HashMap::new(),
nodes: Vec::new(),
live: Vec::new(),
slot_of: HashMap::new(),
free: Vec::new(),
main_root: None,
layers: Vec::new(),
lists: Vec::new(),
toasts: Vec::new(),
node_state: Vec::new(),
engine,
memo: MeasureMemo::default(),
memo_versions: HashMap::new(),
last_measurer: None,
viewport: (0.0, 0.0),
max_height: None,
direction: Direction::Ltr,
rounding: options.rounding,
structure_version: 0,
needs_build: true,
restyle_all: true,
style_dirty: BTreeSet::new(),
visuals_dirty: HashSet::new(),
template_cache: HashMap::new(),
reduce_pending: false,
component_index: HashMap::new(),
last_frames: Vec::new(),
archive: HashMap::new(),
responsive: false,
breakpoint: None,
delta_added: BTreeSet::new(),
delta_removed: BTreeSet::new(),
delta_changed: BTreeSet::new(),
renumbered: false,
pending: Vec::new(),
scrolls: Vec::new(),
restyled: 0,
rebuilt: false,
built_nodes: 0,
step_state: None,
subtree_pending: BTreeSet::new(),
hover_closing: HashSet::new(),
seed: Default::default(),
formatter: Arc::new(crate::format::EnglishFormatter),
clock: None,
data_version: 0,
};
s.refresh_theme();
s
}
pub fn set_formatter(&mut self, formatter: Arc<dyn crate::format::Formatter>) {
self.formatter = formatter;
self.needs_build = true;
}
pub fn formatter(&self) -> &Arc<dyn crate::format::Formatter> {
&self.formatter
}
pub fn set_clock(&mut self, now_ms: Option<f64>) {
self.clock = now_ms;
self.needs_build = true;
}
pub fn tick(&mut self) {
self.needs_build = true;
}
pub fn uses_clock(&self) -> bool {
fn relative_call(v: &Value) -> bool {
match v {
Value::Object(o) => (o.get("call").and_then(Value::as_str) == Some("formatRelativeTime") && o.get("args").and_then(|a| a.get("now")).is_none()) || o.values().any(relative_call),
Value::Array(a) => a.iter().any(relative_call),
_ => false,
}
}
if self.clock.is_some() {
return false;
}
let mut found = false;
for tree in self.root.iter().chain(self.template_cache.values().flatten()) {
tree.walk(&mut |n| found = found || n.props.values().any(relative_call));
}
found
|| self.nodes.iter().zip(&self.live).any(|(n, live)| {
*live && n.component == "Table" && n.props.get("columns").and_then(Value::as_array).is_some_and(|cols| cols.iter().any(|c| c.get("type").and_then(Value::as_str) == Some("relativeTime")))
})
}
pub fn register_extension(&mut self, ext: ExtensionDef) -> Result<(), String> {
let ext = crate::extension::define_extension(ext)?;
let mut all = self.extensions.clone();
all.retain(|e| e.id != ext.id);
all.push(ext);
self.view = CatalogView::with(&all);
self.recipes = Arc::new(RecipeIndex::new(&self.view));
self.extensions = all;
self.template_cache.clear();
self.reduce();
Ok(())
}
fn refresh_theme(&mut self) {
let contrast = match self.settings.contrast {
ContrastSetting::High => true,
ContrastSetting::Normal => false,
ContrastSetting::System => self.settings.system_high_contrast,
};
self.effective = self.theme.as_ref().map(|t| {
if self.settings.density == Density::Default && !contrast {
return t.clone();
}
let mut out = apply_density(t, self.settings.density);
if contrast {
out = apply_contrast(&out);
}
Arc::new(out)
});
self.restyle_all = true;
}
pub fn set_theme(&mut self, theme: Option<Arc<ResolvedTheme>>) {
self.theme = theme;
self.refresh_theme();
self.needs_build = true;
self.invalidate_measures();
}
pub fn theme(&self) -> Option<&Arc<ResolvedTheme>> {
self.theme.as_ref()
}
pub fn effective_theme(&self) -> Option<&Arc<ResolvedTheme>> {
self.effective.as_ref()
}
pub fn set_mode(&mut self, mode: Mode) {
self.settings.mode = if mode == Mode::Dark { ModeSetting::Dark } else { ModeSetting::Light };
if self.mode != mode {
self.mode = mode;
self.restyle_all = true;
}
}
pub fn mode(&self) -> Mode {
self.mode
}
pub fn settings(&self) -> &SurfaceSettings {
&self.settings
}
pub fn set_settings(&mut self, settings: SurfaceSettings) {
let old = std::mem::replace(&mut self.settings, settings);
let s = self.settings.clone();
if old.strings != s.strings {
self.strings = crate::strings::string_table(&s.strings);
self.needs_build = true;
}
if old.read_only != s.read_only {
self.needs_build = true;
self.restyle_all = true;
}
if old.locale != s.locale || old.today != s.today {
self.needs_build = true;
self.restyle_all = true;
}
if old.density != s.density || old.contrast != s.contrast || old.system_high_contrast != s.system_high_contrast {
self.refresh_theme();
}
let mode = resolve_mode(s.mode, s.system_dark);
if mode != self.mode {
self.mode = mode;
self.restyle_all = true;
}
if (old.font_scale - s.font_scale).abs() > f32::EPSILON || old.hover != s.hover || old.reduced_motion != s.reduced_motion {
self.restyle_all = true;
}
if old.insets != s.insets {
self.engine.mark_all_dirty();
}
}
pub fn set_read_only(&mut self, on: bool) {
let mut s = self.settings.clone();
s.read_only = on;
self.set_settings(s);
}
pub fn set_locale(&mut self, locale: &str) {
let mut s = self.settings.clone();
s.locale = locale.to_string();
self.set_settings(s);
}
pub fn set_strings(&mut self, overrides: IndexMap<String, String>) {
let mut s = self.settings.clone();
s.strings = overrides;
self.set_settings(s);
}
pub fn set_mode_setting(&mut self, mode: ModeSetting, system_dark: bool) {
let mut s = self.settings.clone();
s.mode = mode;
s.system_dark = system_dark;
self.set_settings(s);
}
pub fn set_density(&mut self, density: Density) {
let mut s = self.settings.clone();
s.density = density;
self.set_settings(s);
}
pub fn set_contrast(&mut self, contrast: ContrastSetting, system_high: bool) {
let mut s = self.settings.clone();
s.contrast = contrast;
s.system_high_contrast = system_high;
self.set_settings(s);
}
pub fn set_font_scale(&mut self, scale: f32) {
let mut s = self.settings.clone();
s.font_scale = scale.max(0.1);
self.set_settings(s);
}
pub fn set_insets(&mut self, insets: Insets) {
let mut s = self.settings.clone();
s.insets = insets;
self.set_settings(s);
}
pub fn set_pointer(&mut self, hover: bool, reduced_motion: bool) {
let mut s = self.settings.clone();
s.hover = hover;
s.reduced_motion = reduced_motion;
self.set_settings(s);
}
pub fn set_rounding(&mut self, on: bool) {
self.rounding = on;
self.engine.set_rounding(on);
self.engine.mark_all_dirty();
}
pub fn set_viewport(&mut self, width: f32, height: f32, max_height: Option<f32>) -> bool {
if self.viewport == (width, height) && self.max_height == max_height {
return false;
}
let height_changed = self.viewport.1 != height || self.max_height != max_height;
self.viewport = (width, height);
self.max_height = max_height;
for (i, s) in self.node_state.iter().enumerate() {
if s.conditional && self.live.get(i).copied().unwrap_or(false) {
self.style_dirty.insert(i as u32);
}
}
if (height_changed && !self.lists.is_empty()) || (self.responsive && self.active_breakpoint() != self.breakpoint) {
self.needs_build = true;
}
true
}
pub fn viewport(&self) -> (f32, f32) {
self.viewport
}
fn active_breakpoint(&self) -> Option<String> {
crate::conditions::active_breakpoint(self.viewport.0, &self.breakpoints())
}
fn breakpoints(&self) -> IndexMap<String, f32> {
match &self.effective {
Some(t) if !t.tokens.breakpoint.is_empty() => t.tokens.breakpoint.iter().map(|(k, v)| (k.clone(), *v as f32)).collect(),
_ => crate::conditions::default_breakpoints(),
}
}
pub fn set_states(&mut self, id: &str, states: Vec<String>) -> bool {
let current = self.states.get(id);
if current.is_some_and(|c| *c == states) || (current.is_none() && states.is_empty()) {
return false;
}
if states.is_empty() {
self.states.remove(id);
} else {
self.states.insert(id.to_string(), states);
}
if let Some(&slot) = self.slot_of.get(id) {
self.style_dirty.insert(slot);
self.lend_states(slot);
for target in self.hover_targets(slot) {
let open = self.local.open.get(&target).copied().unwrap_or(false);
let active = self.hover_active(&target);
if active && !open {
self.hover_closing.remove(&target);
let events = self.set_open(&target, true);
self.pending.extend(events.into_iter().filter(|e| !matches!(e, OutEvent::Relayout)));
} else if active {
self.hover_closing.remove(&target);
} else if open && self.settings.hover_close_ms == 0 {
let events = self.set_open(&target, false);
self.pending.extend(events.into_iter().filter(|e| !matches!(e, OutEvent::Relayout)));
} else if open && self.hover_closing.insert(target.clone()) {
self.pending.push(OutEvent::HoverTimer { owner: target, delay_ms: self.settings.hover_close_ms });
}
}
}
true
}
pub fn hover_timeout(&mut self, owner: &str) -> Vec<OutEvent> {
if !self.hover_closing.remove(owner) || !self.opens_on_hover(owner) || self.hover_active(owner) {
return vec![];
}
if !self.local.open.get(owner).copied().unwrap_or(false) {
return vec![];
}
self.set_open(owner, false)
}
fn hover_targets(&self, slot: u32) -> Vec<String> {
let mut out = Vec::new();
let mut cur = Some(slot);
let mut guard = 0;
while let Some(c) = cur {
guard += 1;
if guard > 4096 || !self.live.get(c as usize).copied().unwrap_or(false) {
break;
}
let n = &self.nodes[c as usize];
if let Some(t) = n.trigger_for.as_ref().filter(|_| c == slot) {
if self.opens_on_hover(t) && !out.contains(t) {
out.push(t.clone());
}
}
cur = match n.parent {
Some(p) => Some(p),
None => match self.layers.iter().find(|l| l.root == c) {
Some(l) => {
if self.opens_on_hover(&l.owner) && !out.contains(&l.owner) {
out.push(l.owner.clone());
}
self.slot_of.get(&l.owner).copied()
}
None => None,
},
};
}
out
}
fn hover_active(&self, target: &str) -> bool {
self.states.iter().any(|(id, st)| {
st.iter().any(|s| s == "hover" || s == "focus-visible" || s == "focus")
&& self.slot_of.get(id).is_some_and(|&slot| self.live[slot as usize] && self.hover_targets(slot).iter().any(|t| t == target))
&& (st.iter().any(|s| s == "hover" || s == "focus-visible") || self.in_layer_of(self.slot_of[id], target))
})
}
fn in_layer_of(&self, slot: u32, owner: &str) -> bool {
let Some(layer) = self.layers.iter().find(|l| l.owner == owner) else { return false };
let mut cur = Some(slot);
while let Some(c) = cur {
if c == layer.root {
return true;
}
cur = self.nodes[c as usize].parent;
}
false
}
fn opens_on_hover(&self, target: &str) -> bool {
let Some(&slot) = self.slot_of.get(target) else { return false };
let n = &self.nodes[slot as usize];
n.component == "Tooltip" || (n.component == "Popover" && n.props.get("openOn").and_then(Value::as_str) == Some("hover"))
}
pub fn set_pressed(&mut self, ids: &[String]) -> bool {
self.set_flagged("pressed", ids)
}
pub fn set_hovered(&mut self, ids: &[String]) -> bool {
self.set_flagged("hover", ids)
}
pub fn set_hover(&mut self, id: &str, hovered: bool) -> bool {
let mut s = self.states.get(id).cloned().unwrap_or_default();
let has = s.iter().any(|x| x == "hover");
if has == hovered {
return false;
}
if hovered {
s.push("hover".into());
} else {
s.retain(|x| x != "hover");
}
self.set_states(id, s)
}
pub fn host_states(&self, id: &str) -> &[String] {
self.states.get(id).map(Vec::as_slice).unwrap_or(&[])
}
fn set_flagged(&mut self, flag: &str, ids: &[String]) -> bool {
let mut changed = false;
let keep: HashSet<&String> = ids.iter().collect();
let had: Vec<String> = self.states.iter().filter(|(_, s)| s.iter().any(|x| x == flag)).map(|(k, _)| k.clone()).collect();
for id in had {
if !keep.contains(&id) {
let mut s = self.states.get(&id).cloned().unwrap_or_default();
s.retain(|x| x != flag);
changed |= self.set_states(&id, s);
}
}
for id in ids {
let mut s = self.states.get(id).cloned().unwrap_or_default();
if !s.iter().any(|x| x == flag) {
s.push(flag.into());
changed |= self.set_states(id, s);
}
}
changed
}
pub fn take_events(&mut self) -> Vec<OutEvent> {
std::mem::take(&mut self.pending)
}
pub fn invalidate_measures(&mut self) {
self.memo.clear();
self.memo_versions.clear();
self.archive.clear();
self.engine.mark_all_dirty();
}
pub fn mark_dirty(&mut self, index: u32) -> bool {
if !self.live.get(index as usize).copied().unwrap_or(false) {
return false;
}
self.memo.forget(index);
self.memo_versions.remove(&index);
self.engine.mark_dirty(index);
true
}
pub fn apply(&mut self, message: &Value) -> Result<ApplyOutcome, String> {
let obj = message.as_object().ok_or("message must be an object")?;
if let Some(create) = obj.get("createSurface") {
if let Some(cid) = create.get("catalogId").and_then(Value::as_str) {
self.catalog_id = cid.to_string();
}
if let Some(sid) = create.get("surfaceId").and_then(Value::as_str) {
self.id = sid.to_string();
}
self.set_component_list(Vec::new());
self.nested = None;
self.data = Value::Object(Map::new());
self.data_version += 1;
self.local = LocalState::default();
self.template_cache.clear();
self.reduce();
return Ok(ApplyOutcome { structure_changed: true, issues: self.issues.clone() });
}
if let Some(update) = obj.get("updateComponents") {
let components = update.get("components").ok_or("updateComponents.components is required")?;
let list: Vec<FlatComponent> = serde_json::from_value(components.clone()).map_err(|e| format!("components: {e}"))?;
return Ok(self.update_components(list));
}
self.ensure_reduced();
if let Some(update) = obj.get("updateDataModel") {
let path = update.get("path").and_then(Value::as_str).unwrap_or("");
let value = update.get("value").cloned();
let issues = match self.set_data(path, value) {
Ok(()) => Vec::new(),
Err(message) => vec![ReduceIssue { id: path.to_string(), message }],
};
return Ok(ApplyOutcome { structure_changed: self.needs_build, issues });
}
if obj.get("deleteSurface").is_some() {
self.set_component_list(Vec::new());
self.nested = None;
self.root = None;
self.data = Value::Object(Map::new());
self.data_version += 1;
self.local = LocalState::default();
self.needs_build = true;
return Ok(ApplyOutcome { structure_changed: true, issues: Vec::new() });
}
Err("unknown message: expected createSurface | updateComponents | updateDataModel | deleteSurface".into())
}
pub fn update_components(&mut self, list: Vec<FlatComponent>) -> ApplyOutcome {
if self.nested.is_some() {
self.set_component_list(Vec::new());
}
let mut replaced: HashMap<String, Option<FlatComponent>> = HashMap::new();
let mut order: Vec<String> = Vec::new();
for c in list {
if !replaced.contains_key(&c.id) {
order.push(c.id.clone());
}
match self.component_index.get(&c.id) {
Some(&i) => {
let old = std::mem::replace(&mut self.components[i], c);
replaced.entry(old.id.clone()).or_insert(Some(old));
}
None => {
replaced.entry(c.id.clone()).or_insert(None);
self.component_index.insert(c.id.clone(), self.components.len());
self.components.push(c);
}
}
}
self.nested = None;
self.retain_updated_field_values(&replaced);
self.reduce_pending = true;
self.needs_build = true;
let options = ReduceOptions::new(&self.catalog_id).with_view(self.view.clone());
let (components, index) = (&self.components, &self.component_index);
let lookup = |id: &str| index.get(id).map(|&i| components[i].clone());
let mut issues = Vec::new();
for id in &order {
if let Some(&i) = index.get(id.as_str()) {
issues.extend(crate::reducer::component_issues(&components[i], &options, &lookup));
}
}
ApplyOutcome { structure_changed: true, issues }
}
pub fn set_nested(&mut self, tree: NestedNode) -> ApplyOutcome {
self.nested = Some(tree);
self.set_component_list(Vec::new());
self.template_cache.clear();
self.local.field_values.clear();
self.reduce();
ApplyOutcome { structure_changed: true, issues: self.issues.clone() }
}
pub fn set_components(&mut self, components: Vec<FlatComponent>) -> ApplyOutcome {
let before = if self.nested.is_some() { Vec::new() } else { std::mem::take(&mut self.components) };
self.set_component_list(components);
self.nested = None;
self.template_cache.clear();
self.retain_field_values(&before);
self.reduce();
ApplyOutcome { structure_changed: true, issues: self.issues.clone() }
}
fn set_component_list(&mut self, components: Vec<FlatComponent>) {
self.component_index = components.iter().enumerate().map(|(i, c)| (c.id.clone(), i)).collect();
self.components = components;
}
fn retain_updated_field_values(&mut self, replaced: &HashMap<String, Option<FlatComponent>>) {
if self.local.field_values.is_empty() {
return;
}
let (components, index) = (&self.components, &self.component_index);
self.local.field_values.retain(|key, _| {
let mut candidate = key.as_str();
loop {
if let Some(old) = replaced.get(candidate) {
let new = index.get(candidate).map(|&i| &components[i]);
return matches!((old.as_ref(), new), (Some(a), Some(b)) if a == b);
}
if index.contains_key(candidate) {
return true;
}
match candidate.rfind('.') {
Some(i) => candidate = &candidate[..i],
None => return false,
}
}
});
}
fn retain_field_values(&mut self, before: &[FlatComponent]) {
if self.local.field_values.is_empty() {
return;
}
let old: HashMap<&str, &FlatComponent> = before.iter().map(|c| (c.id.as_str(), c)).collect();
let new: HashMap<&str, &FlatComponent> = self.components.iter().map(|c| (c.id.as_str(), c)).collect();
self.local.field_values.retain(|key, _| {
let mut candidate = key.as_str();
loop {
if old.contains_key(candidate) || new.contains_key(candidate) {
return matches!((old.get(candidate), new.get(candidate)), (Some(a), Some(b)) if a == b);
}
match candidate.rfind('.') {
Some(i) => candidate = &candidate[..i],
None => return false,
}
}
});
}
pub fn set_data(&mut self, path: &str, value: Option<Value>) -> Result<(), String> {
set_pointer(&mut self.data, path, value)?;
self.data_version += 1;
self.needs_build = true;
Ok(())
}
pub fn data(&self) -> &Value {
&self.data
}
pub fn root(&mut self) -> Option<&UiNode> {
self.ensure_reduced();
self.root.as_ref()
}
pub fn issues(&mut self) -> &[ReduceIssue] {
self.ensure_reduced();
&self.issues
}
pub(crate) fn ensure_reduced(&mut self) {
if self.reduce_pending {
self.reduce();
}
}
fn reduce(&mut self) {
crate::roomy(|| self.reduce_now())
}
fn reduce_now(&mut self) {
self.template_cache.clear();
self.local.static_keys.clear();
self.local.key_cache.clear();
let options = ReduceOptions::new(&self.catalog_id).with_view(self.view.clone());
let result: Option<ReduceResult> = if let Some(nested) = &self.nested {
Some(crate::reducer::reduce_nested(nested, &options))
} else if !self.components.is_empty() {
Some(reduce_surface(&self.components, &options))
} else {
None
};
match result {
Some(r) => {
self.root = Some(r.root);
self.reduce_issues = r.issues;
self.template_cache = r.templates.unwrap_or_default().into_iter().map(|(id, n)| (id, Some(n))).collect();
}
None => {
self.root = None;
self.reduce_issues.clear();
}
}
self.build_issues.clear();
self.style_issues.clear();
self.refresh_issues();
self.reduce_pending = false;
self.needs_build = true;
}
pub(crate) fn refresh_issues(&mut self) {
let mut all: Vec<ReduceIssue> = Vec::with_capacity(self.reduce_issues.len() + self.build_issues.len() + self.style_issues.len());
for issue in self.reduce_issues.iter().chain(&self.build_issues).chain(&self.style_issues) {
if !all.contains(issue) {
all.push(issue.clone());
}
}
self.issues = all;
}
fn gap_px(&self, name: &str) -> f32 {
match &self.effective {
Some(t) => t.tokens.spacing.get(name).copied().unwrap_or(0.0) as f32,
None => match name {
"xxs" => 2.0,
"xs" => 4.0,
"sm" => 8.0,
"md" => 12.0,
"lg" => 16.0,
"xl" => 24.0,
_ => 0.0,
},
}
}
pub fn structure_version(&self) -> u64 {
self.structure_version
}
fn placed(&self, i: usize) -> PlacedNode {
let n = &self.nodes[i];
let removed = !self.live[i];
let mut states: Vec<String> = Vec::new();
for q in [&n.part_query, &n.own_query].into_iter().flatten() {
for s in &q.states {
if !states.contains(s) {
states.push(s.clone());
}
}
}
PlacedNode {
index: i as u32,
id: n.id.clone(),
component: n.component.clone(),
part: n.part.clone(),
owner: n.owner.clone(),
owner_component: n.owner_component.clone(),
catalog_id: n.catalog_id.clone(),
extension_kind: n.extension_kind.clone(),
depth: n.depth,
parent: if removed { None } else { n.parent },
children: if removed { Vec::new() } else { n.children.clone() },
layer: n.layer,
kind: n.kind,
props: if removed { Map::new() } else { n.props.clone() },
lines: n.lines,
pressable: n.pressable && !removed,
hidden: n.hidden || removed,
accessibility: n.accessibility.clone(),
trigger_for: n.trigger_for.clone(),
states,
form: n.form.clone(),
live: n.live.clone(),
removed,
}
}
pub fn nodes(&mut self) -> Vec<PlacedNode> {
self.settle();
(0..self.nodes.len()).map(|i| self.placed(i)).collect()
}
pub fn nodes_at(&mut self, indices: &[u32]) -> Vec<PlacedNode> {
self.settle();
indices.iter().filter(|i| (**i as usize) < self.nodes.len()).map(|i| self.placed(*i as usize)).collect()
}
pub fn node_count(&mut self) -> usize {
self.settle();
self.live.iter().filter(|l| **l).count()
}
pub fn visual(&self, index: u32) -> Option<&Visual> {
self.node_state.get(index as usize).map(|s| &s.visual)
}
pub fn visuals(&self) -> Vec<Visual> {
self.node_state.iter().map(|s| s.visual.clone()).collect()
}
pub fn text_style(&self, index: u32) -> Option<&TextStyle> {
self.node_state.get(index as usize).map(|s| &s.text)
}
pub fn layout_node(&self, index: u32) -> Option<&LNode> {
self.nodes.get(index as usize).filter(|_| self.live.get(index as usize).copied().unwrap_or(false))
}
pub fn index_of(&self, id: &str) -> Option<u32> {
self.slot_of.get(id).copied()
}
pub fn hover_styled(&self, index: u32) -> bool {
let Some(n) = self.layout_node(index) else { return false };
if n.base_style.contains_key(":hover") {
return true;
}
let Some(theme) = &self.effective else { return false };
[&n.own_query, &n.part_query].into_iter().flatten().any(|q| crate::theme::recipe_reacts_to(theme, q, "hover"))
}
pub fn last_frame(&self, index: u32) -> Option<Frame> {
self.last_frames.get(index as usize).copied()
}
pub fn direction(&self) -> Direction {
self.direction
}
pub fn is_basic(&self) -> bool {
self.catalog_id == A2UI_BASIC_CATALOG_ID
}
pub fn get_data(&self, path: &str) -> Option<&Value> {
get_pointer(&self.data, path)
}
pub fn strings(&self) -> &StringTable {
&self.strings
}
fn settle(&mut self) {
if self.step_state.is_some() {
return;
}
self.ensure_reduced();
if self.needs_build {
self.subtree_pending.clear();
self.rebuild();
} else {
for id in std::mem::take(&mut self.subtree_pending) {
if !self.rebuild_subtree(&id) {
self.rebuild();
self.subtree_pending.clear();
break;
}
}
}
}
fn state_lender(n: &LNode) -> Option<String> {
let owner = n.owner.as_deref()?;
match (n.owner_component.as_deref(), n.part.as_deref()) {
(Some("Tabs"), Some("close")) => n.id.strip_prefix(&format!("{owner}.close")).map(|i| format!("{owner}.tab{i}")),
_ => None,
}
}
fn lend_states(&mut self, slot: u32) {
let n = &self.nodes[slot as usize];
if let (Some(owner), Some("Tabs"), Some("tab")) = (n.owner.clone(), n.owner_component.as_deref(), n.part.as_deref()) {
if let Some(&close) = n.id.strip_prefix(&format!("{owner}.tab")).and_then(|i| self.slot_of.get(&format!("{owner}.close{i}"))) {
self.style_dirty.insert(close);
}
}
let n = &self.nodes[slot as usize];
let hovered = self.states.get(&n.id).is_some_and(|s| s.iter().any(|x| x == "hover"));
if let (true, Some(owner), Some("Menu"), Some("item")) = (hovered, n.owner.clone(), n.owner_component.as_deref(), n.part.as_deref()) {
let top = n.id.strip_prefix(&format!("{owner}.item.")).filter(|rest| !rest.contains('.')).and_then(|i| i.parse::<usize>().ok());
if let Some(i) = top {
let submenu = n.props.get("kind").and_then(Value::as_str) == Some("submenu");
let open = self.local.submenu.get(&owner).copied();
let next = if submenu { Some(i) } else { None };
if open != next && (submenu || open.is_some()) {
match next {
Some(i) => self.local.submenu.insert(owner.clone(), i),
None => self.local.submenu.remove(&owner),
};
self.needs_build = true;
if !self.pending.iter().any(|e| matches!(e, OutEvent::Relayout)) {
self.pending.push(OutEvent::Relayout);
}
}
}
}
let n = &self.nodes[slot as usize];
if let (true, Some(owner), Some(position), Some("item")) = (hovered, n.owner.clone(), n.props.get("position").and_then(Value::as_u64), n.part.as_deref()) {
let key = match n.owner_component.as_deref() {
Some("SearchList") => Some(owner.clone()),
Some("Menu") => n.id.strip_prefix(&format!("{owner}.item")).and_then(|rest| rest.rsplit_once('.')).map(|(sub, _)| format!("{owner}.search{sub}")),
_ => None,
};
if let Some(key) = key {
if self.local.active.get(&key).copied().unwrap_or(0) != position as usize {
self.local.active.insert(key, position as usize);
self.needs_build = true;
if !self.pending.iter().any(|e| matches!(e, OutEvent::Relayout)) {
self.pending.push(OutEvent::Relayout);
}
}
}
}
}
fn states_of(&self, n: &LNode) -> Vec<String> {
let mut states = self.states.get(&n.id).cloned().unwrap_or_default();
if let Some(lender) = Self::state_lender(n) {
for s in self.states.get(&lender).into_iter().flatten() {
if matches!(s.as_str(), "hover" | "focus" | "focus-visible") && !states.contains(s) {
states.push(s.clone());
}
}
}
if n.props.get("disabled").and_then(Value::as_bool) == Some(true) && !states.iter().any(|s| s == "disabled") {
states.push("disabled".into());
}
for q in [&n.part_query, &n.own_query].into_iter().flatten() {
for s in &q.states {
if !states.contains(s) {
states.push(s.clone());
}
}
}
states
}
}