use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use rhai::{
Array, CustomType, Dynamic, Engine, EvalAltResult, FLOAT, FnPtr, INT, ImmutableString, Map,
NativeCallContext, Position, TypeBuilder,
};
use thiserror::Error;
use crate::{
ActionError, ActionId, ActionInvocation, ActionRegistry, AssetError, AssetId, AssetRegistry,
AsyncRuntimeError, AsyncScope, CalendarClock, CapabilityError, CapabilityId,
CapabilityRegistry, ComponentInstancePath, DateStyle, EventSchema, ImageDecodeHandle,
LocaleError, LocaleManager, MotionKey, MotionRuntime, NumberFormatOptions, OpaqueHandle,
ResponsiveError, ResponsiveRuntime, ScriptCallback, ScriptGeneration, ScriptWindowSpec,
StateError, StateStore, StoreError, StoreId, StoreRegistry, SubscriptionCloseReason,
SubscriptionDeliveryPolicy, SubscriptionHandle, SubscriptionOptions, SubscriptionRegistration,
SubscriptionRegistry, TaskHandle, TaskRegistry, TextDirection, ThemeError, ThemeManager,
ThemePreference, ThemeSelection, UiEvent, UiValue, UiValueError, UiValuePath, UiValuePathError,
UiValuePathSegment, WindowCommandError, WindowCommandRegistry,
};
fn domain_is_within(domain: &str, scope: &str) -> bool {
domain == scope
|| domain
.strip_prefix(scope)
.is_some_and(|suffix| suffix.starts_with('/'))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExecutionPhase {
Init,
Render,
Event,
Suspend,
Resume,
Dispose,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ComponentIncarnation(u64);
impl ComponentIncarnation {
pub(crate) const fn unscoped() -> Self {
Self(0)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct RuntimeStateId(u64);
impl Default for RuntimeStateId {
fn default() -> Self {
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
}
}
enum ThemeTarget {
App,
Window(String),
Local,
}
impl ExecutionPhase {
const fn allows_mutation(self) -> bool {
matches!(
self,
Self::Init | Self::Event | Self::Suspend | Self::Resume | Self::Dispose
)
}
const fn allows_async_start(self) -> bool {
matches!(self, Self::Init | Self::Event | Self::Resume)
}
}
#[derive(Debug, Default)]
pub struct UiRuntimeState {
runtime_id: RuntimeStateId,
transaction_depth: usize,
pub component_state: StateStore,
pub stores: StoreRegistry,
pub native_collections: crate::NativeCollectionRegistry,
pub native_documents: crate::NativeTextDocumentRegistry,
#[cfg(feature = "charts")]
pub native_chart_data: BTreeMap<String, crate::NativeChartData>,
pub actions: ActionRegistry,
pub capabilities: CapabilityRegistry,
pub tasks: TaskRegistry,
pub subscriptions: SubscriptionRegistry,
pub timers: crate::TimerRegistry,
pub locale: Option<LocaleManager>,
pub calendar_clock: CalendarClock,
pub clock: crate::RuntimeClock,
pub theme: Option<ThemeManager>,
component_styles: crate::ComponentStyleSheet,
pub assets: AssetRegistry,
pub motions: MotionRuntime,
pub effects: crate::EffectRegistry,
pub signals: crate::SignalRegistry,
pub element_refs: crate::ElementRefRegistry,
pub(crate) geometry: crate::GeometryRegistry,
pub(crate) pointer_capture: crate::PointerCaptureRegistry,
presentations: BTreeMap<String, ViewPresentationState>,
pub budgets: crate::RuntimeBudgets,
pub virtual_requests: crate::VirtualRequestRegistry,
pub windows: WindowCommandRegistry,
pub responsive: ResponsiveRuntime,
pub(crate) environment_dependencies:
crate::environment_dependency::EnvironmentDependencyRegistry,
pub motion_values: BTreeMap<MotionKey, f64>,
pub(crate) motion_ghosts: Vec<crate::motion::MotionGhost>,
pub(crate) window_appearances: BTreeMap<String, crate::SystemAppearance>,
pub traces: crate::TraceBuffer,
component_event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
component_incarnations: BTreeMap<ComponentInstancePath, ComponentIncarnation>,
next_component_incarnation: u64,
dirty: BTreeSet<ComponentInstancePath>,
pending_events: Vec<PendingEvent>,
pending_actions: Vec<ActionInvocation>,
pending_async: Vec<crate::AsyncDelivery>,
pending_element_commands: Vec<crate::element_ref::ElementCommand>,
repaint_windows: BTreeSet<String>,
component_style_generation: u64,
}
impl UiRuntimeState {
pub(crate) fn update_window_appearance(
&mut self,
window: &str,
appearance: crate::SystemAppearance,
) -> bool {
let previous = self
.window_appearances
.insert(window.to_owned(), appearance)
.unwrap_or(crate::SystemAppearance::Dark);
let changed = previous != appearance;
if changed {
let invalidated = self
.environment_dependencies
.invalidate_theme_window(window);
self.mark_dirty(invalidated);
}
changed
}
const SUSPENDED_DELIVERY_CAPACITY: usize = 256;
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub(crate) fn ensure_component_incarnation(
&mut self,
component: &ComponentInstancePath,
) -> ComponentIncarnation {
if let Some(incarnation) = self.component_incarnations.get(component) {
return *incarnation;
}
let next = self.next_component_incarnation.max(1);
let incarnation = ComponentIncarnation(next);
self.next_component_incarnation = next.saturating_add(1);
self.component_incarnations
.insert(component.clone(), incarnation);
incarnation
}
pub(crate) fn component_incarnation(
&self,
component: &ComponentInstancePath,
) -> Option<ComponentIncarnation> {
self.component_incarnations.get(component).copied()
}
pub(crate) fn component_incarnations_snapshot(
&self,
) -> BTreeMap<ComponentInstancePath, ComponentIncarnation> {
self.component_incarnations.clone()
}
pub(crate) fn ensure_presentation(&mut self, view: &str) {
self.presentations.entry(view.to_owned()).or_default();
}
pub fn presentation_geometry(&mut self, view: &str) -> crate::GeometryRegistry {
self.ensure_presentation(view);
self.geometry_for(Some(view))
}
pub fn presentation_pointer_capture(&mut self, view: &str) -> crate::PointerCaptureRegistry {
self.ensure_presentation(view);
self.pointer_capture_for(Some(view))
}
pub(crate) fn geometry_for(&self, view: Option<&str>) -> crate::GeometryRegistry {
view.and_then(|view| self.presentations.get(view))
.map_or_else(|| self.geometry.clone(), |state| state.geometry.clone())
}
pub(crate) fn pointer_capture_for(&self, view: Option<&str>) -> crate::PointerCaptureRegistry {
view.and_then(|view| self.presentations.get(view))
.map_or_else(
|| self.pointer_capture.clone(),
|state| state.pointer_capture.clone(),
)
}
pub(crate) fn remove_presentation(&mut self, view: &str) {
self.presentations.remove(view);
}
pub fn set_component_state_from_host(
&mut self,
component: &ComponentInstancePath,
field: &str,
value: UiValue,
) -> Result<bool, crate::StateError> {
let changed = self.component_state.set(component, field, value)?;
if changed {
self.dirty.insert(component.clone());
}
Ok(changed)
}
pub fn replace_native_collection_from_host(
&mut self,
name: &str,
collection: crate::NativeCollection,
) -> Result<bool, crate::NativeCollectionError> {
let invalidated = self.native_collections.replace(name, collection)?;
let changed = !invalidated.is_empty();
self.dirty.extend(invalidated);
Ok(changed)
}
pub fn register_native_collection_from_host(
&mut self,
_root: &ComponentInstancePath,
name: &str,
collection: crate::NativeCollection,
) -> Result<(), crate::NativeCollectionError> {
let invalidated = self
.native_collections
.register_with_invalidated(name.to_owned(), collection)?;
self.dirty.extend(invalidated);
Ok(())
}
pub fn replace_native_text_document_from_host(
&mut self,
name: &str,
document: crate::NativeTextDocument,
) -> Result<bool, crate::DocumentError> {
let invalidated = self.native_documents.replace(name, document)?;
let changed = !invalidated.is_empty();
self.dirty.extend(invalidated);
Ok(changed)
}
#[cfg(feature = "charts")]
pub fn register_native_chart_data(
&mut self,
name: impl Into<String>,
data: crate::NativeChartData,
) -> Result<(), UiContextError> {
let name = name.into();
if name.is_empty()
|| name.len() > 128
|| !name
.chars()
.next()
.is_some_and(|character| character.is_ascii_alphabetic())
|| !name
.chars()
.all(|character| character.is_ascii_alphanumeric() || character == '_')
{
return Err(UiContextError::InvalidChartDataName(name));
}
self.native_chart_data.insert(name, data);
Ok(())
}
pub fn replace_theme_variant_from_host(
&mut self,
variant: crate::ThemeVariant,
) -> Result<(), crate::ThemeError> {
self.theme
.as_mut()
.ok_or(crate::ThemeError::NoVariants)?
.replace_variant(variant)?;
self.dirty.extend(self.component_state.paths());
Ok(())
}
pub fn select_theme_from_host(
&mut self,
family: &str,
variant: &str,
) -> Result<bool, crate::ThemeError> {
let theme = self.theme.as_mut().ok_or(crate::ThemeError::NoVariants)?;
let previous = theme.generation();
theme.set_app(ThemePreference::Fixed {
selection: ThemeSelection::new(family, variant),
})?;
let changed = theme.generation() != previous;
if changed {
let invalidated = self.environment_dependencies.invalidate_theme_app();
self.dirty.extend(invalidated);
self.mark_all_windows_dirty();
}
Ok(changed)
}
pub fn select_locale_from_host(&mut self, locale: &str) -> Result<bool, crate::LocaleError> {
let locales = self
.locale
.as_mut()
.ok_or_else(|| crate::LocaleError::UnknownLocale(locale.to_owned()))?;
let previous = locales.generation();
locales.set_app(locale)?;
let changed = locales.generation() != previous;
if changed {
let invalidated = self.environment_dependencies.invalidate_locale_app();
self.dirty.extend(invalidated);
self.mark_all_windows_repaint();
}
Ok(changed)
}
pub fn set_motion_preference_from_host(&mut self, preference: crate::MotionPreference) -> bool {
let previous = self.motions.preference();
self.motions.set_preference(preference);
let changed = self.motions.preference() != previous;
if changed {
let now = self.clock.now();
self.motion_values = self.motions.snapshot(now);
self.mark_all_windows_dirty();
}
changed
}
pub fn replace_component_styles_from_host(
&mut self,
styles: crate::ComponentStyleSheet,
) -> bool {
if self.component_styles == styles {
return false;
}
self.component_styles = styles;
self.component_style_generation = self.component_style_generation.saturating_add(1);
self.dirty.extend(self.component_state.paths());
true
}
pub(crate) const fn component_style_generation(&self) -> u64 {
self.component_style_generation
}
pub(crate) const fn component_styles(&self) -> &crate::ComponentStyleSheet {
&self.component_styles
}
pub(crate) fn trace_subscription_closures(&mut self) {
for closure in self.subscriptions.take_closures() {
let scope = match closure.scope {
AsyncScope::App => "/App".to_owned(),
AsyncScope::Window(window) => format!("window:{window}"),
AsyncScope::Component(component) => component.to_string(),
AsyncScope::Effect {
component,
key,
activation,
} => format!("{component}/effect[{key}]#{activation}"),
};
self.traces.push(
crate::RuntimeTraceKind::Subscription,
scope,
format!("close {}: {}", closure.label, closure.reason),
None,
false,
);
}
}
pub fn release_window(
&mut self,
window: &str,
root: &ComponentInstancePath,
) -> Result<(), AssetError> {
self.tasks
.cancel_scope(&AsyncScope::Window(window.to_owned()));
self.tasks.cancel_component_scope(root);
self.timers.cancel_component_scope(root);
self.subscriptions
.cancel_scope(&AsyncScope::Window(window.to_owned()));
self.subscriptions.cancel_component_scope(root);
self.assets.cancel_window_scope(window, root)?;
self.stores.remove_window(window);
self.native_collections.remove_reader_scope(root);
self.native_documents.remove_reader_scope(root);
self.component_state.remove_scope(root);
self.component_event_handlers
.retain(|(path, _), _| !path.is_within(root));
self.component_incarnations
.retain(|path, _| !path.is_within(root));
self.actions.remove_component_scope(root);
self.dirty.retain(|path| !path.is_within(root));
self.pending_events
.retain(|event| !event.target.is_within(root));
self.pending_actions.retain(|action| {
!action
.callback
.component()
.is_some_and(|path| path.is_within(root))
});
self.pending_async.retain(|delivery| match &delivery.scope {
AsyncScope::App => true,
AsyncScope::Window(id) => id != window,
AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
!delivery.scope.is_within_component(root)
}
});
self.pending_element_commands
.retain(|command| command.window() != window);
self.environment_dependencies.remove_window(window);
self.environment_dependencies.remove_scope(root);
self.repaint_windows.remove(window);
self.window_appearances.remove(window);
if let Some(theme) = self.theme.as_mut() {
theme.remove_window(window);
theme.remove_scope(root);
}
if let Some(locale) = self.locale.as_mut() {
locale.remove_window(window);
locale.remove_scope(root);
}
let window_scope = format!("window:{window}");
self.motions.release_node_scope(&window_scope);
let removed_ghosts = self
.motion_ghosts
.iter()
.filter(|ghost| domain_is_within(&ghost.domain, &window_scope))
.map(|ghost| ghost.path.clone())
.collect::<Vec<_>>();
self.motion_ghosts
.retain(|ghost| !domain_is_within(&ghost.domain, &window_scope));
for path in removed_ghosts {
self.motions.cancel_node_scope(&path);
}
self.motions.discard_timeline_events_in_scope(&window_scope);
self.motion_values = self.motions.snapshot(self.clock.now());
self.effects.remove_scope(root);
self.signals.remove_scope(root);
self.element_refs.remove_scope(root);
self.geometry.remove_scope(root);
for presentation in self.presentations.values() {
presentation.geometry.remove_scope(root);
}
self.windows.remove(window);
self.responsive.remove_window(window);
Ok(())
}
pub(crate) fn queue_async(
&mut self,
deliveries: impl IntoIterator<Item = crate::AsyncDelivery>,
) {
self.pending_async.extend(deliveries);
}
pub(crate) fn queue_suspended_async(
&mut self,
deliveries: impl IntoIterator<Item = crate::AsyncDelivery>,
) -> Result<(), AsyncRuntimeError> {
for delivery in deliveries {
if self.pending_async.len() >= Self::SUSPENDED_DELIVERY_CAPACITY {
return Err(AsyncRuntimeError::SuspendedBackpressure {
capacity: Self::SUSPENDED_DELIVERY_CAPACITY,
});
}
self.pending_async.push(delivery);
}
Ok(())
}
pub(crate) fn suspended_delivery_capacity_remaining(&self) -> usize {
Self::SUSPENDED_DELIVERY_CAPACITY.saturating_sub(self.pending_async.len())
}
pub(crate) fn cancel_async_scope(&mut self, scope: &AsyncScope) -> Result<(), AssetError> {
self.assets.cancel_scope(scope)?;
self.tasks.cancel_scope(scope);
self.subscriptions.cancel_scope(scope);
self.pending_async
.retain(|delivery| &delivery.scope != scope);
self.trace_subscription_closures();
Ok(())
}
pub(crate) fn take_window_async(
&mut self,
window: &str,
root: &ComponentInstancePath,
) -> Vec<crate::AsyncDelivery> {
let app_owner = self.windows.open_ids().into_iter().next();
let pending = std::mem::take(&mut self.pending_async);
let (accepted, retained) =
pending
.into_iter()
.partition(|delivery| match &delivery.scope {
AsyncScope::App => app_owner.as_deref() == Some(window),
AsyncScope::Window(id) => id == window,
AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
delivery.scope.is_within_component(root)
}
});
self.pending_async = retained;
accepted
}
pub(crate) fn discard_component_async_before_generation(
&mut self,
root: &ComponentInstancePath,
generation: crate::ScriptGeneration,
) {
self.pending_async.retain(|delivery| {
!delivery.scope.is_within_component(root)
|| delivery.callback.generation() == generation
});
}
pub(crate) fn take_window_element_commands(
&mut self,
window: &str,
) -> Vec<crate::element_ref::ElementCommand> {
let commands = std::mem::take(&mut self.pending_element_commands);
let (selected, retained) = commands
.into_iter()
.partition(|command| command.window() == window);
self.pending_element_commands = retained;
selected
}
pub(crate) fn has_window_dirty(&self, root: &ComponentInstancePath) -> bool {
self.dirty.iter().any(|path| path.is_within(root))
}
pub(crate) fn flush_geometry_dependencies(&mut self) {
self.dirty.extend(self.element_refs.take_dirty());
self.dirty.extend(self.geometry.take_dirty());
for presentation in self.presentations.values() {
self.dirty.extend(presentation.geometry.take_dirty());
}
}
pub(crate) fn take_window_dirty_components(
&mut self,
root: &ComponentInstancePath,
) -> BTreeSet<ComponentInstancePath> {
let all = std::mem::take(&mut self.dirty);
let (selected, retained) = all.into_iter().partition(|path| path.is_within(root));
self.dirty = retained;
selected
}
pub(crate) fn drain_pending_actions(&mut self) -> Vec<ActionInvocation> {
std::mem::take(&mut self.pending_actions)
}
pub(crate) fn drain_pending_events(&mut self) -> Vec<PendingEvent> {
std::mem::take(&mut self.pending_events)
}
pub(crate) fn has_pending_dispatch(&self) -> bool {
!self.pending_actions.is_empty() || !self.pending_events.is_empty()
}
pub(crate) fn has_virtual_requests(&self) -> bool {
!self.virtual_requests.is_empty()
}
pub(crate) fn component_event_handler(
&self,
component: &ComponentInstancePath,
event: &str,
) -> Option<ScriptCallback> {
self.component_event_handlers
.get(&(component.clone(), event.to_owned()))
.cloned()
}
pub(crate) fn reset_component_readers(&mut self, component: &ComponentInstancePath) {
self.stores.reset_reader(component);
self.native_collections.reset_reader(component);
self.native_documents.reset_reader(component);
self.environment_dependencies.reset_reader(component);
self.reset_geometry_read_contribution(&crate::read_dependency::ReadDependency::component(
component,
));
}
pub(crate) fn retain_virtual_read_contributions(
&mut self,
scope: &ComponentInstancePath,
active: &BTreeSet<crate::read_dependency::ReadContribution>,
) {
self.native_collections.retain_contributions(scope, active);
self.native_documents.retain_contributions(scope, active);
self.stores.retain_contributions(scope, active);
self.environment_dependencies
.retain_contributions(scope, active);
self.element_refs.retain_contributions(scope, active);
self.geometry.retain_contributions(scope, active);
for presentation in self.presentations.values() {
presentation.geometry.retain_contributions(scope, active);
}
}
fn reset_geometry_read_contribution(
&mut self,
reader: &crate::read_dependency::ReadDependency,
) {
self.element_refs.reset_contribution(reader);
self.geometry.reset_contribution(reader);
for presentation in self.presentations.values() {
presentation.geometry.reset_contribution(reader);
}
}
pub(crate) fn component_event_handlers_in_scope(
&self,
root: &ComponentInstancePath,
) -> BTreeMap<(ComponentInstancePath, String), ScriptCallback> {
self.component_event_handlers
.iter()
.filter(|((path, _), _)| path.is_within(root))
.map(|(id, callback)| (id.clone(), callback.clone()))
.collect()
}
pub(crate) fn replace_component_event_handlers(
&mut self,
root: &ComponentInstancePath,
handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
) {
self.component_event_handlers
.retain(|(path, _), _| !path.is_within(root));
self.component_event_handlers.extend(handlers);
}
pub(crate) fn mark_all_windows_dirty(&mut self) {
self.dirty.extend(
self.windows
.open_ids()
.into_iter()
.map(|window| ComponentInstancePath::root("App", window)),
);
}
pub(crate) fn mark_dirty(
&mut self,
components: impl IntoIterator<Item = ComponentInstancePath>,
) {
self.dirty.extend(components);
}
pub(crate) fn mark_all_windows_repaint(&mut self) {
self.repaint_windows.extend(self.windows.open_ids());
}
pub(crate) fn mark_window_repaint(&mut self, window: impl Into<String>) {
self.repaint_windows.insert(window.into());
}
pub(crate) fn take_window_repaint(&mut self, window: &str) -> bool {
self.repaint_windows.remove(window)
}
pub(crate) fn reconcile_component_lifetimes(
&mut self,
root: &ComponentInstancePath,
active: &BTreeSet<ComponentInstancePath>,
previous: &BTreeSet<ComponentInstancePath>,
) -> Result<(), AssetError> {
for removed in previous
.iter()
.filter(|path| path.is_within(root) && *path != root && !active.contains(*path))
{
self.tasks.cancel_component_scope(removed);
self.subscriptions.cancel_component_scope(removed);
self.timers.cancel_component_scope(removed);
self.assets.cancel_component_scope(removed)?;
self.actions.remove_component_scope(removed);
}
self.pending_async.retain(|delivery| {
delivery
.scope
.component()
.is_none_or(|path| !path.is_within(root) || active.contains(path))
});
self.pending_actions.retain(|action| {
!action
.callback
.component()
.is_some_and(|path| path.is_within(root) && !active.contains(path))
});
self.pending_events.retain(|event| {
!event.target.is_within(root) || active.contains(&event.target) || event.target == *root
});
self.stores.retain_reader_scope(root, active);
self.component_event_handlers
.retain(|(path, _), _| !path.is_within(root) || path == root || active.contains(path));
self.native_collections.retain_reader_scope(root, active);
self.native_documents.retain_reader_scope(root, active);
self.element_refs.retain_scope(root, active);
self.geometry.retain_reader_scope(root, active);
for presentation in self.presentations.values() {
presentation.geometry.retain_reader_scope(root, active);
}
self.environment_dependencies.retain_scope(root, active);
self.dirty
.retain(|path| !path.is_within(root) || path == root || active.contains(path));
self.component_incarnations
.retain(|path, _| !path.is_within(root) || path == root || active.contains(path));
Ok(())
}
#[must_use]
pub fn drain_batch(&mut self) -> UiMutationBatch {
UiMutationBatch {
dirty: std::mem::take(&mut self.dirty),
events: std::mem::take(&mut self.pending_events),
actions: std::mem::take(&mut self.pending_actions),
}
}
#[must_use]
pub fn dirty_components(&self) -> &BTreeSet<ComponentInstancePath> {
&self.dirty
}
pub(crate) fn snapshot(&self) -> Result<UiStateSnapshot, AssetError> {
Ok(UiStateSnapshot {
runtime_id: self.runtime_id,
transaction_depth: self.transaction_depth,
component_state: self.component_state.clone(),
stores: self.stores.clone(),
native_collections: self.native_collections.clone(),
native_documents: self.native_documents.clone(),
actions: self.actions.clone(),
component_event_handlers: self.component_event_handlers.clone(),
component_incarnations: self.component_incarnations.clone(),
next_component_incarnation: self.next_component_incarnation,
dirty: self.dirty.clone(),
pending_events: self.pending_events.clone(),
pending_actions: self.pending_actions.clone(),
pending_async: self.pending_async.clone(),
pending_element_commands: self.pending_element_commands.clone(),
locale: self.locale.clone(),
theme: self.theme.clone(),
component_styles: self.component_styles.clone(),
component_style_generation: self.component_style_generation,
motions: self.motions.transaction_snapshot(),
effects: self.effects.clone(),
signals: self.signals.clone(),
element_refs: self.element_refs.clone(),
geometry: self.geometry.snapshot(),
pointer_capture: self.pointer_capture.snapshot(),
presentations: self
.presentations
.iter()
.map(|(view, state)| (view.clone(), state.snapshot()))
.collect(),
budgets: self.budgets.clone(),
virtual_requests: self.virtual_requests.snapshot(),
motion_values: self.motion_values.clone(),
motion_ghosts: self.motion_ghosts.clone(),
windows: self.windows.clone(),
responsive: self.responsive.clone(),
environment_dependencies: self.environment_dependencies.clone(),
repaint_windows: self.repaint_windows.clone(),
tasks: self.tasks.snapshot(),
subscriptions: self.subscriptions.snapshot(),
timers: self.timers.clone(),
image_decodes: self.assets.decode_snapshot()?,
})
}
pub(crate) fn restore(&mut self, snapshot: UiStateSnapshot) -> Result<(), UiTransactionError> {
if self.runtime_id != snapshot.runtime_id {
return Err(UiTransactionError::ForeignSnapshot);
}
let expected_depth = snapshot.transaction_depth.saturating_add(1);
if self.transaction_depth != expected_depth {
return Err(UiTransactionError::Unbalanced {
expected: expected_depth,
actual: self.transaction_depth,
});
}
self.tasks.restore(snapshot.tasks);
self.subscriptions.restore(snapshot.subscriptions);
self.timers = snapshot.timers;
let decode_result = self.assets.restore_decode_snapshot(snapshot.image_decodes);
self.component_state = snapshot.component_state;
self.stores = snapshot.stores;
self.native_collections = snapshot.native_collections;
self.native_documents = snapshot.native_documents;
self.actions = snapshot.actions;
self.component_event_handlers = snapshot.component_event_handlers;
self.component_incarnations = snapshot.component_incarnations;
self.next_component_incarnation = snapshot.next_component_incarnation;
self.dirty = snapshot.dirty;
self.pending_events = snapshot.pending_events;
self.pending_actions = snapshot.pending_actions;
self.pending_async = snapshot.pending_async;
self.pending_element_commands = snapshot.pending_element_commands;
self.locale = snapshot.locale;
self.theme = snapshot.theme;
self.component_styles = snapshot.component_styles;
self.component_style_generation = snapshot.component_style_generation;
self.motions = snapshot.motions;
self.effects = snapshot.effects;
self.signals = snapshot.signals;
self.element_refs = snapshot.element_refs;
self.geometry.restore(snapshot.geometry);
self.pointer_capture.restore(snapshot.pointer_capture);
self.presentations
.retain(|view, _| snapshot.presentations.contains_key(view));
for (view, presentation) in snapshot.presentations {
let state = self.presentations.entry(view).or_default();
state.geometry.restore(presentation.geometry);
state.pointer_capture.restore(presentation.pointer_capture);
}
self.budgets = snapshot.budgets;
self.virtual_requests.restore(snapshot.virtual_requests);
self.motion_values = snapshot.motion_values;
self.motion_ghosts = snapshot.motion_ghosts;
self.windows = snapshot.windows;
self.responsive = snapshot.responsive;
self.environment_dependencies = snapshot.environment_dependencies;
self.repaint_windows = snapshot.repaint_windows;
self.transaction_depth = snapshot.transaction_depth;
decode_result.map_err(UiTransactionError::from)
}
pub fn begin_transaction(&mut self) -> Result<UiStateSnapshot, UiTransactionError> {
let snapshot = self.snapshot()?;
let _ = self.assets.begin_transaction()?;
self.tasks.begin_transaction();
self.subscriptions.begin_transaction();
self.transaction_depth = self.transaction_depth.saturating_add(1);
Ok(snapshot)
}
pub fn commit_transaction(&mut self) -> Result<(), UiTransactionError> {
if self.transaction_depth == 0 {
return Err(UiTransactionError::Unbalanced {
expected: 1,
actual: 0,
});
}
self.assets.commit_transaction()?;
self.tasks.commit_transaction();
self.subscriptions.commit_transaction();
self.transaction_depth -= 1;
Ok(())
}
pub fn rollback_transaction(
&mut self,
snapshot: UiStateSnapshot,
) -> Result<(), UiTransactionError> {
self.restore(snapshot)
}
}
#[derive(Debug, Default)]
struct ViewPresentationState {
geometry: crate::GeometryRegistry,
pointer_capture: crate::PointerCaptureRegistry,
}
impl ViewPresentationState {
fn snapshot(&self) -> ViewPresentationSnapshot {
ViewPresentationSnapshot {
geometry: self.geometry.snapshot(),
pointer_capture: self.pointer_capture.snapshot(),
}
}
}
#[derive(Clone, Debug)]
struct ViewPresentationSnapshot {
geometry: crate::geometry::GeometrySnapshot,
pointer_capture: BTreeMap<u64, crate::NodeId>,
}
#[derive(Debug, Error)]
pub enum UiTransactionError {
#[error("transaction snapshot belongs to another UiRuntimeState")]
ForeignSnapshot,
#[error("unbalanced runtime transaction depth: expected {expected}, got {actual}")]
Unbalanced { expected: usize, actual: usize },
#[error(transparent)]
Asset(#[from] AssetError),
}
pub struct UiStateSnapshot {
runtime_id: RuntimeStateId,
transaction_depth: usize,
component_state: StateStore,
stores: StoreRegistry,
native_collections: crate::NativeCollectionRegistry,
native_documents: crate::NativeTextDocumentRegistry,
actions: ActionRegistry,
component_event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
component_incarnations: BTreeMap<ComponentInstancePath, ComponentIncarnation>,
next_component_incarnation: u64,
dirty: BTreeSet<ComponentInstancePath>,
pending_events: Vec<PendingEvent>,
pending_actions: Vec<ActionInvocation>,
pending_async: Vec<crate::AsyncDelivery>,
pending_element_commands: Vec<crate::element_ref::ElementCommand>,
locale: Option<LocaleManager>,
theme: Option<ThemeManager>,
component_styles: crate::ComponentStyleSheet,
component_style_generation: u64,
motions: MotionRuntime,
effects: crate::EffectRegistry,
signals: crate::SignalRegistry,
element_refs: crate::ElementRefRegistry,
geometry: crate::geometry::GeometrySnapshot,
pointer_capture: BTreeMap<u64, crate::NodeId>,
presentations: BTreeMap<String, ViewPresentationSnapshot>,
budgets: crate::RuntimeBudgets,
virtual_requests: crate::virtual_list::VirtualRequestSnapshot,
motion_values: BTreeMap<MotionKey, f64>,
motion_ghosts: Vec<crate::motion::MotionGhost>,
windows: WindowCommandRegistry,
responsive: ResponsiveRuntime,
environment_dependencies: crate::environment_dependency::EnvironmentDependencyRegistry,
repaint_windows: BTreeSet<String>,
tasks: crate::async_runtime::TaskRegistrySnapshot,
subscriptions: crate::async_runtime::SubscriptionRegistrySnapshot,
timers: crate::TimerRegistry,
image_decodes: crate::asset::ImageDecodeSnapshot,
}
impl Clone for UiStateSnapshot {
fn clone(&self) -> Self {
Self {
runtime_id: self.runtime_id,
transaction_depth: self.transaction_depth,
component_state: self.component_state.clone(),
stores: self.stores.clone(),
native_collections: self.native_collections.clone(),
native_documents: self.native_documents.clone(),
actions: self.actions.clone(),
component_event_handlers: self.component_event_handlers.clone(),
component_incarnations: self.component_incarnations.clone(),
next_component_incarnation: self.next_component_incarnation,
dirty: self.dirty.clone(),
pending_events: self.pending_events.clone(),
pending_actions: self.pending_actions.clone(),
pending_async: self.pending_async.clone(),
pending_element_commands: self.pending_element_commands.clone(),
locale: self.locale.clone(),
theme: self.theme.clone(),
component_styles: self.component_styles.clone(),
component_style_generation: self.component_style_generation,
motions: self.motions.transaction_snapshot(),
effects: self.effects.clone(),
signals: self.signals.clone(),
element_refs: self.element_refs.clone(),
geometry: self.geometry.clone(),
pointer_capture: self.pointer_capture.clone(),
presentations: self.presentations.clone(),
budgets: self.budgets.clone(),
virtual_requests: self.virtual_requests.clone(),
motion_values: self.motion_values.clone(),
motion_ghosts: self.motion_ghosts.clone(),
windows: self.windows.clone(),
responsive: self.responsive.clone(),
environment_dependencies: self.environment_dependencies.clone(),
repaint_windows: self.repaint_windows.clone(),
tasks: self.tasks.clone(),
subscriptions: self.subscriptions.clone(),
timers: self.timers.clone(),
image_decodes: self.image_decodes.clone(),
}
}
}
impl UiStateSnapshot {
pub(crate) const fn component_state(&self) -> &StateStore {
&self.component_state
}
}
#[derive(Clone, Debug)]
pub struct PendingEvent {
pub target: ComponentInstancePath,
pub event: UiEvent,
}
#[derive(Clone, Debug, Default)]
pub struct UiMutationBatch {
pub dirty: BTreeSet<ComponentInstancePath>,
pub events: Vec<PendingEvent>,
pub actions: Vec<ActionInvocation>,
}
#[derive(Clone, Debug)]
pub struct UiContext {
runtime: Rc<RefCell<UiRuntimeState>>,
component: ComponentInstancePath,
incarnation: ComponentIncarnation,
callback_component: ComponentInstancePath,
callback_incarnation: ComponentIncarnation,
callback_events: BTreeMap<String, EventSchema>,
read_dependency: crate::read_dependency::ReadDependency,
window: Option<String>,
view: Option<String>,
phase: ExecutionPhase,
events: BTreeMap<String, EventSchema>,
generation: ScriptGeneration,
native_context: Option<crate::invocation::ScriptInvocationContext>,
async_scope: Option<AsyncScope>,
component_style: Option<crate::Style>,
component_part_styles: BTreeMap<String, crate::Style>,
global_component_part_styles: BTreeMap<String, crate::Style>,
non_reusable_render_reads: Rc<RefCell<BTreeSet<ComponentInstancePath>>>,
event_target: Option<crate::GeometryBounds>,
}
impl UiContext {
#[must_use]
pub fn new(
runtime: Rc<RefCell<UiRuntimeState>>,
component: ComponentInstancePath,
window: Option<String>,
phase: ExecutionPhase,
events: BTreeMap<String, EventSchema>,
) -> Self {
if let Some(window) = window.as_deref()
&& let Ok(mut runtime) = runtime.try_borrow_mut()
{
runtime.ensure_presentation(window);
}
let incarnation = runtime
.try_borrow_mut()
.map(|mut runtime| runtime.ensure_component_incarnation(&component))
.unwrap_or_default();
let read_dependency = crate::read_dependency::ReadDependency::component(&component);
let context = Self {
runtime,
component: component.clone(),
incarnation,
callback_component: component,
read_dependency,
callback_incarnation: incarnation,
callback_events: events.clone(),
window,
view: None,
phase,
events,
generation: ScriptGeneration::default(),
native_context: None,
async_scope: None,
component_style: None,
component_part_styles: BTreeMap::new(),
global_component_part_styles: BTreeMap::new(),
non_reusable_render_reads: Rc::new(RefCell::new(BTreeSet::new())),
event_target: None,
};
if phase == ExecutionPhase::Render
&& let Ok(mut runtime) = context.runtime.try_borrow_mut()
{
runtime.reset_component_readers(&context.component);
}
context
}
#[must_use]
pub fn with_generation(mut self, generation: ScriptGeneration) -> Self {
self.generation = generation;
self
}
#[must_use]
pub fn with_view_id(mut self, view: impl Into<String>) -> Self {
let view = view.into();
if let Ok(mut runtime) = self.runtime.try_borrow_mut() {
runtime.ensure_presentation(&view);
}
self.view = Some(view);
self
}
#[must_use]
pub(crate) fn with_optional_view_id(mut self, view: Option<String>) -> Self {
if let Some(view) = view.as_deref()
&& let Ok(mut runtime) = self.runtime.try_borrow_mut()
{
runtime.ensure_presentation(view);
}
self.view = view;
self
}
pub(crate) fn for_component(
&self,
component: ComponentInstancePath,
events: BTreeMap<String, EventSchema>,
) -> Self {
let incarnation = self
.runtime
.try_borrow_mut()
.map(|mut runtime| runtime.ensure_component_incarnation(&component))
.unwrap_or_default();
let read_dependency = crate::read_dependency::ReadDependency::component(&component);
let context = Self {
runtime: Rc::clone(&self.runtime),
component: component.clone(),
incarnation,
callback_component: component,
read_dependency,
callback_incarnation: incarnation,
callback_events: events.clone(),
window: self.window.clone(),
view: self.view.clone(),
phase: self.phase,
events,
generation: self.generation,
native_context: self.native_context.clone(),
async_scope: self.async_scope.clone(),
component_style: self.component_style.clone(),
component_part_styles: self.component_part_styles.clone(),
global_component_part_styles: self.global_component_part_styles.clone(),
non_reusable_render_reads: Rc::clone(&self.non_reusable_render_reads),
event_target: self.event_target,
};
if context.phase == ExecutionPhase::Render
&& let Ok(mut runtime) = context.runtime.try_borrow_mut()
{
runtime.reset_component_readers(&context.component);
}
context
}
pub(crate) fn for_structural_scope(
&self,
component: ComponentInstancePath,
events: BTreeMap<String, EventSchema>,
) -> Self {
let mut context = self.for_component(component, events);
context.callback_component = self.callback_component.clone();
context.callback_incarnation = self.callback_incarnation;
context.callback_events = self.callback_events.clone();
context.read_dependency = self.read_dependency.clone();
context
}
pub(crate) fn for_virtual_item(
&self,
collection: &crate::VirtualCollectionId,
key: &str,
) -> Self {
let mut context = self.clone();
context.read_dependency = crate::read_dependency::ReadDependency::virtual_item(
&self.callback_component,
collection,
key,
);
if let Ok(mut runtime) = context.runtime.try_borrow_mut() {
runtime
.native_collections
.reset_contribution(&context.read_dependency);
runtime
.native_documents
.reset_contribution(&context.read_dependency);
runtime.stores.reset_contribution(&context.read_dependency);
runtime
.environment_dependencies
.reset_contribution(&context.read_dependency);
runtime.reset_geometry_read_contribution(&context.read_dependency);
}
context
}
pub(crate) fn with_native_context(
mut self,
native_context: Option<crate::invocation::ScriptInvocationContext>,
) -> Self {
self.native_context = native_context;
self
}
pub(crate) const fn with_event_target(mut self, target: Option<crate::GeometryBounds>) -> Self {
self.event_target = target;
self
}
pub(crate) fn with_async_scope(mut self, scope: AsyncScope) -> Self {
self.async_scope = Some(scope);
self
}
pub(crate) fn with_component_styles(
mut self,
global_part_styles: BTreeMap<String, crate::Style>,
style: Option<crate::Style>,
part_styles: BTreeMap<String, crate::Style>,
) -> Self {
self.global_component_part_styles = global_part_styles;
self.component_style = style;
self.component_part_styles = part_styles;
self
}
pub(crate) fn reset_non_reusable_render_reads(&self) {
self.non_reusable_render_reads.borrow_mut().clear();
}
pub(crate) fn component_render_is_reusable(&self) -> bool {
!self
.non_reusable_render_reads
.borrow()
.contains(&self.component)
}
fn mark_non_reusable_render_read(&self) {
if self.phase == ExecutionPhase::Render {
self.non_reusable_render_reads
.borrow_mut()
.insert(self.callback_component.clone());
}
}
fn resolve_component_style(&self, part: &str, mut base: crate::Style) -> crate::Style {
if let Some(style) = self.global_component_part_styles.get(part) {
base = base.merged(style);
}
if part == "root"
&& let Some(style) = &self.component_style
{
base = base.merged(style);
}
if let Some(style) = self.component_part_styles.get(part) {
base = base.merged(style);
}
base
}
fn scoped_callback(&self, function: FnPtr) -> Result<ScriptCallback, UiContextError> {
let mut callback = ScriptCallback::try_from_fn_ptr(function, self.generation)?;
callback.bind_component_scope_if_unset(
&self.callback_component,
self.callback_incarnation,
self.callback_events.clone(),
);
if let Some(context) = &self.native_context {
callback.bind_native_context_if_unset(context.clone());
}
Ok(callback)
}
#[must_use]
pub fn runtime(&self) -> &Rc<RefCell<UiRuntimeState>> {
&self.runtime
}
pub(crate) fn component_path(&self) -> &ComponentInstancePath {
&self.component
}
pub(crate) const fn component_incarnation(&self) -> ComponentIncarnation {
self.incarnation
}
pub(crate) fn event_schemas(&self) -> &BTreeMap<String, EventSchema> {
&self.events
}
pub(crate) fn callback_component_path(&self) -> &ComponentInstancePath {
&self.callback_component
}
pub(crate) const fn callback_component_incarnation(&self) -> ComponentIncarnation {
self.callback_incarnation
}
pub(crate) fn callback_event_schemas(&self) -> &BTreeMap<String, EventSchema> {
&self.callback_events
}
pub(crate) fn native_context(&self) -> Option<&crate::invocation::ScriptInvocationContext> {
self.native_context.as_ref()
}
pub fn get_state(&self, field: &str) -> Result<UiValue, UiContextError> {
self.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.component_state
.get(&self.callback_component, field)
.cloned()
.ok_or_else(|| UiContextError::UnknownState {
component: self.callback_component.clone(),
field: field.to_owned(),
})
}
pub fn get_state_path(
&self,
field: &str,
path: &UiValuePath,
) -> Result<UiValue, UiContextError> {
Ok(self.get_state(field)?.get_path(path)?.clone())
}
pub fn set_state(&self, field: &str, value: Dynamic) -> Result<(), UiContextError> {
self.require_mutation()?;
let value = UiValue::from_dynamic(value)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let sensitive = runtime.component_state.is_sensitive(&self.component, field);
let changed = runtime
.component_state
.set(&self.component, field, value.clone())?;
if changed {
runtime.dirty.insert(self.component.clone());
}
runtime.traces.push(
crate::RuntimeTraceKind::State,
self.component.to_string(),
format!("set {field}"),
Some(value),
sensitive,
);
Ok(())
}
pub fn set_state_path(
&self,
field: &str,
path: &UiValuePath,
value: Dynamic,
) -> Result<(), UiContextError> {
self.require_mutation()?;
let value = UiValue::from_dynamic(value)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let mut root = runtime
.component_state
.get(&self.component, field)
.cloned()
.ok_or_else(|| UiContextError::UnknownState {
component: self.component.clone(),
field: field.to_owned(),
})?;
root.set_path(path, value.clone())?;
let sensitive = runtime.component_state.is_sensitive(&self.component, field);
if runtime.component_state.set(&self.component, field, root)? {
runtime.dirty.insert(self.component.clone());
}
runtime.traces.push(
crate::RuntimeTraceKind::State,
self.component.to_string(),
format!("set nested {field}"),
Some(value),
sensitive,
);
Ok(())
}
pub fn get_signal(&self, signal: &crate::NativeSignal) -> Result<Dynamic, UiContextError> {
self.mark_non_reusable_render_read();
Ok(self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.signals
.read(signal)?
.into_dynamic())
}
pub fn set_signal(
&self,
signal: &crate::NativeSignal,
value: Dynamic,
) -> Result<(), UiContextError> {
self.require_mutation()?;
let value = crate::SignalValue::from_dynamic_for_kind(value, signal.id().kind())?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let changed =
runtime
.signals
.write_from(signal, value.clone(), crate::SignalWriter::Script)?;
runtime.traces.push(
crate::RuntimeTraceKind::Signal,
signal.id().component().to_string(),
format!(
"set {} ({}){}",
signal.id().key(),
signal.id().kind().as_str(),
if changed { "" } else { " unchanged" }
),
None,
false,
);
Ok(())
}
pub fn get_signal_by_key(&self, key: &str) -> Result<Dynamic, UiContextError> {
self.mark_non_reusable_render_read();
let runtime = self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?;
let signal = runtime.signals.resolve(&self.component, key)?;
Ok(runtime.signals.read(&signal)?.into_dynamic())
}
pub(crate) fn parent_optional_float_signal_ref(
&self,
key: &str,
) -> Result<crate::NativeSignal, UiContextError> {
let runtime = self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?;
let mut scope = self.component.parent();
while let Some(component) = scope {
if let Ok(signal) = runtime.signals.resolve(&component, key) {
if signal.id().kind() == crate::SignalKind::OptionalFloat {
return Ok(signal);
}
return Err(crate::SignalError::TypeMismatch {
expected: crate::SignalKind::OptionalFloat,
actual: signal.id().kind(),
}
.into());
}
if let Some(incarnation) = runtime.component_incarnation(&component) {
let id = crate::SignalId::new_scoped(
component,
incarnation,
key,
crate::SignalKind::OptionalFloat,
)?;
return Ok(crate::NativeSignal::new(id));
}
scope = component.parent();
}
Err(crate::SignalError::UnknownKey {
component: self.component.clone(),
key: key.to_owned(),
}
.into())
}
pub fn set_signal_by_key(&self, key: &str, value: Dynamic) -> Result<(), UiContextError> {
self.require_mutation()?;
let signal = self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.signals
.resolve(&self.component, key)?;
self.set_signal(&signal, value)
}
pub fn element_bounds(&self, reference: &crate::ElementRef) -> Result<UiValue, UiContextError> {
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let node = if self.phase == ExecutionPhase::Render {
runtime
.element_refs
.resolve_and_track_geometry(reference, &self.read_dependency)
} else {
Some(runtime.element_refs.resolve(reference)?)
};
let Some(node) = node else {
return Ok(UiValue::Null);
};
let geometry_registry =
runtime.geometry_for(self.view.as_deref().or(self.window.as_deref()));
drop(runtime);
let geometry = if self.phase == ExecutionPhase::Render {
geometry_registry.read_ref_tracked(reference.id(), node, &self.read_dependency)
} else {
geometry_registry.get(node)
};
let Some(geometry) = geometry else {
return Ok(UiValue::Null);
};
Ok(UiValue::Map(BTreeMap::from([
("layout".to_owned(), geometry_bounds_value(geometry.layout)),
("visual".to_owned(), geometry_bounds_value(geometry.visual)),
(
"clip".to_owned(),
geometry.clip.map_or(UiValue::Null, geometry_bounds_value),
),
])))
}
pub fn element_bounds_by_key(&self, key: &str) -> Result<UiValue, UiContextError> {
let reference = self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.element_refs
.resolve_key(&self.component, key)?;
self.element_bounds(&reference)
}
pub fn event_target_bounds(&self) -> Result<UiValue, UiContextError> {
if self.phase != ExecutionPhase::Event {
return Err(UiContextError::EventTargetOutsideEvent);
}
Ok(self
.event_target
.map_or(UiValue::Null, crate::GeometryBounds::into_value))
}
pub fn focus_element(&self, reference: &crate::ElementRef) -> Result<(), UiContextError> {
self.require_mutation()?;
let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let node = runtime.element_refs.resolve(reference)?;
runtime
.pending_element_commands
.push(crate::element_ref::ElementCommand::Focus { window, node });
Ok(())
}
pub fn focus_element_by_key(&self, key: &str) -> Result<(), UiContextError> {
let reference = self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.element_refs
.resolve_key(&self.component, key)?;
self.focus_element(&reference)
}
pub fn scroll_element_to(
&self,
reference: &crate::ElementRef,
x: f64,
y: f64,
) -> Result<(), UiContextError> {
self.require_mutation()?;
if !x.is_finite() || !y.is_finite() || x < 0.0 || y < 0.0 {
return Err(UiContextError::InvalidScrollOffset { x, y });
}
let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let node = runtime.element_refs.resolve(reference)?;
runtime
.pending_element_commands
.push(crate::element_ref::ElementCommand::ScrollTo { window, node, x, y });
Ok(())
}
pub fn scroll_element_to_by_key(
&self,
key: &str,
x: f64,
y: f64,
) -> Result<(), UiContextError> {
let reference = self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.element_refs
.resolve_key(&self.component, key)?;
self.scroll_element_to(&reference, x, y)
}
pub fn scroll_element_into_view(
&self,
reference: &crate::ElementRef,
) -> Result<(), UiContextError> {
self.require_mutation()?;
let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let node = runtime.element_refs.resolve(reference)?;
runtime
.pending_element_commands
.push(crate::element_ref::ElementCommand::ScrollIntoView { window, node });
Ok(())
}
pub fn scroll_element_into_view_by_key(&self, key: &str) -> Result<(), UiContextError> {
let reference = self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.element_refs
.resolve_key(&self.component, key)?;
self.scroll_element_into_view(&reference)
}
pub fn get_app_store(&self, store: &str, field: &str) -> Result<UiValue, UiContextError> {
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
Ok(runtime
.stores
.read_dependency(&self.read_dependency, &StoreId::app(store), field)?)
}
pub fn get_native_collection(
&self,
name: &str,
) -> Result<crate::NativeCollection, UiContextError> {
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
Ok(runtime.native_collections.read_dependency(
&self.read_dependency,
name,
self.phase == ExecutionPhase::Render,
)?)
}
pub fn get_native_text_document(
&self,
name: &str,
) -> Result<crate::NativeTextDocument, UiContextError> {
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
Ok(runtime
.native_documents
.read_dependency(&self.read_dependency, name)?)
}
#[cfg(feature = "charts")]
pub fn get_native_chart_data(
&self,
name: &str,
) -> Result<crate::NativeChartData, UiContextError> {
self.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.native_chart_data
.get(name)
.cloned()
.ok_or_else(|| UiContextError::UnknownChartData(name.to_owned()))
}
pub fn get_app_store_path(
&self,
store: &str,
field: &str,
path: &UiValuePath,
) -> Result<UiValue, UiContextError> {
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
Ok(runtime.stores.read_path_dependency(
&self.read_dependency,
&StoreId::app(store),
field,
path,
)?)
}
pub fn set_app_store(
&self,
store: &str,
field: &str,
value: Dynamic,
) -> Result<(), UiContextError> {
self.require_mutation()?;
let value = UiValue::from_dynamic(value)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let id = StoreId::app(store);
let sensitive = runtime.stores.is_sensitive(&id, field);
let invalidated = runtime.stores.write(&id, field, value.clone())?;
runtime.dirty.extend(invalidated);
runtime.traces.push(
crate::RuntimeTraceKind::Store,
format!("app:{store}"),
format!("set {field}"),
Some(value),
sensitive,
);
Ok(())
}
pub fn set_app_store_path(
&self,
store: &str,
field: &str,
path: &UiValuePath,
value: Dynamic,
) -> Result<(), UiContextError> {
self.require_mutation()?;
let value = UiValue::from_dynamic(value)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let id = StoreId::app(store);
let sensitive = runtime.stores.is_sensitive(&id, field);
let invalidated = runtime.stores.write_path(&id, field, path, value.clone())?;
runtime.dirty.extend(invalidated);
runtime.traces.push(
crate::RuntimeTraceKind::Store,
format!("app:{store}"),
format!("set nested {field}"),
Some(value),
sensitive,
);
Ok(())
}
pub fn get_window_store(&self, store: &str, field: &str) -> Result<UiValue, UiContextError> {
let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
Ok(runtime.stores.read_dependency(
&self.read_dependency,
&StoreId::window(window, store),
field,
)?)
}
pub fn get_window_store_path(
&self,
store: &str,
field: &str,
path: &UiValuePath,
) -> Result<UiValue, UiContextError> {
let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
Ok(runtime.stores.read_path_dependency(
&self.read_dependency,
&StoreId::window(window, store),
field,
path,
)?)
}
pub fn set_window_store(
&self,
store: &str,
field: &str,
value: Dynamic,
) -> Result<(), UiContextError> {
self.require_mutation()?;
let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
let value = UiValue::from_dynamic(value)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let id = StoreId::window(window, store);
let sensitive = runtime.stores.is_sensitive(&id, field);
let invalidated = runtime.stores.write(&id, field, value.clone())?;
runtime.dirty.extend(invalidated);
runtime.traces.push(
crate::RuntimeTraceKind::Store,
format!("window:{window}:{store}"),
format!("set {field}"),
Some(value),
sensitive,
);
Ok(())
}
pub fn set_window_store_path(
&self,
store: &str,
field: &str,
path: &UiValuePath,
value: Dynamic,
) -> Result<(), UiContextError> {
self.require_mutation()?;
let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
let value = UiValue::from_dynamic(value)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let id = StoreId::window(window, store);
let sensitive = runtime.stores.is_sensitive(&id, field);
let invalidated = runtime.stores.write_path(&id, field, path, value.clone())?;
runtime.dirty.extend(invalidated);
runtime.traces.push(
crate::RuntimeTraceKind::Store,
format!("window:{window}:{store}"),
format!("set nested {field}"),
Some(value),
sensitive,
);
Ok(())
}
pub fn emit(&self, event: &str, payload: Dynamic) -> Result<(), UiContextError> {
self.require_mutation()?;
let schema = self
.events
.get(event)
.ok_or_else(|| UiContextError::UnknownEvent(event.to_owned()))?;
schema
.payload
.validate(&payload)
.map_err(UiContextError::InvalidEvent)?;
let payload = UiValue::from_dynamic(payload)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime.pending_events.push(PendingEvent {
target: self.component.clone(),
event: UiEvent {
name: event.to_owned(),
payload,
},
});
runtime.traces.push(
crate::RuntimeTraceKind::Event,
self.component.to_string(),
format!("emit {event}"),
None,
true,
);
Ok(())
}
pub fn dispatch_action(&self, action: &str, payload: Dynamic) -> Result<(), UiContextError> {
self.require_mutation()?;
let id = ActionId::parse(action)?;
let payload = UiValue::from_dynamic(payload)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let invocation = runtime.actions.dispatch(&id, payload)?;
runtime.pending_actions.push(invocation);
runtime.traces.push(
crate::RuntimeTraceKind::Action,
self.component.to_string(),
format!("dispatch {action}"),
None,
true,
);
Ok(())
}
pub fn register_action(&self, action: &str, callback: FnPtr) -> Result<(), UiContextError> {
self.require_mutation()?;
let id = ActionId::parse(action)?;
let callback = self.scoped_callback(callback)?;
self.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?
.actions
.register_or_replace(id, callback);
Ok(())
}
pub fn set_action_enabled(&self, action: &str, enabled: bool) -> Result<(), UiContextError> {
self.require_mutation()?;
let id = ActionId::parse(action)?;
self.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?
.actions
.set_enabled(&id, enabled)?;
Ok(())
}
pub fn call_capability(
&self,
capability: &str,
method: &str,
input: Dynamic,
) -> Result<UiValue, UiContextError> {
self.require_mutation()?;
let id = CapabilityId::parse(capability)?;
let input = UiValue::from_dynamic(input)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let output = runtime.capabilities.call(&id, method, input)?;
runtime.traces.push(
crate::RuntimeTraceKind::Capability,
self.component.to_string(),
format!("call {capability}.{method}"),
None,
true,
);
Ok(output)
}
fn read_locale<T>(
&self,
read: impl FnOnce(
&LocaleManager,
Option<&str>,
&ComponentInstancePath,
) -> Result<T, LocaleError>,
) -> Result<T, UiContextError> {
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime
.environment_dependencies
.track_locale(self.window.as_deref(), &self.read_dependency);
let locale = runtime
.locale
.as_ref()
.ok_or(UiContextError::LocaleUnavailable)?;
Ok(read(locale, self.window.as_deref(), &self.component)?)
}
pub fn text(&self, key: &str) -> Result<String, UiContextError> {
self.read_locale(|locale, window, component| locale.text(window, Some(component), key))
}
pub fn text_direction(&self) -> Result<String, UiContextError> {
match self
.read_locale(|locale, window, component| locale.direction(window, Some(component)))?
{
TextDirection::LeftToRight => Ok("ltr".to_owned()),
TextDirection::RightToLeft => Ok("rtl".to_owned()),
}
}
pub fn today(&self) -> Result<String, UiContextError> {
Ok(self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.calendar_clock
.today()
.to_iso())
}
pub fn format_date(&self, iso_date: &str, style: &str) -> Result<String, UiContextError> {
let style = DateStyle::parse(style)?;
self.read_locale(|locale, window, component| {
locale.format_date(window, Some(component), iso_date, style)
})
}
pub fn format_month_year(&self, iso_date: &str) -> Result<String, UiContextError> {
self.read_locale(|locale, window, component| {
locale.format_month_year(window, Some(component), iso_date)
})
}
pub fn calendar_metadata(&self) -> Result<Map, UiContextError> {
let calendar = self.read_locale(|locale, window, component| {
locale.calendar(window, Some(component)).cloned()
})?;
let dynamic = rhai::serde::to_dynamic(calendar)
.map_err(|error| LocaleError::Decode(error.to_string()))?;
Ok(dynamic.cast::<Map>())
}
pub fn number_metadata(&self) -> Result<Map, UiContextError> {
let number = self.read_locale(|locale, window, component| {
locale.number(window, Some(component)).cloned()
})?;
let dynamic = rhai::serde::to_dynamic(number)
.map_err(|error| LocaleError::Decode(error.to_string()))?;
Ok(dynamic.cast::<Map>())
}
pub fn format_integer(
&self,
value: INT,
options: NumberFormatOptions,
) -> Result<String, UiContextError> {
self.read_locale(|locale, window, component| {
locale.format_integer(window, Some(component), value, options)
})
}
pub fn format_number(
&self,
value: FLOAT,
options: NumberFormatOptions,
) -> Result<String, UiContextError> {
self.read_locale(|locale, window, component| {
locale.format_number(window, Some(component), value, options)
})
}
pub fn set_locale(&self, locale: &str) -> Result<(), UiContextError> {
self.require_mutation()?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let changed = {
let locales = runtime
.locale
.as_mut()
.ok_or(UiContextError::LocaleUnavailable)?;
let previous = locales.generation();
locales.set_app(locale)?;
locales.generation() != previous
};
if changed {
let invalidated = runtime.environment_dependencies.invalidate_locale_app();
runtime.dirty.extend(invalidated);
runtime.mark_all_windows_repaint();
}
runtime.traces.push(
crate::RuntimeTraceKind::Locale,
self.component.to_string(),
format!("select {locale}"),
None,
false,
);
Ok(())
}
pub fn set_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
self.set_theme_preference(
ThemeTarget::App,
ThemePreference::Fixed {
selection: ThemeSelection::new(family, variant),
},
format!("select {family}/{variant}"),
)
}
pub fn set_window_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
self.set_theme_preference(
ThemeTarget::Window(window),
ThemePreference::Fixed {
selection: ThemeSelection::new(family, variant),
},
format!("select window {family}/{variant}"),
)
}
pub fn window_id(&self) -> Result<String, UiContextError> {
self.window.clone().ok_or(UiContextError::MissingWindow)
}
pub fn view_id(&self) -> Result<String, UiContextError> {
self.view.clone().ok_or(UiContextError::MissingView)
}
pub fn viewport_class(&self) -> Result<String, UiContextError> {
let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime
.environment_dependencies
.track_viewport(window, &self.read_dependency);
Ok(runtime.responsive.class(window).as_str().to_owned())
}
pub fn open_window(&self, spec: ScriptWindowSpec) -> Result<(), UiContextError> {
self.require_mutation()?;
let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
let id = spec.id.clone();
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime.windows.request_open_from(window, spec)?;
runtime.traces.push(
crate::RuntimeTraceKind::Window,
self.component.to_string(),
format!("open {id}"),
None,
false,
);
Ok(())
}
pub fn focus_window(&self, id: &str) -> Result<(), UiContextError> {
self.require_mutation()?;
let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime.windows.request_focus_from(window, id)?;
runtime.traces.push(
crate::RuntimeTraceKind::Window,
self.component.to_string(),
format!("focus {id}"),
None,
false,
);
Ok(())
}
pub fn close_window(&self, id: &str) -> Result<(), UiContextError> {
self.require_mutation()?;
let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime.windows.request_close_from(window, id)?;
runtime.traces.push(
crate::RuntimeTraceKind::Window,
self.component.to_string(),
format!("close {id}"),
None,
false,
);
Ok(())
}
pub fn set_close_handler(&self, callback: FnPtr) -> Result<(), UiContextError> {
self.require_mutation()?;
let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
let callback = self.scoped_callback(callback)?;
self.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?
.windows
.set_close_handler(window, Some(callback))?;
Ok(())
}
pub fn clear_close_handler(&self) -> Result<(), UiContextError> {
self.require_mutation()?;
let window = self.window.as_ref().ok_or(UiContextError::MissingWindow)?;
self.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?
.windows
.set_close_handler(window, None)?;
Ok(())
}
pub fn set_window_locale(&self, locale: &str) -> Result<(), UiContextError> {
self.require_mutation()?;
let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let changed = {
let locales = runtime
.locale
.as_mut()
.ok_or(UiContextError::LocaleUnavailable)?;
let previous = locales.generation();
locales.set_window(window.clone(), locale)?;
locales.generation() != previous
};
if changed {
let invalidated = runtime
.environment_dependencies
.invalidate_locale_window(&window);
runtime.dirty.extend(invalidated);
runtime.mark_window_repaint(window.clone());
}
runtime.traces.push(
crate::RuntimeTraceKind::Locale,
self.component.to_string(),
format!("select window {window} locale {locale}"),
None,
false,
);
Ok(())
}
pub fn set_local_locale(&self, locale: &str) -> Result<(), UiContextError> {
self.require_mutation()?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let changed = {
let locales = runtime
.locale
.as_mut()
.ok_or(UiContextError::LocaleUnavailable)?;
let previous = locales.generation();
locales.set_scope(self.component.clone(), locale)?;
locales.generation() != previous
};
if changed {
let invalidated = runtime
.environment_dependencies
.invalidate_locale_scope(&self.component);
runtime.dirty.extend(invalidated);
if let Some(window) = &self.window {
runtime.mark_window_repaint(window.clone());
}
}
runtime.traces.push(
crate::RuntimeTraceKind::Locale,
self.component.to_string(),
format!("select subtree locale {locale}"),
None,
false,
);
Ok(())
}
pub fn set_local_theme(&self, family: &str, variant: &str) -> Result<(), UiContextError> {
self.set_theme_preference(
ThemeTarget::Local,
ThemePreference::Fixed {
selection: ThemeSelection::new(family, variant),
},
format!("select subtree {family}/{variant}"),
)
}
pub fn set_theme_system(&self, family: &str) -> Result<(), UiContextError> {
self.set_theme_preference(
ThemeTarget::App,
ThemePreference::System {
family: family.to_owned(),
},
format!("follow system family {family}"),
)
}
pub fn set_window_theme_system(&self, family: &str) -> Result<(), UiContextError> {
let window = self.window.clone().ok_or(UiContextError::MissingWindow)?;
self.set_theme_preference(
ThemeTarget::Window(window),
ThemePreference::System {
family: family.to_owned(),
},
format!("follow window system family {family}"),
)
}
pub fn set_local_theme_system(&self, family: &str) -> Result<(), UiContextError> {
self.set_theme_preference(
ThemeTarget::Local,
ThemePreference::System {
family: family.to_owned(),
},
format!("follow subtree system family {family}"),
)
}
fn set_theme_preference(
&self,
target: ThemeTarget,
preference: ThemePreference,
message: String,
) -> Result<(), UiContextError> {
self.require_mutation()?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let invalidated = match &target {
ThemeTarget::App => runtime.environment_dependencies.invalidate_theme_app(),
ThemeTarget::Window(window) => runtime
.environment_dependencies
.invalidate_theme_window(window),
ThemeTarget::Local => runtime
.environment_dependencies
.invalidate_theme_scope(&self.component),
};
let app_target = matches!(&target, ThemeTarget::App);
let theme = runtime
.theme
.as_mut()
.ok_or(UiContextError::ThemeUnavailable)?;
match target {
ThemeTarget::App => theme.set_app(preference)?,
ThemeTarget::Window(window) => theme.set_window(window, preference)?,
ThemeTarget::Local => theme.set_scope(self.component.clone(), preference)?,
}
if app_target {
runtime.mark_all_windows_dirty();
} else {
runtime.dirty.insert(self.component.clone());
}
runtime.dirty.extend(invalidated);
runtime.traces.push(
crate::RuntimeTraceKind::Theme,
self.component.to_string(),
message,
None,
false,
);
Ok(())
}
pub fn set_reduced_motion(&self, reduced: bool) -> Result<(), UiContextError> {
self.require_mutation()?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime.motions.request_preference(if reduced {
crate::MotionPreference::Reduced
} else {
crate::MotionPreference::Normal
});
let now = runtime.clock.now();
runtime.motion_values = runtime.motions.snapshot(now);
runtime.dirty.insert(self.component.clone());
runtime.traces.push(
crate::RuntimeTraceKind::State,
self.component.to_string(),
format!("reduced motion {reduced}"),
None,
false,
);
Ok(())
}
pub fn motion_handle(&self, name: &str) -> Result<crate::MotionHandle, UiContextError> {
let scope = self.motion_scope();
Ok(self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.motions
.timeline_handle_for_owner(
&scope,
&self.component,
self.incarnation,
self.generation,
name,
)?)
}
#[must_use]
pub fn motion_tokens(&self) -> crate::ThemeMotion {
self.with_resolved_theme(|variant| variant.tokens.motion.clone())
.unwrap_or_default()
}
#[must_use]
pub fn resolved_theme_variant(&self) -> Option<crate::ThemeVariantInfo> {
self.with_resolved_theme(|variant| crate::ThemeVariantInfo {
family: variant.family.clone(),
name: variant.name.clone(),
mode: variant.mode,
})
}
fn with_resolved_theme<R>(&self, project: impl FnOnce(&crate::ThemeVariant) -> R) -> Option<R> {
let Ok(mut runtime) = self.runtime.try_borrow_mut() else {
return None;
};
if self.phase == ExecutionPhase::Render {
runtime
.environment_dependencies
.track_theme(self.window.as_deref(), &self.read_dependency);
}
let appearance = self
.window
.as_deref()
.and_then(|window| runtime.window_appearances.get(window).copied())
.unwrap_or(crate::SystemAppearance::Dark);
runtime
.theme
.as_ref()
.and_then(|theme| {
theme
.resolve(self.window.as_deref(), Some(&self.component), appearance)
.ok()
})
.map(|theme| project(theme.variant()))
}
#[must_use]
pub fn resolved_theme_selection(&self) -> Option<ThemeSelection> {
self.with_resolved_theme(|variant| ThemeSelection::new(&variant.family, &variant.name))
}
pub fn motion_duration(&self, role: &str) -> Result<u64, UiContextError> {
self.motion_tokens()
.durations_ms
.get(role)
.copied()
.ok_or_else(|| UiContextError::UnknownMotionToken {
category: "duration",
role: role.to_owned(),
})
}
pub fn motion_easing(&self, role: &str) -> Result<crate::MotionEasing, UiContextError> {
self.motion_tokens()
.easings
.get(role)
.copied()
.ok_or_else(|| UiContextError::UnknownMotionToken {
category: "easing",
role: role.to_owned(),
})
}
pub fn motion_spring(&self, role: &str) -> Result<crate::ThemeMotionSpring, UiContextError> {
self.motion_tokens()
.springs
.get(role)
.copied()
.ok_or_else(|| UiContextError::UnknownMotionToken {
category: "spring",
role: role.to_owned(),
})
}
pub fn motion_distance(&self, role: &str) -> Result<f64, UiContextError> {
self.motion_tokens()
.distances
.get(role)
.copied()
.ok_or_else(|| UiContextError::UnknownMotionToken {
category: "distance",
role: role.to_owned(),
})
}
pub fn motion_stagger(&self, role: &str) -> Result<u64, UiContextError> {
self.motion_tokens()
.staggers_ms
.get(role)
.copied()
.ok_or_else(|| UiContextError::UnknownMotionToken {
category: "stagger",
role: role.to_owned(),
})
}
#[must_use]
pub fn motion_quality(&self) -> crate::MotionQuality {
self.runtime
.try_borrow()
.map_or(crate::MotionQuality::Low, |runtime| {
runtime.motions.quality()
})
}
pub fn play_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
self.require_mutation()?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime.motions.validate_handle_owner(
handle,
&self.motion_scope(),
&self.component,
self.incarnation,
self.generation,
)?;
let now = runtime.clock.now();
runtime.motions.play_timeline(handle, now)?;
Ok(())
}
pub fn pause_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
self.require_mutation()?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime.motions.validate_handle_owner(
handle,
&self.motion_scope(),
&self.component,
self.incarnation,
self.generation,
)?;
let now = runtime.clock.now();
runtime.motions.pause_timeline(handle, now)?;
Ok(())
}
pub fn seek_motion(
&self,
handle: &crate::MotionHandle,
position_ms: u64,
) -> Result<(), UiContextError> {
self.require_mutation()?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime.motions.validate_handle_owner(
handle,
&self.motion_scope(),
&self.component,
self.incarnation,
self.generation,
)?;
let now = runtime.clock.now();
runtime.motions.seek_timeline(handle, position_ms, now)?;
runtime.motion_values = runtime.motions.snapshot(now);
Ok(())
}
pub fn restart_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
self.require_mutation()?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime.motions.validate_handle_owner(
handle,
&self.motion_scope(),
&self.component,
self.incarnation,
self.generation,
)?;
let now = runtime.clock.now();
runtime.motions.restart_timeline(handle, now)?;
Ok(())
}
pub fn cancel_motion(&self, handle: &crate::MotionHandle) -> Result<(), UiContextError> {
self.require_mutation()?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
runtime.motions.validate_handle_owner(
handle,
&self.motion_scope(),
&self.component,
self.incarnation,
self.generation,
)?;
runtime.motions.cancel_timeline(handle)?;
Ok(())
}
fn motion_scope(&self) -> String {
match (self.window.as_deref(), self.view.as_deref()) {
(Some(window), Some(view)) => format!("window:{window}/view:{view}/root"),
(Some(window), None) => format!("window:{window}/root"),
(None, Some(view)) => format!("view:{view}/root"),
(None, None) => "root".to_owned(),
}
}
pub fn load_image(&self, asset: &AssetId) -> Result<OpaqueHandle, UiContextError> {
self.require_mutation()?;
Ok(self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.assets
.load_image(asset)?
.opaque()
.clone())
}
pub fn start_image_decode(
&self,
asset: &AssetId,
success: FnPtr,
error: FnPtr,
) -> Result<ImageDecodeHandle, UiContextError> {
self.require_mutation()?;
self.require_generation()?;
let success = self.scoped_callback(success)?;
let error = self.scoped_callback(error)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
crate::RuntimeBudgets::check(
"image_decodes",
runtime.assets.pending_decode_count().saturating_add(1),
runtime.budgets.image_decodes,
)?;
let handle = runtime.assets.start_image_decode(
asset,
self.async_scope
.clone()
.unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
self.generation,
success,
error,
)?;
runtime.traces.push(
crate::RuntimeTraceKind::Task,
self.component.to_string(),
format!("decode image {}", asset.as_str()),
None,
false,
);
Ok(handle)
}
pub fn cancel_image_decode(&self, handle: ImageDecodeHandle) -> Result<bool, UiContextError> {
self.require_mutation()?;
Ok(self
.runtime
.try_borrow()
.map_err(|_| UiContextError::Borrowed)?
.assets
.cancel_image_decode(handle)?)
}
pub fn cancel_task(&self, handle: TaskHandle) -> Result<bool, UiContextError> {
self.require_mutation()?;
Ok(self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?
.tasks
.cancel(handle))
}
pub fn cancel_subscription(&self, handle: SubscriptionHandle) -> Result<bool, UiContextError> {
self.require_mutation()?;
Ok(self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?
.subscriptions
.cancel(handle))
}
pub fn pause_timeout(&self, key: &str) -> Result<bool, UiContextError> {
self.require_mutation()?;
let id = crate::TimerId::new(self.component.clone(), key)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let now = runtime.clock.now();
Ok(runtime.timers.pause(&id, now))
}
pub fn resume_timeout(&self, key: &str) -> Result<bool, UiContextError> {
self.require_mutation()?;
let id = crate::TimerId::new(self.component.clone(), key)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
let now = runtime.clock.now();
Ok(runtime.timers.resume(&id, now))
}
pub fn cancel_timeout(&self, key: &str) -> Result<bool, UiContextError> {
self.require_mutation()?;
let id = crate::TimerId::new(self.component.clone(), key)?;
Ok(self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?
.timers
.cancel(&id))
}
pub fn start_task(
&self,
capability: &str,
method: &str,
input: Dynamic,
success: FnPtr,
error: FnPtr,
) -> Result<TaskHandle, UiContextError> {
self.require_mutation()?;
self.require_async_start()?;
self.require_generation()?;
let id = CapabilityId::parse(capability)?;
let input = UiValue::from_dynamic(input)?;
let success = self.scoped_callback(success)?;
let error = self.scoped_callback(error)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
crate::RuntimeBudgets::check(
"background_tasks",
runtime.tasks.active_count().saturating_add(1),
runtime.budgets.background_tasks,
)?;
let (work, output) = runtime.capabilities.start_task(&id, method, input)?;
let handle = runtime.tasks.spawn_cancellable(
self.async_scope
.clone()
.unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
self.generation,
success,
error,
output,
move |cancellation| work.run(cancellation),
)?;
runtime.traces.push(
crate::RuntimeTraceKind::Task,
self.component.to_string(),
format!("start {capability}.{method}"),
None,
true,
);
Ok(handle)
}
pub fn start_subscription(
&self,
capability: &str,
method: &str,
input: Dynamic,
success: FnPtr,
error: FnPtr,
options: SubscriptionOptions,
) -> Result<SubscriptionHandle, UiContextError> {
self.require_mutation()?;
self.require_async_start()?;
self.require_generation()?;
if !matches!(self.async_scope, Some(AsyncScope::Effect { .. })) {
return Err(UiContextError::SubscriptionRequiresEffect);
}
let id = CapabilityId::parse(capability)?;
let input = UiValue::from_dynamic(input)?;
let success = self.scoped_callback(success)?;
let error = self.scoped_callback(error)?;
let mut runtime = self
.runtime
.try_borrow_mut()
.map_err(|_| UiContextError::Borrowed)?;
crate::RuntimeBudgets::check(
"subscriptions",
runtime.subscriptions.active_count().saturating_add(1),
runtime.budgets.subscriptions,
)?;
let (work, output) = runtime
.capabilities
.start_subscription(&id, method, input)?;
let registration = SubscriptionRegistration::new(
format!("{capability}.{method}"),
self.async_scope
.clone()
.unwrap_or_else(|| AsyncScope::Component(self.component.clone())),
self.generation,
success,
error,
output,
)
.with_options(options);
let (handle, emitter) = runtime.subscriptions.subscribe(registration);
let closer = emitter.clone();
if let Err(spawn_error) = std::thread::Builder::new()
.name("gpui-rhai-subscription".to_owned())
.spawn(move || run_subscription_work(work, emitter, &closer))
{
let _ = runtime
.subscriptions
.cancel_with_reason(handle, SubscriptionCloseReason::StartupFailed);
return Err(AsyncRuntimeError::Spawn(spawn_error).into());
}
runtime.traces.push(
crate::RuntimeTraceKind::Subscription,
self.component.to_string(),
format!("start {capability}.{method}"),
None,
true,
);
Ok(handle)
}
fn require_mutation(&self) -> Result<(), UiContextError> {
if self.phase.allows_mutation() {
Ok(())
} else {
Err(UiContextError::MutationDuringRender)
}
}
fn require_async_start(&self) -> Result<(), UiContextError> {
if self.phase.allows_async_start() {
Ok(())
} else {
Err(UiContextError::AsyncDuringSuspend)
}
}
fn require_generation(&self) -> Result<(), UiContextError> {
if self.generation == ScriptGeneration::default() {
Err(UiContextError::MissingGeneration)
} else {
Ok(())
}
}
}
fn run_subscription_work(
work: crate::SubscriptionWork,
emitter: crate::SubscriptionEmitter,
closer: &crate::SubscriptionEmitter,
) {
work.run(emitter);
closer.close_with_reason(SubscriptionCloseReason::WorkReturned);
}
impl CustomType for UiContext {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("UiContext")
.with_fn(
"emit",
|context: &mut Self, event: ImmutableString, payload: Dynamic| {
context
.emit(event.as_str(), payload)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"component_style",
|context: &mut Self, part: ImmutableString, base: crate::Style| {
context.resolve_component_style(part.as_str(), base)
},
)
.with_fn(
"call_capability",
|context: &mut Self,
capability: ImmutableString,
method: ImmutableString,
input: Dynamic| {
context
.call_capability(capability.as_str(), method.as_str(), input)
.map(UiValue::into_dynamic)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
register_state_store_context_methods(&mut builder);
register_native_collection_context_methods(&mut builder);
register_native_document_context_methods(&mut builder);
#[cfg(feature = "charts")]
register_native_chart_data_context_methods(&mut builder);
register_signal_context_methods(&mut builder);
register_element_ref_context_methods(&mut builder);
register_async_context_methods(&mut builder);
register_action_context_methods(&mut builder);
register_locale_context_methods(&mut builder);
register_theme_context_methods(&mut builder);
register_motion_context_methods(&mut builder);
register_asset_context_methods(&mut builder);
register_window_context_methods(&mut builder);
}
}
fn register_native_collection_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder.with_fn(
"get_native_collection",
|context: &mut UiContext, name: ImmutableString| {
context
.get_native_collection(name.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn register_native_document_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder.with_fn(
"get_native_text_document",
|context: &mut UiContext, name: ImmutableString| {
context
.get_native_text_document(name.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
#[cfg(feature = "charts")]
fn register_native_chart_data_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder.with_fn(
"get_native_chart_data",
|context: &mut UiContext, name: ImmutableString| {
context
.get_native_chart_data(name.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn register_state_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
register_state_context_methods(builder);
register_app_store_context_methods(builder);
register_window_store_context_methods(builder);
}
fn register_state_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn(
"get_state",
|context: &mut UiContext, field: ImmutableString| {
context
.get_state(field.as_str())
.map(UiValue::into_dynamic)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"get_state_path",
|context: &mut UiContext, field: ImmutableString, path: Array| {
let path = rhai_value_path(path)?;
context
.get_state_path(field.as_str(), &path)
.map(UiValue::into_dynamic)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_state",
|context: &mut UiContext, field: ImmutableString, value: Dynamic| {
context
.set_state(field.as_str(), value)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_state_path",
|context: &mut UiContext, field: ImmutableString, path: Array, value: Dynamic| {
let path = rhai_value_path(path)?;
context
.set_state_path(field.as_str(), &path, value)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn register_app_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn(
"get_app_store",
|context: &mut UiContext, store: ImmutableString, field: ImmutableString| {
context
.get_app_store(store.as_str(), field.as_str())
.map(UiValue::into_dynamic)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"get_app_store_path",
|context: &mut UiContext,
store: ImmutableString,
field: ImmutableString,
path: Array| {
let path = rhai_value_path(path)?;
context
.get_app_store_path(store.as_str(), field.as_str(), &path)
.map(UiValue::into_dynamic)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_app_store",
|context: &mut UiContext,
store: ImmutableString,
field: ImmutableString,
value: Dynamic| {
context
.set_app_store(store.as_str(), field.as_str(), value)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_app_store_path",
|context: &mut UiContext,
store: ImmutableString,
field: ImmutableString,
path: Array,
value: Dynamic| {
let path = rhai_value_path(path)?;
context
.set_app_store_path(store.as_str(), field.as_str(), &path, value)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn register_window_store_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn(
"get_window_store",
|context: &mut UiContext, store: ImmutableString, field: ImmutableString| {
context
.get_window_store(store.as_str(), field.as_str())
.map(UiValue::into_dynamic)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"get_window_store_path",
|context: &mut UiContext,
store: ImmutableString,
field: ImmutableString,
path: Array| {
let path = rhai_value_path(path)?;
context
.get_window_store_path(store.as_str(), field.as_str(), &path)
.map(UiValue::into_dynamic)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_window_store",
|context: &mut UiContext,
store: ImmutableString,
field: ImmutableString,
value: Dynamic| {
context
.set_window_store(store.as_str(), field.as_str(), value)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_window_store_path",
|context: &mut UiContext,
store: ImmutableString,
field: ImmutableString,
path: Array,
value: Dynamic| {
let path = rhai_value_path(path)?;
context
.set_window_store_path(store.as_str(), field.as_str(), &path, value)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn rhai_value_path(values: Array) -> Result<UiValuePath, Box<EvalAltResult>> {
if values.len() > 64 {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"value path must have at most 64 segments".into(),
Position::NONE,
)));
}
let segments = values
.into_iter()
.enumerate()
.map(|(index, value)| {
let actual = value.type_name().to_owned();
if value.is::<ImmutableString>() {
return Ok(UiValuePathSegment::Key(
value.cast::<ImmutableString>().to_string(),
));
}
if value.is::<INT>() {
let index_value = value.cast::<INT>();
return usize::try_from(index_value)
.map(UiValuePathSegment::Index)
.map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
format!("value path segment {index} must be a non-negative integer")
.into(),
Position::NONE,
))
});
}
if let Some(mut selector) = value.try_cast::<Map>() {
let by = selector
.remove("by")
.and_then(Dynamic::try_cast::<ImmutableString>);
let key = selector
.remove("key")
.and_then(Dynamic::try_cast::<ImmutableString>);
if selector.is_empty()
&& let (Some(by), Some(key)) = (by, key)
{
return Ok(UiValuePathSegment::Item {
key_field: by.to_string(),
key: key.to_string(),
});
}
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!(
"value path segment {index} keyed selector must be exactly #{{ by: string, key: string }}"
)
.into(),
Position::NONE,
)));
}
Err(Box::new(EvalAltResult::ErrorRuntime(
format!(
"value path segment {index} must be a string, non-negative integer, or keyed selector map; got {actual}"
)
.into(),
Position::NONE,
)))
})
.collect::<Result<Vec<_>, _>>()?;
UiValuePath::new(segments).map_err(|error| {
Box::new(EvalAltResult::ErrorRuntime(
error.to_string().into(),
Position::NONE,
))
})
}
fn register_signal_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn(
"get_signal",
|context: &mut UiContext, signal: crate::NativeSignal| {
context
.get_signal(&signal)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"get_signal",
|context: &mut UiContext, key: ImmutableString| {
context
.get_signal_by_key(key.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_signal",
|context: &mut UiContext, signal: crate::NativeSignal, value: Dynamic| {
context
.set_signal(&signal, value)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_signal",
|context: &mut UiContext, key: ImmutableString, value: Dynamic| {
context
.set_signal_by_key(key.as_str(), value)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn register_element_ref_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn(
"element_bounds",
|context: &mut UiContext, reference: crate::ElementRef| {
context
.element_bounds(&reference)
.map(UiValue::into_dynamic)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"element_bounds",
|context: &mut UiContext, key: ImmutableString| {
context
.element_bounds_by_key(key.as_str())
.map(UiValue::into_dynamic)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn("event_target_bounds", |context: &mut UiContext| {
context
.event_target_bounds()
.map(UiValue::into_dynamic)
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn(
"focus",
|context: &mut UiContext, reference: crate::ElementRef| {
context
.focus_element(&reference)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn("focus", |context: &mut UiContext, key: ImmutableString| {
context
.focus_element_by_key(key.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
});
}
fn geometry_bounds_value(bounds: crate::GeometryBounds) -> UiValue {
bounds.into_value()
}
fn register_action_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn(
"dispatch_action",
|context: &mut UiContext, action: ImmutableString, payload: Dynamic| {
context
.dispatch_action(action.as_str(), payload)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"register_action",
|call: NativeCallContext<'_>,
context: &mut UiContext,
action: ImmutableString,
callback: FnPtr| {
context_at_call(context, &call)
.register_action(action.as_str(), callback)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_action_enabled",
|context: &mut UiContext, action: ImmutableString, enabled: bool| {
context
.set_action_enabled(action.as_str(), enabled)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"scroll_to",
|context: &mut UiContext, reference: crate::ElementRef, x: FLOAT, y: FLOAT| {
context
.scroll_element_to(&reference, x, y)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"scroll_to",
|context: &mut UiContext, key: ImmutableString, x: FLOAT, y: FLOAT| {
context
.scroll_element_to_by_key(key.as_str(), x, y)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"scroll_into_view",
|context: &mut UiContext, reference: crate::ElementRef| {
context
.scroll_element_into_view(&reference)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"scroll_into_view",
|context: &mut UiContext, key: ImmutableString| {
context
.scroll_element_into_view_by_key(key.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn register_async_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn(
"start_task",
|call: NativeCallContext<'_>,
context: &mut UiContext,
capability: ImmutableString,
method: ImmutableString,
input: Dynamic,
success: FnPtr,
error: FnPtr| {
context_at_call(context, &call)
.start_task(capability.as_str(), method.as_str(), input, success, error)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"start_subscription",
|call: NativeCallContext<'_>,
context: &mut UiContext,
capability: ImmutableString,
method: ImmutableString,
input: Dynamic,
success: FnPtr,
error: FnPtr,
options: Map| {
let options = subscription_options(options)?;
context_at_call(context, &call)
.start_subscription(
capability.as_str(),
method.as_str(),
input,
success,
error,
options,
)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"cancel_task",
|context: &mut UiContext, handle: TaskHandle| {
context
.cancel_task(handle)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"cancel_subscription",
|context: &mut UiContext, handle: SubscriptionHandle| {
context
.cancel_subscription(handle)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"pause_timeout",
|context: &mut UiContext, key: ImmutableString| {
context
.pause_timeout(key.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"resume_timeout",
|context: &mut UiContext, key: ImmutableString| {
context
.resume_timeout(key.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"cancel_timeout",
|context: &mut UiContext, key: ImmutableString| {
context
.cancel_timeout(key.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn subscription_options(mut options: Map) -> Result<SubscriptionOptions, Box<EvalAltResult>> {
let defaults = SubscriptionOptions::default();
let delivery = options.remove("delivery").map_or(
Ok(defaults.delivery()),
|value| -> Result<_, Box<EvalAltResult>> {
let value = value.try_cast::<ImmutableString>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"subscription delivery must be `all` or `latest`".into(),
Position::NONE,
))
})?;
match value.as_str() {
"all" => Ok(SubscriptionDeliveryPolicy::All),
"latest" => Ok(SubscriptionDeliveryPolicy::Latest),
_ => Err(Box::new(EvalAltResult::ErrorRuntime(
"subscription delivery must be `all` or `latest`".into(),
Position::NONE,
))),
}
},
)?;
let capacity = options.remove("capacity").map_or(
Ok(defaults.capacity()),
|value| -> Result<_, Box<EvalAltResult>> {
value
.try_cast::<INT>()
.and_then(|value| usize::try_from(value).ok())
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"subscription capacity must be a positive integer".into(),
Position::NONE,
))
})
},
)?;
let throttle = options.remove("throttle_ms").map_or(
Ok(defaults.throttle()),
|value| -> Result<_, Box<EvalAltResult>> {
value
.try_cast::<INT>()
.and_then(|value| u64::try_from(value).ok())
.map(Duration::from_millis)
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"subscription throttle_ms must be a non-negative integer".into(),
Position::NONE,
))
})
},
)?;
if let Some(name) = options.keys().next() {
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("unknown subscription option `{name}`").into(),
Position::NONE,
)));
}
SubscriptionOptions::new(delivery, capacity, throttle).map_err(|error| {
Box::new(EvalAltResult::ErrorRuntime(
error.to_string().into(),
Position::NONE,
))
})
}
fn register_locale_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn("t", |context: &mut UiContext, key: ImmutableString| {
context
.text(key.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn(
"set_locale",
|context: &mut UiContext, locale: ImmutableString| {
context
.set_locale(locale.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn("text_direction", |context: &mut UiContext| {
context
.text_direction()
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn("today", |context: &mut UiContext| {
context
.today()
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn(
"format_date",
|context: &mut UiContext, date: ImmutableString, style: ImmutableString| {
context
.format_date(date.as_str(), style.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"format_month_year",
|context: &mut UiContext, date: ImmutableString| {
context
.format_month_year(date.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn("calendar", |context: &mut UiContext| {
context
.calendar_metadata()
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn("number", |context: &mut UiContext| {
context
.number_metadata()
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn("format_number", |context: &mut UiContext, value: INT| {
context
.format_integer(value, NumberFormatOptions::default())
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn("format_number", |context: &mut UiContext, value: FLOAT| {
context
.format_number(value, NumberFormatOptions::default())
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn(
"format_number",
|context: &mut UiContext,
value: INT,
options: Map|
-> Result<String, Box<EvalAltResult>> {
let options = number_format_options(options)?;
context
.format_integer(value, options)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"format_number",
|context: &mut UiContext,
value: FLOAT,
options: Map|
-> Result<String, Box<EvalAltResult>> {
let options = number_format_options(options)?;
context
.format_number(value, options)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn number_format_options(mut options: Map) -> Result<NumberFormatOptions, Box<EvalAltResult>> {
let mut parsed = NumberFormatOptions::default();
if let Some(value) = options.remove("min_fraction_digits") {
let value = value.try_cast::<INT>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"min_fraction_digits must be an integer".into(),
Position::NONE,
))
})?;
parsed.min_fraction_digits = u8::try_from(value).map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
"min_fraction_digits must be between 0 and 12".into(),
Position::NONE,
))
})?;
}
if let Some(value) = options.remove("max_fraction_digits") {
let value = value.try_cast::<INT>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"max_fraction_digits must be an integer".into(),
Position::NONE,
))
})?;
parsed.max_fraction_digits = u8::try_from(value).map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
"max_fraction_digits must be between 0 and 12".into(),
Position::NONE,
))
})?;
}
if let Some(value) = options.remove("grouping") {
parsed.grouping = value.try_cast::<bool>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"grouping must be a bool".into(),
Position::NONE,
))
})?;
}
if let Some((unknown, _)) = options.into_iter().next() {
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("unknown number format option `{unknown}`").into(),
Position::NONE,
)));
}
parsed.validate().map_err(|error| {
Box::new(EvalAltResult::ErrorRuntime(
error.to_string().into(),
Position::NONE,
))
})?;
Ok(parsed)
}
fn register_theme_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn(
"theme_variant",
|context: &mut UiContext| -> Result<Dynamic, Box<EvalAltResult>> {
let Some(variant) = context.resolved_theme_variant() else {
return Ok(Dynamic::UNIT);
};
let mut map = rhai::Map::new();
map.insert("family".into(), Dynamic::from(variant.family.clone()));
map.insert("name".into(), Dynamic::from(variant.name.clone()));
map.insert(
"mode".into(),
Dynamic::from(match variant.mode {
crate::ThemeMode::Light => "light",
crate::ThemeMode::Dark => "dark",
}),
);
Ok(Dynamic::from(map))
},
)
.with_fn(
"set_theme",
|context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
context
.set_theme(family.as_str(), variant.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_window_theme",
|context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
context
.set_window_theme(family.as_str(), variant.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_local_theme",
|context: &mut UiContext, family: ImmutableString, variant: ImmutableString| {
context
.set_local_theme(family.as_str(), variant.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_theme_system",
|context: &mut UiContext, family: ImmutableString| {
context
.set_theme_system(family.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_window_theme_system",
|context: &mut UiContext, family: ImmutableString| {
context
.set_window_theme_system(family.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_local_theme_system",
|context: &mut UiContext, family: ImmutableString| {
context
.set_local_theme_system(family.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
#[allow(clippy::too_many_lines)]
fn register_motion_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn(
"set_reduced_motion",
|context: &mut UiContext, reduced: bool| {
context
.set_reduced_motion(reduced)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"motion_handle",
|context: &mut UiContext, name: ImmutableString| {
context
.motion_handle(name.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"motion_duration",
|context: &mut UiContext, role: ImmutableString| {
context
.motion_duration(role.as_str())
.map(|value| INT::try_from(value).unwrap_or(INT::MAX))
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"motion_easing",
|context: &mut UiContext, role: ImmutableString| {
context
.motion_easing(role.as_str())
.map(|value| match value {
crate::MotionEasing::Linear => "linear",
crate::MotionEasing::EaseIn => "ease_in",
crate::MotionEasing::EaseOut => "ease_out",
crate::MotionEasing::EaseInOut => "ease_in_out",
})
.map(ImmutableString::from)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"motion_spring",
|context: &mut UiContext, role: ImmutableString| {
context
.motion_spring(role.as_str())
.map(|spring| {
Map::from_iter([
("stiffness".into(), Dynamic::from_float(spring.stiffness)),
("damping".into(), Dynamic::from_float(spring.damping)),
("mass".into(), Dynamic::from_float(spring.mass)),
])
})
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"motion_distance",
|context: &mut UiContext, role: ImmutableString| {
context
.motion_distance(role.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"motion_stagger",
|context: &mut UiContext, role: ImmutableString| {
context
.motion_stagger(role.as_str())
.map(|value| INT::try_from(value).unwrap_or(INT::MAX))
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn("motion_quality", |context: &mut UiContext| {
match context.motion_quality() {
crate::MotionQuality::Low => "low",
crate::MotionQuality::Medium => "medium",
crate::MotionQuality::High => "high",
}
})
.with_fn(
"play_motion",
|context: &mut UiContext, handle: crate::MotionHandle| {
context
.play_motion(&handle)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"resume_motion",
|context: &mut UiContext, handle: crate::MotionHandle| {
context
.play_motion(&handle)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"pause_motion",
|context: &mut UiContext, handle: crate::MotionHandle| {
context
.pause_motion(&handle)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"seek_motion",
|context: &mut UiContext, handle: crate::MotionHandle, position_ms: INT| {
let position_ms = u64::try_from(position_ms).map_err(|_| {
Box::new(context_runtime_error(&UiContextError::Motion(
crate::MotionError::InvalidTimeline(
"seek position must be non-negative".to_owned(),
),
)))
})?;
context
.seek_motion(&handle, position_ms)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"restart_motion",
|context: &mut UiContext, handle: crate::MotionHandle| {
context
.restart_motion(&handle)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"cancel_motion",
|context: &mut UiContext, handle: crate::MotionHandle| {
context
.cancel_motion(&handle)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn register_asset_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn("load_image", |context: &mut UiContext, asset: AssetId| {
context
.load_image(&asset)
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn(
"start_image_decode",
|call: NativeCallContext<'_>,
context: &mut UiContext,
asset: AssetId,
success: FnPtr,
error: FnPtr| {
context_at_call(context, &call)
.start_image_decode(&asset, success, error)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"cancel_image_decode",
|context: &mut UiContext, handle: ImageDecodeHandle| {
context
.cancel_image_decode(handle)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn register_window_context_methods(builder: &mut TypeBuilder<UiContext>) {
builder
.with_fn("window_id", |context: &mut UiContext| {
context
.window_id()
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn("view_id", |context: &mut UiContext| {
context
.view_id()
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn("viewport_class", |context: &mut UiContext| {
context
.viewport_class()
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn(
"open_window",
|context: &mut UiContext,
id: ImmutableString,
title: ImmutableString,
width: rhai::INT,
height: rhai::INT,
focus: bool| {
context
.open_window(ScriptWindowSpec {
id: id.to_string(),
title: title.to_string(),
width: width.to_string().parse::<f64>().unwrap_or(f64::INFINITY),
height: height.to_string().parse::<f64>().unwrap_or(f64::INFINITY),
focus,
})
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"focus_window",
|context: &mut UiContext, id: ImmutableString| {
context
.focus_window(id.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"close_window",
|context: &mut UiContext, id: ImmutableString| {
context
.close_window(id.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_close_handler",
|call: NativeCallContext<'_>, context: &mut UiContext, callback: FnPtr| {
context_at_call(context, &call)
.set_close_handler(callback)
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn("clear_close_handler", |context: &mut UiContext| {
context
.clear_close_handler()
.map_err(|error| Box::new(context_runtime_error(&error)))
})
.with_fn(
"set_window_locale",
|context: &mut UiContext, locale: ImmutableString| {
context
.set_window_locale(locale.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
)
.with_fn(
"set_local_locale",
|context: &mut UiContext, locale: ImmutableString| {
context
.set_local_locale(locale.as_str())
.map_err(|error| Box::new(context_runtime_error(&error)))
},
);
}
fn context_at_call(context: &UiContext, call: &NativeCallContext<'_>) -> UiContext {
context.clone().with_native_context(Some(
crate::invocation::ScriptInvocationContext::capture_retained(call),
))
}
pub(crate) fn register_ui_context_api(engine: &mut Engine) {
engine.build_type::<UiContext>();
}
fn context_runtime_error(error: &UiContextError) -> EvalAltResult {
EvalAltResult::ErrorRuntime(error.to_string().into(), Position::NONE)
}
#[derive(Debug, Error)]
pub enum UiContextError {
#[error("UI runtime state is already mutably borrowed by another callback")]
Borrowed,
#[error("state mutation and effects are forbidden during view rendering")]
MutationDuringRender,
#[error("new async work is forbidden while a view is suspending")]
AsyncDuringSuspend,
#[error("subscriptions must be started by a declarative component effect")]
SubscriptionRequiresEffect,
#[error("event target geometry is available only during event callbacks")]
EventTargetOutsideEvent,
#[error("async work requires a bound script generation")]
MissingGeneration,
#[error("no locale manager is configured for this application")]
LocaleUnavailable,
#[error("no theme manager is configured for this application")]
ThemeUnavailable,
#[cfg(feature = "charts")]
#[error("native chart data name `{0}` must be a safe identifier")]
InvalidChartDataName(String),
#[cfg(feature = "charts")]
#[error("native chart data `{0}` is not registered")]
UnknownChartData(String),
#[error("unknown theme motion {category} token `{role}`")]
UnknownMotionToken {
category: &'static str,
role: String,
},
#[error("component `{component}` has no state field `{field}`")]
UnknownState {
component: ComponentInstancePath,
field: String,
},
#[error("event `{0}` is not declared by this component")]
UnknownEvent(String),
#[error("event payload is invalid: {0}")]
InvalidEvent(crate::SchemaValidationError),
#[error("this UI context is not associated with a window")]
MissingWindow,
#[error("this UI context is not associated with a mounted script view")]
MissingView,
#[error(transparent)]
Window(#[from] WindowCommandError),
#[error(transparent)]
Responsive(#[from] ResponsiveError),
#[error(transparent)]
State(#[from] StateError),
#[error(transparent)]
Store(#[from] StoreError),
#[error(transparent)]
NativeCollection(#[from] crate::NativeCollectionError),
#[error(transparent)]
Document(#[from] crate::DocumentError),
#[error(transparent)]
Action(#[from] ActionError),
#[error(transparent)]
Capability(#[from] CapabilityError),
#[error(transparent)]
Async(#[from] AsyncRuntimeError),
#[error(transparent)]
Locale(#[from] LocaleError),
#[error(transparent)]
Theme(#[from] ThemeError),
#[error(transparent)]
Asset(#[from] AssetError),
#[error(transparent)]
Value(#[from] UiValueError),
#[error(transparent)]
ValuePath(#[from] UiValuePathError),
#[error(transparent)]
Signal(#[from] crate::SignalError),
#[error(transparent)]
ElementRef(#[from] crate::ElementRefError),
#[error(transparent)]
Geometry(#[from] crate::GeometryError),
#[error("scroll offset must be finite and non-negative, got ({x}, {y})")]
InvalidScrollOffset { x: f64, y: f64 },
#[error(transparent)]
Callback(#[from] crate::ScriptCallbackDefinitionError),
#[error(transparent)]
Timer(#[from] crate::TimerError),
#[error(transparent)]
Motion(#[from] crate::MotionError),
#[error(transparent)]
Budget(#[from] crate::RuntimeBudgetError),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ComponentStateSchema, RuntimeEngine, StateField, SubscriptionWork, ValueSchema};
fn mounted_context(phase: ExecutionPhase) -> UiContext {
let path = ComponentInstancePath::root("Counter", "counter");
let schema = ComponentStateSchema::new(BTreeMap::from([
(
"count".to_owned(),
StateField::new(ValueSchema::integer(), UiValue::Integer(0)),
),
(
"profile".to_owned(),
StateField::new(
ValueSchema::UiValue,
UiValue::Map(BTreeMap::from([(
"name".to_owned(),
UiValue::String("Ada".to_owned()),
)])),
),
),
]))
.unwrap();
let mut state = UiRuntimeState::new();
let mut render = state.component_state.begin_render();
render.mount(path.clone(), &schema).unwrap();
state.component_state.commit_render(render);
UiContext::new(
Rc::new(RefCell::new(state)),
path,
Some("main".to_owned()),
phase,
BTreeMap::from([(
"change".to_owned(),
EventSchema {
payload: ValueSchema::integer(),
},
)]),
)
}
#[test]
fn theme_variant_reads_the_resolved_selection() {
let context = mounted_context(ExecutionPhase::Event);
assert!(context.resolved_theme_variant().is_none());
assert_eq!(context.resolved_theme_selection(), None);
let engine = RuntimeEngine::new();
let dark = crate::load_theme_source(
engine.engine(),
"default_dark.rhai",
include_str!("../../../registry/themes/default_dark.rhai"),
)
.unwrap();
let mut light = dark.clone();
light.name = "Light".to_owned();
light.mode = crate::ThemeMode::Light;
let manager = crate::ThemeManager::from_variants(
[dark, light],
crate::ThemeSelection::new("Default".to_owned(), "Dark".to_owned()),
)
.unwrap();
context.runtime.borrow_mut().theme = Some(manager);
let variant = context.resolved_theme_variant().expect("resolves");
assert_eq!(variant.mode, crate::ThemeMode::Dark);
assert_eq!(
context.resolved_theme_selection(),
Some(ThemeSelection::new("Default", "Dark"))
);
}
#[test]
fn first_native_appearance_replaces_dark_fallback_and_invalidates_render_readers_once() {
let context = mounted_context(ExecutionPhase::Render);
let engine = RuntimeEngine::new();
let dark = crate::load_theme_source(
engine.engine(),
"dark",
include_str!("../../../registry/themes/default_dark.rhai"),
)
.unwrap();
let light = crate::load_theme_source(
engine.engine(),
"light",
include_str!("../../../registry/themes/default_light.rhai"),
)
.unwrap();
let mut manager = crate::ThemeManager::from_variants(
[dark, light],
ThemeSelection::new("Default", "Dark"),
)
.unwrap();
manager
.set_app(ThemePreference::System {
family: "Default".into(),
})
.unwrap();
context.runtime.borrow_mut().theme = Some(manager);
assert_eq!(
context.resolved_theme_variant().unwrap().mode,
crate::ThemeMode::Dark
);
{
let mut runtime = context.runtime.borrow_mut();
runtime.dirty.clear();
assert!(runtime.update_window_appearance("main", crate::SystemAppearance::Light));
assert_eq!(
runtime.dirty,
BTreeSet::from([context.component_path().clone()])
);
runtime.dirty.clear();
assert!(!runtime.update_window_appearance("main", crate::SystemAppearance::Light));
assert!(runtime.dirty.is_empty());
}
assert_eq!(
context.resolved_theme_variant().unwrap().mode,
crate::ThemeMode::Light
);
context
.runtime
.borrow_mut()
.update_window_appearance("main", crate::SystemAppearance::Dark);
assert_eq!(
context.resolved_theme_variant().unwrap().mode,
crate::ThemeMode::Dark
);
}
#[test]
fn render_signal_reads_disable_component_reuse() {
let context = mounted_context(ExecutionPhase::Render);
let signal = crate::NativeSignal::new(
crate::SignalId::new(
context.component_path().clone(),
"hot",
crate::SignalKind::Float,
)
.unwrap(),
);
assert!(context.get_signal(&signal).is_err());
assert!(!context.component_render_is_reusable());
}
#[test]
fn render_context_rejects_mutation() {
let context = mounted_context(ExecutionPhase::Render);
assert!(matches!(
context.set_state("count", Dynamic::from(1_i64)),
Err(UiContextError::MutationDuringRender)
));
}
#[test]
fn event_target_bounds_are_untracked_and_event_only() {
let context = mounted_context(ExecutionPhase::Event).with_event_target(Some(
crate::GeometryBounds::new(13.0, 24.0, 90.0, 40.0).unwrap(),
));
assert_eq!(
context.event_target_bounds().unwrap(),
crate::GeometryBounds::new(13.0, 24.0, 90.0, 40.0)
.unwrap()
.into_value()
);
assert!(context.runtime().borrow().geometry.take_dirty().is_empty());
assert!(matches!(
mounted_context(ExecutionPhase::Render).event_target_bounds(),
Err(UiContextError::EventTargetOutsideEvent)
));
assert_eq!(
mounted_context(ExecutionPhase::Event)
.event_target_bounds()
.unwrap(),
UiValue::Null
);
}
#[test]
fn viewport_class_is_window_scoped_and_script_readable() {
let context = mounted_context(ExecutionPhase::Render);
context
.runtime()
.borrow_mut()
.responsive
.update_window("main", 480.0)
.unwrap();
assert_eq!(context.viewport_class().unwrap(), "compact");
}
#[test]
fn locale_and_viewport_reads_register_exact_component_dependencies() {
let engine = Engine::new();
let en = crate::load_locale_source(
&engine,
"en.rhai",
include_str!("../../../registry/locales/en.rhai"),
)
.unwrap();
let zh = crate::load_locale_source(
&engine,
"zh_cn.rhai",
include_str!("../../../registry/locales/zh_cn.rhai"),
)
.unwrap();
let mut state = UiRuntimeState::new();
state.locale = Some(LocaleManager::new([en, zh], "en", "en").unwrap());
state.responsive.update_window("main", 480.0).unwrap();
state.windows.register_open("main").unwrap();
let runtime = Rc::new(RefCell::new(state));
let reader = ComponentInstancePath::root("View", "main").child("Reader", "reader");
let unrelated = ComponentInstancePath::root("View", "main").child("Static", "static");
let render_context = UiContext::new(
Rc::clone(&runtime),
reader.clone(),
Some("main".to_owned()),
ExecutionPhase::Render,
BTreeMap::new(),
);
assert!(!render_context.text("common.loading").unwrap().is_empty());
assert_eq!(render_context.viewport_class().unwrap(), "compact");
let event = UiContext::new(
Rc::clone(&runtime),
unrelated,
Some("main".to_owned()),
ExecutionPhase::Event,
BTreeMap::new(),
);
event.set_locale("zh-CN").unwrap();
assert_eq!(
runtime.borrow_mut().drain_batch().dirty,
BTreeSet::from([reader.clone()])
);
assert_eq!(
runtime
.borrow()
.environment_dependencies
.invalidate_viewport("main"),
BTreeSet::from([reader])
);
assert!(runtime.borrow_mut().take_window_repaint("main"));
}
#[test]
fn event_mutations_are_batched() {
let context = mounted_context(ExecutionPhase::Event);
context.set_state("count", Dynamic::from(1_i64)).unwrap();
context.set_state("count", Dynamic::from(2_i64)).unwrap();
context.emit("change", Dynamic::from(2_i64)).unwrap();
let mut runtime = context.runtime().borrow_mut();
let batch = runtime.drain_batch();
assert_eq!(batch.dirty.len(), 1);
assert_eq!(batch.events.len(), 1);
assert_eq!(
runtime
.component_state
.get(&ComponentInstancePath::root("Counter", "counter"), "count",),
Some(&UiValue::Integer(2))
);
}
#[test]
fn semantic_event_and_action_traces_never_retain_payload_values() {
let secret = "AUDIT_ONLY_SYNTHETIC_TOKEN";
let path = ComponentInstancePath::root("SecretInput", "audit");
let schema = ComponentStateSchema::new(BTreeMap::from([(
"token".to_owned(),
StateField::new(ValueSchema::string(), UiValue::String(secret.to_owned()))
.sensitive(true),
)]))
.unwrap();
let mut state = UiRuntimeState::new();
let mut render = state.component_state.begin_render();
render.mount(path.clone(), &schema).unwrap();
state.component_state.commit_render(render);
let mut engine = RuntimeEngine::new();
let compiled = engine
.compile("fn view() { text(\"trace\") } fn receive(ctx, payload) {}")
.unwrap();
state.actions.register_or_replace(
crate::ActionId::parse("audit.receive").unwrap(),
engine.callback(&compiled, "receive").unwrap(),
);
let runtime = Rc::new(RefCell::new(state));
let context = UiContext::new(
Rc::clone(&runtime),
path,
Some("main".to_owned()),
ExecutionPhase::Event,
BTreeMap::from([(
"change".to_owned(),
EventSchema {
payload: ValueSchema::string(),
},
)]),
);
let event_value = context.get_state("token").unwrap().into_dynamic();
context.emit("change", event_value).unwrap();
let action_value = context.get_state("token").unwrap().into_dynamic();
context
.dispatch_action("audit.receive", action_value)
.unwrap();
let traces = runtime.borrow().traces.snapshot();
let semantic = traces
.iter()
.filter(|trace| {
matches!(
trace.kind,
crate::RuntimeTraceKind::Event | crate::RuntimeTraceKind::Action
)
})
.collect::<Vec<_>>();
assert_eq!(semantic.len(), 2);
assert!(
semantic
.iter()
.all(|trace| trace.sensitive && trace.payload.is_none())
);
assert!(!format!("{traces:?}").contains(secret));
}
#[test]
fn scripts_receive_restricted_context_without_gpui_types() {
let context = mounted_context(ExecutionPhase::Event);
let mut runtime = RuntimeEngine::new();
let compiled = runtime
.compile(
r#"
fn view() { text("context") }
fn increment(ctx) {
ctx.set_state("count", ctx.get_state("count") + 1);
}
"#,
)
.unwrap();
runtime.render(&compiled).unwrap();
let callback = runtime.callback(&compiled, "increment").unwrap();
let _ = runtime
.invoke_callback(&compiled, &callback, (context.clone(),))
.unwrap();
assert_eq!(context.get_state("count").unwrap(), UiValue::Integer(1));
}
#[test]
fn structural_store_paths_invalidate_executable_owner_not_synthetic_scope() {
for window_store in [false, true] {
let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
let owner = ComponentInstancePath::root("Panel", "paths");
let store = if window_store {
StoreId::window("main", "data")
} else {
StoreId::app("data")
};
runtime
.borrow_mut()
.stores
.declare(
store,
ComponentStateSchema::new(BTreeMap::from([(
"model".into(),
StateField::new(
ValueSchema::UiValue,
UiValue::Map(BTreeMap::from([
("label".into(), UiValue::String("before".into())),
("sibling".into(), UiValue::String("untouched".into())),
])),
),
)]))
.unwrap(),
)
.unwrap();
let render = UiContext::new(
Rc::clone(&runtime),
owner.clone(),
Some("main".into()),
ExecutionPhase::Render,
BTreeMap::new(),
);
let structural = render
.for_structural_scope(owner.child("VirtualCollection", "rows"), BTreeMap::new());
let label = UiValuePath::new(vec![UiValuePathSegment::Key("label".into())]).unwrap();
let sibling =
UiValuePath::new(vec![UiValuePathSegment::Key("sibling".into())]).unwrap();
let value = if window_store {
structural.get_window_store_path("data", "model", &label)
} else {
structural.get_app_store_path("data", "model", &label)
}
.unwrap();
assert_eq!(value, UiValue::String("before".into()));
let mut event = render.clone();
event.phase = ExecutionPhase::Event;
let write = |path: &UiValuePath| {
if window_store {
event.set_window_store_path("data", "model", path, Dynamic::from("after"))
} else {
event.set_app_store_path("data", "model", path, Dynamic::from("after"))
}
.unwrap();
};
write(&sibling);
assert!(runtime.borrow().dirty.is_empty());
write(&label);
assert_eq!(runtime.borrow().dirty, BTreeSet::from([owner]));
}
}
#[test]
fn scripts_use_bounded_nested_and_keyed_store_paths() {
let context = mounted_context(ExecutionPhase::Event);
let row = |id: &str, label: &str| {
UiValue::Map(BTreeMap::from([
("id".to_owned(), UiValue::String(id.to_owned())),
("label".to_owned(), UiValue::String(label.to_owned())),
]))
};
context
.runtime()
.borrow_mut()
.stores
.declare(
StoreId::app("model"),
ComponentStateSchema::new(BTreeMap::from([(
"data".to_owned(),
StateField::new(
ValueSchema::UiValue,
UiValue::Map(BTreeMap::from([(
"rows".to_owned(),
UiValue::Array(vec![row("alpha", "Alpha"), row("beta", "Beta")]),
)])),
),
)]))
.unwrap(),
)
.unwrap();
let mut runtime = RuntimeEngine::new();
let compiled = runtime
.compile(
r#"
fn view() { text("paths") }
fn update(ctx) {
let name = ctx.get_state_path("profile", ["name"]);
ctx.set_state_path("profile", ["name"], name + " Lovelace");
let label = ctx.get_app_store_path(
"model",
"data",
["rows", #{ by: "id", key: "beta" }, "label"]
);
ctx.set_app_store_path(
"model",
"data",
["rows", #{ by: "id", key: "beta" }, "label"],
label + "!"
);
}
"#,
)
.unwrap();
runtime.render(&compiled).unwrap();
let callback = runtime.callback(&compiled, "update").unwrap();
let _: Dynamic = runtime
.invoke_callback(&compiled, &callback, (context.clone(),))
.unwrap();
let profile = context.get_state("profile").unwrap();
assert_eq!(
profile
.get_path(
&UiValuePath::new(vec![UiValuePathSegment::Key("name".to_owned())]).unwrap()
)
.unwrap(),
&UiValue::String("Ada Lovelace".to_owned())
);
let row_path = UiValuePath::new(vec![
UiValuePathSegment::Key("rows".to_owned()),
UiValuePathSegment::Item {
key_field: "id".to_owned(),
key: "beta".to_owned(),
},
UiValuePathSegment::Key("label".to_owned()),
])
.unwrap();
assert_eq!(
context
.runtime()
.borrow_mut()
.stores
.read_path_tracked(
&ComponentInstancePath::root("Test", "reader"),
&StoreId::app("model"),
"data",
&row_path,
)
.unwrap(),
UiValue::String("Beta!".to_owned())
);
assert!(rhai_value_path(vec![Dynamic::from(-1_i64)]).is_err());
}
#[test]
fn script_theme_switch_preserves_component_state_and_ast_generation() {
let context = mounted_context(ExecutionPhase::Event);
context.set_state("count", Dynamic::from(7_i64)).unwrap();
let mut runtime = RuntimeEngine::new();
let dark = crate::load_theme_source(
runtime.engine(),
"default_dark.rhai",
include_str!("../../../registry/themes/default_dark.rhai"),
)
.unwrap();
let mocha = crate::load_theme_source(
runtime.engine(),
"catppuccin_mocha.rhai",
include_str!("../../../registry/themes/catppuccin_mocha.rhai"),
)
.unwrap();
context.runtime().borrow_mut().theme = Some(
ThemeManager::from_variants([dark, mocha], ThemeSelection::new("Default", "Dark"))
.unwrap(),
);
let compiled = runtime
.compile(
r#"
fn view() { text("theme") }
fn switch_theme(ctx) { ctx.set_theme("Catppuccin", "Mocha"); }
"#,
)
.unwrap();
runtime.render(&compiled).unwrap();
let generation = compiled.generation();
let callback = runtime.callback(&compiled, "switch_theme").unwrap();
let _ = runtime
.invoke_callback(&compiled, &callback, (context.clone(),))
.unwrap();
assert_eq!(context.get_state("count").unwrap(), UiValue::Integer(7));
assert_eq!(compiled.generation(), generation);
let state = context.runtime().borrow();
let theme = state
.theme
.as_ref()
.unwrap()
.resolve(Some("main"), None, crate::SystemAppearance::Dark)
.unwrap();
assert_eq!(theme.variant().family, "Catppuccin");
assert_eq!(theme.variant().name, "Mocha");
drop(state);
context.set_theme_system("Default").unwrap();
let state = context.runtime().borrow();
assert!(matches!(
state.theme.as_ref().unwrap().app_preference(),
ThemePreference::System { family } if family == "Default"
));
}
#[test]
fn script_reads_fixed_today_calendar_and_locale_formatters() {
let mut engine = Engine::new();
register_ui_context_api(&mut engine);
let bundle = crate::load_locale_source(
&engine,
"en.rhai",
include_str!("../../../registry/locales/en.rhai"),
)
.unwrap();
let mut state = UiRuntimeState::new();
state.locale = Some(LocaleManager::new([bundle], "en", "en").unwrap());
state.calendar_clock =
crate::CalendarClock::fixed(crate::GregorianDate::parse_iso("2026-08-29").unwrap());
let context = UiContext::new(
Rc::new(RefCell::new(state)),
ComponentInstancePath::root("App", "root"),
Some("main".to_owned()),
ExecutionPhase::Render,
BTreeMap::new(),
);
let mut scope = rhai::Scope::new();
scope.push("ctx", context);
let values = engine
.eval_with_scope::<rhai::Array>(
&mut scope,
r#"[
ctx.today(),
ctx.format_date("2024-02-29", "long"),
ctx.format_number(12345, #{ min_fraction_digits: 0, max_fraction_digits: 0 }),
ctx.calendar().first_weekday,
]"#,
)
.unwrap();
assert_eq!(values[0].clone_cast::<String>(), "2026-08-29");
assert_eq!(
values[1].clone_cast::<String>(),
"Thursday, February 29, 2024"
);
assert_eq!(values[2].clone_cast::<String>(), "12,345");
assert_eq!(values[3].clone_cast::<String>(), "sunday");
}
#[test]
fn returning_subscription_work_closes_external_emitters_with_reason() {
let mut engine = RuntimeEngine::new();
let compiled = engine
.compile(
r#"
fn view() { text("subscription") }
fn success(ctx, value) { value }
fn failure(ctx, error) { error }
"#,
)
.unwrap();
let generation = compiled.generation();
let success = engine.callback(&compiled, "success").unwrap();
let failure = engine.callback(&compiled, "failure").unwrap();
let mut state = UiRuntimeState::new();
let registration = SubscriptionRegistration::new(
"app.stream.watch",
AsyncScope::App,
generation,
success,
failure,
ValueSchema::integer(),
);
let (_, emitter) = state.subscriptions.subscribe(registration);
let external = emitter.clone();
run_subscription_work(SubscriptionWork::new(|_| {}), emitter.clone(), &emitter);
assert!(matches!(
external.emit(UiValue::Integer(1)),
Err(AsyncRuntimeError::Closed {
reason: SubscriptionCloseReason::WorkReturned
})
));
let _ = state.subscriptions.drain(generation);
assert_eq!(
state.subscriptions.take_closures()[0].reason,
SubscriptionCloseReason::WorkReturned
);
let registration = SubscriptionRegistration::new(
"app.stream.trace",
AsyncScope::App,
generation,
engine.callback(&compiled, "success").unwrap(),
engine.callback(&compiled, "failure").unwrap(),
ValueSchema::integer(),
);
let (_, emitter) = state.subscriptions.subscribe(registration);
emitter.close_with_reason(SubscriptionCloseReason::WorkReturned);
let _ = state.subscriptions.drain(generation);
state.trace_subscription_closures();
let traces = state.traces.snapshot();
assert!(traces.iter().any(|trace| {
trace.kind == crate::RuntimeTraceKind::Subscription
&& trace.message == "close app.stream.trace: the subscription work returned"
}));
}
#[test]
fn receiver_subscription_forwards_external_values_until_sender_drop() {
let mut engine = RuntimeEngine::new();
let compiled = engine
.compile(
r#"
fn view() { text("subscription") }
fn success(ctx, value) { value }
fn failure(ctx, error) { error }
"#,
)
.unwrap();
let generation = compiled.generation();
let registration = SubscriptionRegistration::new(
"app.stream.receiver",
AsyncScope::App,
generation,
engine.callback(&compiled, "success").unwrap(),
engine.callback(&compiled, "failure").unwrap(),
ValueSchema::integer(),
);
let mut subscriptions = SubscriptionRegistry::new();
let (_, emitter) = subscriptions.subscribe(registration);
let external = emitter.clone();
let closer = emitter.clone();
let (sender, receiver) = std::sync::mpsc::channel();
let worker = std::thread::spawn(move || {
run_subscription_work(SubscriptionWork::from_receiver(receiver), emitter, &closer);
});
sender.send(UiValue::Integer(42)).unwrap();
drop(sender);
worker.join().unwrap();
let deliveries = subscriptions.drain(generation);
assert_eq!(deliveries[0].payload, UiValue::Integer(42));
assert!(matches!(
external.emit(UiValue::Integer(43)),
Err(AsyncRuntimeError::Closed {
reason: SubscriptionCloseReason::WorkReturned
})
));
}
#[test]
fn receiver_subscription_waits_for_lossless_capacity_without_ending_the_stream() {
let mut engine = RuntimeEngine::new();
let compiled = engine
.compile(
"fn view() { text(\"stream\") } fn success(ctx, value) {} fn failure(ctx, value) {}",
)
.unwrap();
engine.render(&compiled).unwrap();
let generation = compiled.generation();
let registration = SubscriptionRegistration::new(
"stream",
AsyncScope::App,
generation,
engine.callback(&compiled, "success").unwrap(),
engine.callback(&compiled, "failure").unwrap(),
ValueSchema::integer(),
)
.with_capacity(1)
.unwrap();
let mut subscriptions = SubscriptionRegistry::new();
let (_, emitter) = subscriptions.subscribe(registration);
let closer = emitter.clone();
let (sender, receiver) = std::sync::mpsc::channel();
let worker = std::thread::spawn(move || {
run_subscription_work(SubscriptionWork::from_receiver(receiver), emitter, &closer);
});
sender.send(UiValue::Integer(1)).unwrap();
sender.send(UiValue::Integer(2)).unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(1);
let first = loop {
let values = subscriptions.drain(generation);
if !values.is_empty() {
break values;
}
assert!(std::time::Instant::now() < deadline);
std::thread::yield_now();
};
assert_eq!(first[0].payload, UiValue::Integer(1));
let second = loop {
let values = subscriptions.drain(generation);
if !values.is_empty() {
break values;
}
assert!(std::time::Instant::now() < deadline);
std::thread::yield_now();
};
assert_eq!(second[0].payload, UiValue::Integer(2));
assert_eq!(subscriptions.active_count(), 1);
drop(sender);
worker.join().unwrap();
}
#[test]
fn runtime_transaction_rollback_restores_cancelled_subscription() {
let mut engine = RuntimeEngine::new();
let compiled = engine
.compile(
"fn view() { text(\"test\") } fn success(ctx, value) {} fn failure(ctx, value) {}",
)
.unwrap();
engine.render(&compiled).unwrap();
let success = engine.callback(&compiled, "success").unwrap();
let error = engine.callback(&compiled, "failure").unwrap();
let generation = compiled.generation();
let mut state = UiRuntimeState::new();
let (handle, emitter) = state.subscriptions.subscribe(SubscriptionRegistration::new(
"rollback",
AsyncScope::App,
generation,
success,
error,
ValueSchema::integer(),
));
let checkpoint = state.begin_transaction().unwrap();
assert!(state.subscriptions.cancel(handle));
assert_eq!(emitter.close_reason(), None);
state.rollback_transaction(checkpoint).unwrap();
assert_eq!(state.subscriptions.active_count(), 1);
emitter.emit(UiValue::Integer(7)).unwrap();
assert_eq!(state.subscriptions.drain(generation).len(), 1);
}
#[test]
fn transaction_tokens_cannot_cross_runtime_or_commit_unbalanced() {
let mut first = UiRuntimeState::new();
let mut second = UiRuntimeState::new();
let checkpoint = first.begin_transaction().unwrap();
assert!(matches!(
second.rollback_transaction(checkpoint.clone()),
Err(UiTransactionError::ForeignSnapshot)
));
assert!(matches!(
second.commit_transaction(),
Err(UiTransactionError::Unbalanced { .. })
));
first.rollback_transaction(checkpoint).unwrap();
}
#[test]
fn presentation_geometry_and_capture_are_isolated_per_view() {
let mut first = crate::RetainedUiTree::new();
first.reconcile(crate::UiNode::text("first")).unwrap();
let mut second = crate::RetainedUiTree::new();
second.reconcile(crate::UiNode::text("second")).unwrap();
let first_node = first.root_id().unwrap();
let second_node = second.root_id().unwrap();
assert_eq!(
first_node, second_node,
"tree-local IDs intentionally overlap"
);
let mut state = UiRuntimeState::new();
state.ensure_presentation("first");
state.ensure_presentation("second");
let first_geometry = state.geometry_for(Some("first"));
let second_geometry = state.geometry_for(Some("second"));
let bounds = |width| crate::ElementGeometry {
layout: crate::GeometryBounds::new(0.0, 0.0, width, 20.0).unwrap(),
visual: crate::GeometryBounds::new(0.0, 0.0, width, 20.0).unwrap(),
clip: None,
};
first_geometry.update(first_node, bounds(100.0));
second_geometry.update(second_node, bounds(300.0));
second_geometry.begin_frame();
assert!(
(first_geometry.get(first_node).unwrap().visual.width - 100.0).abs() < f64::EPSILON
);
assert!(first_geometry.is_presented(first_node));
let first_capture = state.pointer_capture_for(Some("first"));
let second_capture = state.pointer_capture_for(Some("second"));
first_capture.capture(0, first_node);
second_capture.capture(0, second_node);
assert_eq!(first_capture.captured(0), Some(first_node));
assert_eq!(second_capture.captured(0), Some(second_node));
}
#[test]
fn window_release_uses_segment_boundaries_and_clears_suspend_tombstones() {
let now = std::time::Instant::now();
let transition = || {
let mut spec =
crate::MotionTransition::new(crate::MotionProperty::Opacity, 0.0, 1.0, 1_000);
spec.easing = crate::MotionEasing::Linear;
crate::MotionSource::Transition(spec)
};
let mut state = UiRuntimeState::new();
let ghost_path = "window:w2/view:v2/root/ghost:1";
state
.motions
.start_exit(
ComponentInstancePath::root("UiNode", ghost_path),
transition(),
now,
)
.unwrap();
state.motion_ghosts.push(crate::motion::MotionGhost {
id: "1".to_owned(),
node: crate::UiNode::text("ghost"),
bounds: crate::GeometryBounds::new(0.0, 0.0, 10.0, 10.0).unwrap(),
path: ghost_path.to_owned(),
domain: "window:w2/view:v2/root".to_owned(),
});
state
.release_window("w", &ComponentInstancePath::root("View", "v"))
.unwrap();
assert_eq!(state.motion_ghosts.len(), 1);
assert_eq!(state.motions.resource_usage().active, 1);
let mut reopened = UiRuntimeState::new();
let scope = "window:w/view:v/root";
reopened
.motions
.start(
ComponentInstancePath::root("UiNode", format!("{scope}/node:1")),
transition(),
now,
)
.unwrap();
reopened
.motions
.suspend_node_scope(scope, now + Duration::from_millis(200));
reopened
.release_window("w", &ComponentInstancePath::root("View", "v"))
.unwrap();
let key = reopened
.motions
.start(
ComponentInstancePath::root("UiNode", format!("{scope}/node:2")),
transition(),
now + Duration::from_millis(200),
)
.unwrap();
let frame = reopened.motions.tick(now + Duration::from_millis(400));
assert!(frame.needs_frame);
assert!((frame.values[&key] - 0.2).abs() < 0.01);
}
#[test]
fn timeline_handles_cannot_cross_view_contexts() {
let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
let now = std::time::Instant::now();
let mut transition =
crate::MotionTransition::new(crate::MotionProperty::Opacity, 0.0, 1.0, 1_000);
transition.easing = crate::MotionEasing::Linear;
let handle = runtime
.borrow_mut()
.motions
.start_timeline(
ComponentInstancePath::root("UiNode", "window:a/view:a/root"),
crate::MotionTimeline::new(
"foreign",
crate::MotionTimelineStep::Track(crate::MotionTrack {
target: ".".to_owned(),
source: crate::MotionSource::Transition(transition),
}),
),
now,
)
.unwrap();
let foreign = UiContext::new(
Rc::clone(&runtime),
ComponentInstancePath::root("View", "b"),
Some("b".to_owned()),
ExecutionPhase::Event,
BTreeMap::new(),
)
.with_view_id("b");
assert!(matches!(
foreign.cancel_motion(&handle),
Err(UiContextError::Motion(
crate::MotionError::ForeignTimelineHandle(_)
))
));
assert_eq!(
runtime.borrow().motions.timeline_state(&handle),
Some(crate::MotionPlaybackState::Playing)
);
}
#[test]
fn focus_by_ref_key_queues_a_window_scoped_retained_command() {
let context = mounted_context(ExecutionPhase::Event);
let mut tree = crate::RetainedUiTree::new();
tree.reconcile(crate::UiNode::text("field")).unwrap();
let reference = crate::ElementRef::new(
crate::ElementRefId::new(context.component_path().clone(), "field").unwrap(),
);
context.runtime().borrow_mut().element_refs.reconcile(
context.component_path(),
BTreeMap::from([(reference.id().clone(), tree.root_id().unwrap())]),
);
let node = tree.root_id().unwrap();
context
.runtime()
.borrow()
.geometry_for(Some("main"))
.update(
node,
crate::ElementGeometry {
layout: crate::GeometryBounds::new(1.0, 2.0, 120.0, 24.0).unwrap(),
visual: crate::GeometryBounds::new(3.0, 4.0, 120.0, 24.0).unwrap(),
clip: None,
},
);
assert!(matches!(
context.element_bounds_by_key("field").unwrap(),
UiValue::Map(bounds)
if matches!(bounds.get("visual"), Some(UiValue::Map(visual))
if visual.get("width") == Some(&UiValue::Float(120.0)))
));
context.focus_element_by_key("field").unwrap();
context
.scroll_element_to_by_key("field", 12.0, 24.0)
.unwrap();
context.scroll_element_into_view_by_key("field").unwrap();
let commands = context
.runtime()
.borrow_mut()
.take_window_element_commands("main");
assert!(matches!(
commands.as_slice(),
[crate::element_ref::ElementCommand::Focus { node, .. },
crate::element_ref::ElementCommand::ScrollTo { node: scroll_node, x, y, .. },
crate::element_ref::ElementCommand::ScrollIntoView { node: reveal_node, .. }]
if *node == tree.root_id().unwrap()
&& scroll_node == node
&& reveal_node == node
&& (*x - 12.0).abs() < f64::EPSILON
&& (*y - 24.0).abs() < f64::EPSILON
));
}
#[test]
fn host_motion_preference_changes_without_replacing_runtime_state() {
let mut runtime = UiRuntimeState::new();
runtime.motions = crate::MotionRuntime::new(crate::MotionPreference::Normal);
runtime.responsive.update_window("main", 480.0).unwrap();
assert!(!runtime.set_motion_preference_from_host(crate::MotionPreference::Normal));
assert!(runtime.set_motion_preference_from_host(crate::MotionPreference::Reduced));
assert_eq!(
runtime.motions.preference(),
crate::MotionPreference::Reduced
);
assert_eq!(
runtime.responsive.class("main"),
crate::ViewportClass::Compact
);
assert!(runtime.set_motion_preference_from_host(crate::MotionPreference::None));
assert_eq!(runtime.motions.preference(), crate::MotionPreference::None);
}
#[test]
fn host_can_register_a_new_native_collection_after_mount() {
let mut runtime = UiRuntimeState::new();
let root = ComponentInstancePath::root("View", "live");
assert!(
runtime
.native_collections
.read_tracked(&root, "live_rows")
.is_err()
);
let collection = crate::NativeCollection::new(
"id",
[BTreeMap::from([(
"id".to_owned(),
UiValue::String("row-1".to_owned()),
)])],
)
.unwrap();
runtime
.register_native_collection_from_host(&root, "live_rows", collection)
.unwrap();
assert!(runtime.has_window_dirty(&root));
assert!(matches!(
runtime.register_native_collection_from_host(
&root,
"live_rows",
crate::NativeCollection::new("id", std::iter::empty::<BTreeMap<String, UiValue>>())
.unwrap(),
),
Err(crate::NativeCollectionError::DuplicateCollection(_))
));
}
}