diff --git a/CHANGES.md b/CHANGES.md index 73d91bf5..c2eb4567 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +* 0.2.12 + * Clamping is now true by default on overflow + * Globals can now be registered with the runtime + * `attributes.value_as::` is now available on `Attributes` + * Performance improvements * 0.2.11 * FEATURE: ranges * `padding` function diff --git a/Cargo.toml b/Cargo.toml index 280f0f7f..f20ef105 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "anathema" edition = "2024" -version = "0.2.11" +version = "0.2.12-beta" license = "MIT" description = "Create beautiful, easily customisable terminal applications" keywords = ["tui", "terminal", "widgets", "ui", "layout"] @@ -31,7 +31,7 @@ anathema-testutils = { path = "anathema-testutils" } [features] default = [] -profile = ["anathema-runtime/profile", "anathema-widgets/profile", "anathema-backend/profile"] +profile = ["anathema-runtime/profile", "anathema-widgets/profile", "anathema-backend/profile", "anathema-value-resolver/profile"] serde = ["anathema-state/serde", "anathema-store/serde"] # filelog = ["anathema-debug/filelog", "anathema-widgets/filelog", "anathema-runtime/filelog"] @@ -40,7 +40,7 @@ workspace = true [workspace.package] edition = "2024" -version = "0.2.11" +version = "0.2.12-beta" [workspace.dependencies] bitflags = "2.4.1" @@ -48,16 +48,16 @@ crossterm = "0.28.1" unicode-width = "0.1.11" flume = "0.11.0" notify = "6.1.1" -anathema-default-widgets = { path = "./anathema-default-widgets", version = "0.2.11" } -anathema-backend = { path = "./anathema-backend", version = "0.2.11" } -anathema-runtime = { path = "./anathema-runtime", version = "0.2.11" } -anathema-state = { path = "./anathema-state", version = "0.2.11" } -anathema-state-derive = { path = "./anathema-state-derive", version = "0.2.11" } -anathema-store = { path = "./anathema-store", version = "0.2.11" } -anathema-templates = { path = "./anathema-templates", version = "0.2.11" } -anathema-widgets = { path = "./anathema-widgets", version = "0.2.11" } -anathema-geometry = { path = "./anathema-geometry", version = "0.2.11" } -anathema-value-resolver = { path = "./anathema-value-resolver", version = "0.2.11" } +anathema-default-widgets = { path = "./anathema-default-widgets", version = "0.2.12-beta" } +anathema-backend = { path = "./anathema-backend", version = "0.2.12-beta" } +anathema-runtime = { path = "./anathema-runtime", version = "0.2.12-beta" } +anathema-state = { path = "./anathema-state", version = "0.2.12-beta" } +anathema-state-derive = { path = "./anathema-state-derive", version = "0.2.12-beta" } +anathema-store = { path = "./anathema-store", version = "0.2.12-beta" } +anathema-templates = { path = "./anathema-templates", version = "0.2.12-beta" } +anathema-widgets = { path = "./anathema-widgets", version = "0.2.12-beta" } +anathema-geometry = { path = "./anathema-geometry", version = "0.2.12-beta" } +anathema-value-resolver = { path = "./anathema-value-resolver", version = "0.2.12-beta" } [workspace] members = [ diff --git a/anathema-backend/src/lib.rs b/anathema-backend/src/lib.rs index a3958f7e..6fe3cd0b 100644 --- a/anathema-backend/src/lib.rs +++ b/anathema-backend/src/lib.rs @@ -7,7 +7,9 @@ use anathema_widgets::components::events::Event; use anathema_widgets::error::Result; use anathema_widgets::layout::{Constraints, LayoutCtx, LayoutFilter, PositionFilter, Viewport}; use anathema_widgets::paint::PaintFilter; -use anathema_widgets::{GlyphMap, LayoutForEach, PaintChildren, PositionChildren, WidgetTreeView}; +use anathema_widgets::{ + DirtyWidgets, GlyphMap, Layout, LayoutForEach, PaintChildren, PositionChildren, WidgetTreeView, +}; pub mod testing; pub mod tui; @@ -55,14 +57,7 @@ impl<'rt, 'bp, T: Backend> WidgetCycle<'rt, 'bp, T> { } } - fn fixed(&mut self, ctx: &mut LayoutCtx<'_, 'bp>, needs_layout: bool) -> Result<()> { - // ----------------------------------------------------------------------------- - // - Layout - - // ----------------------------------------------------------------------------- - if needs_layout { - self.layout(ctx, LayoutFilter)?; - } - + fn fixed(&mut self, ctx: &mut LayoutCtx<'_, 'bp>) -> Result<()> { // ----------------------------------------------------------------------------- // - Position - // ----------------------------------------------------------------------------- @@ -90,24 +85,107 @@ impl<'rt, 'bp, T: Backend> WidgetCycle<'rt, 'bp, T> { Ok(()) } - pub fn run(&mut self, ctx: &mut LayoutCtx<'_, 'bp>, needs_layout: bool) -> Result<()> { - self.fixed(ctx, needs_layout)?; + pub fn run( + &mut self, + ctx: &mut LayoutCtx<'_, 'bp>, + force_layout: bool, + dirty_widgets: &mut DirtyWidgets, + ) -> Result<()> { + // ----------------------------------------------------------------------------- + // - Layout - + // ----------------------------------------------------------------------------- + self.layout(ctx, LayoutFilter, dirty_widgets, force_layout)?; + + // ----------------------------------------------------------------------------- + // - Position and paint - + // ----------------------------------------------------------------------------- + self.fixed(ctx)?; self.floating(ctx)?; Ok(()) } - fn layout(&mut self, ctx: &mut LayoutCtx<'_, 'bp>, filter: LayoutFilter) -> Result<()> { + fn layout( + &mut self, + ctx: &mut LayoutCtx<'_, 'bp>, + filter: LayoutFilter, + dirty_widgets: &mut DirtyWidgets, + force_layout: bool, + ) -> Result<()> { #[cfg(feature = "profile")] puffin::profile_function!(); - let tree = self.tree.view(); - - let scope = Scope::root(); - let mut for_each = LayoutForEach::new(tree, &scope, filter, None); - let constraints = self.constraints; - _ = for_each.each(ctx, |ctx, widget, children| { - _ = widget.layout(children, constraints, ctx)?; - Ok(ControlFlow::Break(())) - })?; + + let mut tree = self.tree.view(); + + if force_layout { + // Perform a layout across the entire tree + let scope = Scope::root(); + let mut for_each = LayoutForEach::new(tree, &scope, filter); + let constraints = self.constraints; + _ = for_each.each(ctx, |ctx, widget, children| { + _ = widget.layout(children, constraints, ctx)?; + Ok(ControlFlow::Break(())) + })?; + return Ok(()); + } + + // If a widget has changed, mark the parent as dirty + + // Layout only changed widgets. + // These are the parents of changed widgets. + // + // Investigate the possibility of attaching an offset as existing widgets don't need + // to reflow unless the constraint has changed. + // + // This means `additional_widgets` needs to store (key, offset) where offset can be None + // + // If this is going to work we need to consider `expand` and `spacer` + // + // Since widgets can be made by anyone and they are always guaranteed to give + // access to all their children this might not be a possibility. + // + // parent + // widget 0 + // widget 1 + // widget 2 | <- if this changes, only reflow this, three and four + // widget 3 |-- reflow + // widget 4 | + + // TODO: make `additional_widgets` a scratch buffer part of `DirtyWidgets`. + // Also ensure that it tracks last id as well + // ... and removes it when done! + let mut additional_widgets = vec![]; + + loop { + for widget_id in dirty_widgets.drain() { + if !tree.contains(widget_id) { + continue; + } + tree.with_value_mut(widget_id, |_, widget, children| { + let scope = Scope::root(); + let mut children = LayoutForEach::new(children, &scope, filter); + children.parent_element = Some(widget_id); + let parent_id = widget.parent_widget; + let anathema_widgets::WidgetKind::Element(widget) = &mut widget.kind else { return }; + + let constraints = widget.constraints(); + if let Ok(Layout::Changed(_)) = widget.layout(children, constraints, ctx) { + // write into scratch buffer + if let Some(id) = parent_id { + additional_widgets.push(id); + } + } + }); + } + + // merge the scratch if it's not empty + + if additional_widgets.is_empty() { + break; + } + + dirty_widgets.inner.append(&mut additional_widgets); + } + Ok(()) } diff --git a/anathema-default-widgets/src/overflow.rs b/anathema-default-widgets/src/overflow.rs index 1d7a2e53..1a869c49 100644 --- a/anathema-default-widgets/src/overflow.rs +++ b/anathema-default-widgets/src/overflow.rs @@ -163,7 +163,7 @@ impl Widget for Overflow { let mut pos = ctx.pos; // If the value is clamped, update the offset - match attributes.get_as::(CLAMP).unwrap_or_default() { + match attributes.get_as::(CLAMP).unwrap_or(true) { false => (), true => self.clamp(self.inner_size, ctx.inner_size), } diff --git a/anathema-default-widgets/src/testing.rs b/anathema-default-widgets/src/testing.rs index d5285cba..b0591013 100644 --- a/anathema-default-widgets/src/testing.rs +++ b/anathema-default-widgets/src/testing.rs @@ -11,7 +11,7 @@ use anathema_widgets::layout::{Constraints, LayoutCtx, Viewport}; use anathema_widgets::paint::{Glyph, paint}; use anathema_widgets::query::{Children, Elements}; use anathema_widgets::{ - Components, Factory, FloatingWidgets, GlyphMap, Style, WidgetKind, WidgetRenderer, WidgetTree, eval_blueprint, + Components, DirtyWidgets, Factory, FloatingWidgets, GlyphMap, Style, WidgetRenderer, WidgetTree, eval_blueprint, update_widget, }; @@ -159,7 +159,8 @@ impl TestRunner { let main = doc.add_component("main", src.to_template()).unwrap(); component_registry.add_component(main, (), TestState::new()); - let (blueprint, globals) = doc.compile().unwrap(); + let mut variables = Default::default(); + let blueprint = doc.compile(&mut variables).unwrap(); Self { factory, @@ -167,7 +168,7 @@ impl TestRunner { states, component_registry, blueprint, - variables: globals, + variables, components: Components::new(), function_table: FunctionTable::new(), } @@ -234,7 +235,7 @@ impl<'bp> TestInstance<'bp> { function_table, ); - let mut ctx = ctx.eval_ctx(None); + let mut ctx = ctx.eval_ctx(None, None); let mut view = tree.view(); eval_blueprint(blueprint, &mut ctx, &scope, &[], &mut view).unwrap(); @@ -290,11 +291,12 @@ impl<'bp> TestInstance<'bp> { sub.iter().for_each(|value_id| { let widget_id = value_id.key(); - if let Some(widget) = tree.get_mut(widget_id) { - if let WidgetKind::Element(element) = &mut widget.kind { - element.invalidate_cache(); - } - } + // TODO: review if this is still needed: - TB 2025-08-27 + // if let Some(widget) = tree.get_mut(widget_id) { + // if let WidgetKind::Element(element) = &mut widget.kind { + // element.invalidate_cache(); + // } + // } // check that the node hasn't already been removed if !tree.contains(widget_id) { @@ -303,7 +305,7 @@ impl<'bp> TestInstance<'bp> { _ = tree .with_value_mut(value_id.key(), |_path, widget, tree| { - update_widget(widget, value_id, change, tree, &mut ctx) + update_widget(widget, value_id, change, tree, &mut ctx, &mut DirtyWidgets::empty()) }) .unwrap(); }) @@ -332,7 +334,7 @@ impl<'bp> TestInstance<'bp> { ); let mut cycle = WidgetCycle::new(self.backend, self.tree.view(), constraints); - _ = cycle.run(&mut ctx, true); + _ = cycle.run(&mut ctx, true, &mut DirtyWidgets::empty()); self.backend.render(&mut self.glyph_map); @@ -354,8 +356,8 @@ impl<'bp> TestInstance<'bp> { // let path = &[0, 0, 0]; let tree = self.tree.view(); - let mut update = true; - let mut children = Children::new(tree, &mut self.attribute_storage, &mut update); + let mut dirty_widgets = DirtyWidgets::empty(); + let mut children = Children::new(tree, &mut self.attribute_storage, &mut dirty_widgets); let elements = children.elements(); f(elements); self diff --git a/anathema-default-widgets/src/text.rs b/anathema-default-widgets/src/text.rs index 8fbbd354..5d88a9cb 100644 --- a/anathema-default-widgets/src/text.rs +++ b/anathema-default-widgets/src/text.rs @@ -97,8 +97,8 @@ impl Widget for Text { let Some(_span) = child.try_to_ref::() else { return Ok(ControlFlow::Continue(())); }; - self.strings.set_style(child.id()); + self.strings.set_style(child.id()); let attributes = ctx.attributes(child.id()); if let Some(text) = attributes.value() { text.strings(|s| match self.strings.add_str(s) { diff --git a/anathema-geometry/src/region.rs b/anathema-geometry/src/region.rs index 6046638c..cbc6d691 100644 --- a/anathema-geometry/src/region.rs +++ b/anathema-geometry/src/region.rs @@ -49,12 +49,19 @@ impl Region { } /// Check if a region contains a position. - /// Regions are exclusive, so a region from 0,0 to 10, 10 contains `Pos::ZERO` + /// The check is exclusive, so a region from 0,0 to 10, 10 contains `Pos::ZERO` /// but not `Pos::New(10, 10)` pub const fn contains(&self, pos: Pos) -> bool { pos.x >= self.from.x && pos.x < self.to.x && pos.y >= self.from.y && pos.y < self.to.y } + /// Check if a region contains a position. + /// The check is inclusive, so a region from 0,0 to 10, 10 contains `Pos::ZERO` + /// as well as `Pos::New(10, 10)` + pub const fn icontains(&self, pos: Pos) -> bool { + pos.x >= self.from.x && pos.x <= self.to.x && pos.y >= self.from.y && pos.y <= self.to.y + } + /// Constrain a region to fit within another region pub fn constrain(&mut self, other: &Region) { self.from.x = self.from.x.max(other.from.x); diff --git a/anathema-runtime/src/builder.rs b/anathema-runtime/src/builder.rs index 1e640aa1..46b7755a 100644 --- a/anathema-runtime/src/builder.rs +++ b/anathema-runtime/src/builder.rs @@ -3,7 +3,7 @@ use std::sync::atomic::Ordering; use anathema_backend::Backend; use anathema_default_widgets::register_default_widgets; use anathema_geometry::Size; -use anathema_templates::{Document, ToSourceKind}; +use anathema_templates::{Document, Expression, ToSourceKind, Variables}; use anathema_value_resolver::{Function, FunctionTable}; use anathema_widgets::components::deferred::DeferredComponents; use anathema_widgets::components::events::Event; @@ -28,6 +28,7 @@ pub struct Builder { global_event_handler: G, hot_reload: bool, function_table: FunctionTable, + variables: Variables, } impl Builder { @@ -54,6 +55,7 @@ impl Builder { global_event_handler, hot_reload: true, function_table: FunctionTable::new(), + variables: Variables::new(), } } @@ -164,9 +166,15 @@ impl Builder { global_event_handler, hot_reload: self.hot_reload, function_table: self.function_table, + variables: Variables::new(), } } + pub fn register_global(&mut self, key: impl Into, value: impl Into) -> Result<()> { + self.variables.define_global(key, value).map_err(|e| e.to_error(None))?; + Ok(()) + } + pub fn finish(mut self, backend: &mut B, mut f: F) -> Result<()> where F: FnMut(&mut Runtime, &mut B) -> Result<()>, @@ -180,8 +188,8 @@ impl Builder { server }; - let (blueprint, globals) = loop { - match self.document.compile() { + let blueprint = loop { + match self.document.compile(&mut self.variables) { Ok(val) => break val, // This can only show template errors. // Widget errors doesn't become available until after the first tick. @@ -199,7 +207,7 @@ impl Builder { let mut inst = Runtime::new( blueprint, - globals, + self.variables, self.component_registry, self.document, self.factory, diff --git a/anathema-runtime/src/runtime/mod.rs b/anathema-runtime/src/runtime/mod.rs index cba995d1..224ca467 100644 --- a/anathema-runtime/src/runtime/mod.rs +++ b/anathema-runtime/src/runtime/mod.rs @@ -7,7 +7,7 @@ use anathema_geometry::Size; use anathema_state::{Changes, StateId, States, clear_all_changes, clear_all_subs, drain_changes}; use anathema_store::tree::root_node; use anathema_templates::blueprints::Blueprint; -use anathema_templates::{Document, Variables}; +use anathema_templates::{Document, Expression, Variables}; use anathema_value_resolver::{AttributeStorage, FunctionTable, Scope}; use anathema_widgets::components::deferred::{CommandKind, DeferredComponents}; use anathema_widgets::components::events::{Event, EventType}; @@ -19,8 +19,8 @@ use anathema_widgets::layout::{LayoutCtx, Viewport}; use anathema_widgets::query::Children; use anathema_widgets::tabindex::{Index, TabIndex}; use anathema_widgets::{ - Component, Components, Factory, FloatingWidgets, GlyphMap, WidgetContainer, WidgetId, WidgetKind, WidgetTree, - eval_blueprint, update_widget, + Component, Components, DirtyWidgets, Factory, FloatingWidgets, GlyphMap, WidgetContainer, WidgetId, WidgetKind, + WidgetTree, eval_blueprint, update_widget, }; use flume::Receiver; use notify::RecommendedWatcher; @@ -65,6 +65,11 @@ impl Runtime<()> { Builder::new(doc, backend.size(), ()) } + pub fn register_global(&mut self, key: impl Into, value: impl Into) -> Result<()> { + self.variables.define_global(key, value).map_err(|e| e.to_error(None))?; + Ok(()) + } + /// Create a runtime builder using an existing emitter pub fn with_receiver( message_receiver: MessageReceiver, @@ -178,7 +183,11 @@ impl Runtime { return Err(Error::Stop); } - frame.present(backend); + if frame.needs_paint { + frame.present(backend); + frame.needs_paint = false; + } + frame.cleanup(); std::thread::sleep(Duration::from_micros(sleep_micros)); } @@ -247,11 +256,13 @@ impl Runtime { message_receiver: &self.message_receiver, dt: &mut self.dt, - needs_layout: true, + force_layout: true, + needs_paint: true, post_cycle_events: VecDeque::new(), global_event_handler: &self.global_event_handler, tabindex: None, + dirty_widgets: DirtyWidgets::empty(), }; Ok(inst) @@ -267,9 +278,8 @@ impl Runtime { // Reload templates self.document.reload_templates()?; - let (blueprint, variables) = self.document.compile()?; + let blueprint = self.document.compile(&mut self.variables)?; self.blueprint = blueprint; - self.variables = variables; Ok(()) } @@ -287,8 +297,10 @@ pub struct Frame<'rt, 'bp, G> { emitter: &'rt Emitter, message_receiver: &'rt flume::Receiver, dt: &'rt mut Instant, - needs_layout: bool, + force_layout: bool, + needs_paint: bool, post_cycle_events: VecDeque, + dirty_widgets: DirtyWidgets, global_event_handler: &'rt G, pub tabindex: Option, @@ -368,7 +380,7 @@ impl<'rt, 'bp, G: GlobalEventHandler> Frame<'rt, 'bp, G> { // Should be called only once to initialise the node tree. pub fn init_tree(&mut self) -> Result<()> { - let mut ctx = self.layout_ctx.eval_ctx(None); + let mut ctx = self.layout_ctx.eval_ctx(None, None); eval_blueprint( self.blueprint, &mut ctx, @@ -410,14 +422,12 @@ impl<'rt, 'bp, G: GlobalEventHandler> Frame<'rt, 'bp, G> { } } - pub fn present(&mut self, backend: &mut B) -> Duration { + pub fn present(&mut self, backend: &mut B) { #[cfg(feature = "profile")] puffin::profile_function!(); - let now = Instant::now(); backend.render(self.layout_ctx.glyph_map); backend.clear(); - now.elapsed() } pub fn cleanup(&mut self) { @@ -474,7 +484,7 @@ impl<'rt, 'bp, G: GlobalEventHandler> Frame<'rt, 'bp, G> { while let Some(event) = backend.next_event(remaining) { if let Event::Resize(size) = event { self.layout_ctx.viewport.resize(size); - self.needs_layout = true; + self.force_layout = true; backend.resize(size, self.layout_ctx.glyph_map); } @@ -626,10 +636,16 @@ impl<'rt, 'bp, G: GlobalEventHandler> Frame<'rt, 'bp, G> { #[cfg(feature = "profile")] puffin::profile_function!(); + if !self.force_layout && self.dirty_widgets.is_empty() { + return Ok(()); + } + let mut cycle = WidgetCycle::new(backend, self.tree.view(), self.layout_ctx.viewport.constraints()); - cycle.run(&mut self.layout_ctx, self.needs_layout)?; + cycle.run(&mut self.layout_ctx, self.force_layout, &mut self.dirty_widgets)?; + + self.force_layout = false; + self.needs_paint = true; - self.needs_layout = false; Ok(()) } @@ -643,30 +659,31 @@ impl<'rt, 'bp, G: GlobalEventHandler> Frame<'rt, 'bp, G> { return Ok(()); } - self.needs_layout = true; + // self.needs_layout = true; let mut tree = self.tree.view(); self.changes.iter().try_for_each(|(sub, change)| { sub.iter().try_for_each(|value_id| { let widget_id = value_id.key(); - if let Some(widget) = tree.get_mut(widget_id) { - if let WidgetKind::Element(element) = &mut widget.kind { - element.invalidate_cache(); - } - } - // check that the node hasn't already been removed if !tree.contains(widget_id) { return Result::Ok(()); } tree.with_value_mut(widget_id, |_path, widget, tree| { - update_widget(widget, value_id, change, tree, &mut self.layout_ctx) + update_widget( + widget, + value_id, + change, + tree, + &mut self.layout_ctx, + &mut self.dirty_widgets, + ) }) .unwrap_or(Ok(()))?; - Ok(()) + Result::Ok(()) })?; Result::Ok(()) @@ -701,7 +718,7 @@ impl<'rt, 'bp, G: GlobalEventHandler> Frame<'rt, 'bp, G> { self.layout_ctx .attribute_storage .with_mut(widget_id, |attributes, storage| { - let elements = Children::new(children, storage, &mut self.needs_layout); + let elements = Children::new(children, storage, &mut self.dirty_widgets); let ctx = AnyComponentContext::new( component.parent.map(Into::into), diff --git a/anathema-runtime/src/runtime/testing.rs b/anathema-runtime/src/runtime/testing.rs index 149ba361..4d6637f1 100644 --- a/anathema-runtime/src/runtime/testing.rs +++ b/anathema-runtime/src/runtime/testing.rs @@ -3,7 +3,6 @@ use std::time::{Duration, Instant}; use anathema_backend::Backend; use anathema_state::{Watched, Watcher, drain_watchers}; use anathema_store::stack::Stack; -use anathema_widgets::query::Children; use crate::Frame; use crate::error::Result; @@ -17,13 +16,14 @@ impl<'bp, G> Frame<'_, 'bp, G> where G: GlobalEventHandler, { - pub fn elements(&mut self) -> Children<'_, 'bp> { - Children::new( - self.tree.view(), - self.layout_ctx.attribute_storage, - &mut self.needs_layout, - ) - } + // TODO: Do we need this for anything? - TB 2025-08-27 + // pub fn elements(&mut self) -> Children<'_, 'bp> { + // Children::new( + // self.tree.view(), + // self.layout_ctx.attribute_storage, + // &mut self.needs_layout, + // ) + // } // TODO: this can't really be called a frame if we can tick it multiple // times. Maybe RuntimeMut or something less mental diff --git a/anathema-state/src/lib.rs b/anathema-state/src/lib.rs index 414270c9..6a1dd47b 100644 --- a/anathema-state/src/lib.rs +++ b/anathema-state/src/lib.rs @@ -6,9 +6,10 @@ use anathema_store::slab::Key; pub use crate::colors::{Color, FromColor}; pub use crate::numbers::Number; pub use crate::states::{AnyList, AnyMap, State, StateId, States, TypeId}; +pub use crate::store::subscriber::SubTo; pub use crate::store::watchers::Watcher; pub use crate::store::{ - Change, Changes, SubTo, Subscriber, Watched, clear_all_changes, clear_all_subs, drain_changes, drain_watchers, + Change, Changes, Subscriber, Watched, clear_all_changes, clear_all_subs, drain_changes, drain_watchers, }; pub use crate::value::{List, Map, Maybe, Nullable, PendingValue, SharedState, Type, Value, ValueRef}; diff --git a/anathema-state/src/store/mod.rs b/anathema-state/src/store/mod.rs index 463c090b..fd7ee010 100644 --- a/anathema-state/src/store/mod.rs +++ b/anathema-state/src/store/mod.rs @@ -8,8 +8,8 @@ use watchers::{Watcher, Watchers}; pub(crate) use self::change::changed; pub use self::change::{Change, Changes, clear_all_changes, drain_changes}; +pub use self::subscriber::Subscriber; use self::subscriber::{SubKey, SubscriberMap}; -pub use self::subscriber::{SubTo, Subscriber}; use crate::Type; mod change; diff --git a/anathema-state/src/store/subscriber.rs b/anathema-state/src/store/subscriber.rs index dc3a70fa..c5191381 100644 --- a/anathema-state/src/store/subscriber.rs +++ b/anathema-state/src/store/subscriber.rs @@ -4,113 +4,6 @@ use anathema_store::smallmap::SmallIndex; use super::SUBSCRIBERS; use crate::Key; -/// Store sub keys. -/// This is used by any type that needs to track any or all the -/// values that it subscribes to. -#[derive(Debug, Default)] -pub enum SubTo { - #[default] - Zero, - One(SubKey), - Two(SubKey, SubKey), - Three(SubKey, SubKey, SubKey), - Four(SubKey, SubKey, SubKey, SubKey), - Many(Vec), -} - -impl SubTo { - pub fn empty() -> Self { - Self::Zero - } - - pub fn push(&mut self, key: SubKey) { - let this = std::mem::take(self); - *self = match this { - Self::Zero => Self::One(key), - Self::One(key1) => Self::Two(key1, key), - Self::Two(key1, key2) => Self::Three(key1, key2, key), - Self::Three(key1, key2, key3) => Self::Four(key1, key2, key3, key), - Self::Four(key1, key2, key3, key4) => Self::Many(vec![key1, key2, key3, key4]), - Self::Many(mut keys) => { - keys.push(key); - Self::Many(keys) - } - } - } - - pub fn unsubscribe(&mut self, sub: Subscriber) { - match std::mem::take(self) { - SubTo::Zero => (), - SubTo::One(key) => { - unsubscribe(key, sub); - } - SubTo::Two(key1, key2) => { - unsubscribe(key1, sub); - unsubscribe(key2, sub); - } - SubTo::Three(key1, key2, key3) => { - unsubscribe(key1, sub); - unsubscribe(key2, sub); - unsubscribe(key3, sub); - } - SubTo::Four(key1, key2, key3, key4) => { - unsubscribe(key1, sub); - unsubscribe(key2, sub); - unsubscribe(key3, sub); - unsubscribe(key4, sub); - } - SubTo::Many(vec) => vec.into_iter().for_each(|key| unsubscribe(key, sub)), - } - } - - // TODO: clean this up, it's gross - pub fn remove(&mut self, sub_key: SubKey) { - match self { - SubTo::Zero => (), - SubTo::One(key) if *key == sub_key => *self = SubTo::Zero, - SubTo::Two(key1, key2) if *key1 == sub_key => *self = SubTo::One(*key2), - SubTo::Two(key1, key2) if *key2 == sub_key => *self = SubTo::One(*key1), - SubTo::Three(key1, key2, key3) => { - if sub_key == *key1 { - *self = SubTo::Two(*key2, *key3); - return; - } - - if sub_key == *key2 { - *self = SubTo::Two(*key1, *key3); - return; - } - - if sub_key == *key3 { - *self = SubTo::Two(*key1, *key2); - } - } - SubTo::Four(key1, key2, key3, key4) => { - if sub_key == *key1 { - *self = SubTo::Three(*key2, *key3, *key4); - return; - } - - if sub_key == *key2 { - *self = SubTo::Three(*key1, *key3, *key4); - return; - } - - if sub_key == *key3 { - *self = SubTo::Three(*key1, *key2, *key4); - return; - } - - if sub_key == *key4 { - *self = SubTo::Three(*key1, *key2, *key3); - } - } - SubTo::Many(vec) => vec.retain(|key| *key != sub_key), - _ => {} - } - } -} - #[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] #[repr(transparent)] pub struct KeyIndex(u8); @@ -215,20 +108,18 @@ impl Subscribers { match self { Self::Empty => *self = Self::One(sub), Self::One(key) if *key != sub => *self = Self::Arr([*key, sub, Subscriber::MAX], KeyIndex::TWO), + Self::One(key) => *self = Self::Arr([*key, sub, Subscriber::MAX], KeyIndex::TWO), Self::Arr(arr_keys, index) if *index == KeyIndex::MAX => { let mut keys = Vec::with_capacity(KeyIndex::max() + 1); keys.extend_from_slice(arr_keys); keys.push(sub); *self = Self::Heap(keys); } - Self::Arr(keys, index) if !keys.contains(&sub) => { + Self::Arr(keys, index) => { keys[index.0 as usize] = sub; index.add(); } - Self::Heap(keys) if !keys.contains(&sub) => keys.push(sub), - - // The sub is already registered - Self::Arr(..) | Self::One(_) | Self::Heap(_) => (), + Self::Heap(keys) => keys.push(sub), } } @@ -353,6 +244,140 @@ pub(crate) fn unsubscribe(sub_key: SubKey, subscriber: Subscriber) { SUBSCRIBERS.with_borrow_mut(|subs| subs.unsubscribe(sub_key, subscriber)); } +// ----------------------------------------------------------------------------- +// - Track what values something is subscribed to - +// ----------------------------------------------------------------------------- + +/// Store sub keys. +/// This is used by any type that needs to track any or all the +/// values that it subscribes to. +#[derive(Debug, Default)] +pub enum SubTo { + #[default] + Zero, + One(SubKey), + Two(SubKey, SubKey), + Three(SubKey, SubKey, SubKey), + Four(SubKey, SubKey, SubKey, SubKey), + Many(Vec), +} + +impl SubTo { + pub fn empty() -> Self { + Self::Zero + } + + pub fn push(&mut self, key: SubKey) { + let this = std::mem::take(self); + *self = match this { + Self::Zero => Self::One(key), + Self::One(key1) => Self::Two(key1, key), + Self::Two(key1, key2) => Self::Three(key1, key2, key), + Self::Three(key1, key2, key3) => Self::Four(key1, key2, key3, key), + Self::Four(key1, key2, key3, key4) => Self::Many(vec![key1, key2, key3, key4]), + Self::Many(mut keys) => { + keys.push(key); + Self::Many(keys) + } + } + } + + // TODO: another gross function impl + // - TB 2025-09-11 + pub fn merge(&mut self, other: Self) { + match other { + SubTo::Zero => (), + SubTo::One(key) => self.push(key), + SubTo::Two(key1, key2) => { + self.push(key1); + self.push(key2); + } + SubTo::Three(key1, key2, key3) => { + self.push(key1); + self.push(key2); + self.push(key3); + } + SubTo::Four(key1, key2, key3, key4) => { + self.push(key1); + self.push(key2); + self.push(key3); + self.push(key4); + } + SubTo::Many(items) => items.into_iter().for_each(|key| self.push(key)), + } + } + + pub fn unsubscribe(&mut self, sub: Subscriber) { + match std::mem::take(self) { + SubTo::Zero => (), + SubTo::One(key) => unsubscribe(key, sub), + SubTo::Two(key1, key2) => { + unsubscribe(key1, sub); + unsubscribe(key2, sub); + } + SubTo::Three(key1, key2, key3) => { + unsubscribe(key1, sub); + unsubscribe(key2, sub); + unsubscribe(key3, sub); + } + SubTo::Four(key1, key2, key3, key4) => { + unsubscribe(key1, sub); + unsubscribe(key2, sub); + unsubscribe(key3, sub); + unsubscribe(key4, sub); + } + SubTo::Many(vec) => vec.into_iter().for_each(|key| unsubscribe(key, sub)), + } + } + + // TODO: clean this up, it's gross + pub fn remove(&mut self, sub_key: SubKey) { + match self { + SubTo::Zero => (), + SubTo::One(key) if *key == sub_key => *self = SubTo::Zero, + SubTo::Two(key1, key2) if *key1 == sub_key => *self = SubTo::One(*key2), + SubTo::Two(key1, key2) if *key2 == sub_key => *self = SubTo::One(*key1), + SubTo::Three(key1, key2, key3) => { + if sub_key == *key1 { + *self = SubTo::Two(*key2, *key3); + return; + } + + if sub_key == *key2 { + *self = SubTo::Two(*key1, *key3); + return; + } + + if sub_key == *key3 { + *self = SubTo::Two(*key1, *key2); + } + } + SubTo::Four(key1, key2, key3, key4) => { + if sub_key == *key1 { + *self = SubTo::Three(*key2, *key3, *key4); + return; + } + + if sub_key == *key2 { + *self = SubTo::Three(*key1, *key3, *key4); + return; + } + + if sub_key == *key3 { + *self = SubTo::Three(*key1, *key2, *key4); + return; + } + + if sub_key == *key4 { + *self = SubTo::Three(*key1, *key2, *key3); + } + } + SubTo::Many(vec) => vec.retain(|key| *key != sub_key), + _ => {} + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/anathema-state/src/store/values.rs b/anathema-state/src/store/values.rs index d57f3068..8e5c9540 100644 --- a/anathema-state/src/store/values.rs +++ b/anathema-state/src/store/values.rs @@ -130,20 +130,3 @@ pub(crate) fn drop_value(key: ValueKey) -> OwnedValue { value } - -// /// This should only be used for debugging. -// pub fn dump_state() -> String { -// use std::fmt::Write; -// let mut string = String::new(); -// let _ = writeln!( -// &mut string, -// "\n\n=== SHARED ===\n{}\n", -// SHARED.with(|s| s.dump_state()) -// ); -// let _ = writeln!( -// &mut string, -// "=== OWNED ===\n{}\n", -// OWNED.with(|s| s.dump_state()) -// ); -// string -// } diff --git a/anathema-store/src/tree/view.rs b/anathema-store/src/tree/view.rs index 91d7a7c0..5d081e8d 100644 --- a/anathema-store/src/tree/view.rs +++ b/anathema-store/src/tree/view.rs @@ -93,8 +93,8 @@ impl<'tree, T> TreeView<'tree, T> { self.path_ref(id).into() } - // Find the value id by the path - fn id(&self, path: &[u16]) -> Option { + /// Find the value id by the path + pub fn id(&self, path: &[u16]) -> Option { self.layout.with(path, |nodes| nodes.value()) } diff --git a/anathema-templates/src/document.rs b/anathema-templates/src/document.rs index 200ed395..d5ff8698 100644 --- a/anathema-templates/src/document.rs +++ b/anathema-templates/src/document.rs @@ -21,7 +21,6 @@ use crate::{ComponentBlueprintId, Lexer, Variables}; pub struct Document { template: TemplateSource, pub strings: Strings, - globals: Variables, components: ComponentTemplates, pub hot_reload: bool, } @@ -32,7 +31,6 @@ impl Document { Self { template, strings: Strings::new(), - globals: Variables::default(), components: ComponentTemplates::new(), hot_reload: true, } @@ -58,9 +56,7 @@ impl Document { Ok(id) } - pub fn compile(&mut self) -> Result<(Blueprint, Variables)> { - self.globals = Variables::default(); - + pub fn compile(&mut self, globals: &mut Variables) -> Result { let tokens = Lexer::new(&self.template, &mut self.strings).collect::>>()?; let tokens = Tokens::new(tokens, self.template.len()); let parser = Parser::new(tokens, &mut self.strings, &self.template, &mut self.components); @@ -69,7 +65,7 @@ impl Document { let mut context = Context { template: &self.template, - variables: &mut self.globals, + variables: globals, strings: &mut self.strings, components: &mut self.components, slots: SmallMap::empty(), @@ -79,7 +75,7 @@ impl Document { let mut blueprints = Scope::new(statements).eval(&mut context)?; match blueprints.is_empty() { true => Err(Error::no_template(ErrorKind::EmptyTemplate)), - false => Ok((blueprints.remove(0), self.globals.take())), + false => Ok(blueprints.remove(0)), } } diff --git a/anathema-templates/src/error/mod.rs b/anathema-templates/src/error/mod.rs index 7ed079c5..7f025ccd 100644 --- a/anathema-templates/src/error/mod.rs +++ b/anathema-templates/src/error/mod.rs @@ -58,7 +58,7 @@ pub enum ErrorKind { } impl ErrorKind { - pub(crate) fn to_error(self, template: Option) -> Error { + pub fn to_error(self, template: Option) -> Error { Error::new(template, self) } } diff --git a/anathema-templates/src/expressions/mod.rs b/anathema-templates/src/expressions/mod.rs index 25d82faf..83de162e 100644 --- a/anathema-templates/src/expressions/mod.rs +++ b/anathema-templates/src/expressions/mod.rs @@ -97,6 +97,33 @@ impl> From for Expression { } } +impl> From> for Expression { + fn from(map: HashMap) -> Self { + let map = map.into_iter().map(|(key, val)| (key, val.into())).collect(); + Self::Map(map) + } +} + +impl> From> for Expression { + fn from(list: Vec) -> Self { + let inner = list.into_iter().map(|i| i.into()).collect(); + Self::List(inner) + } +} + +impl> From<[T; N]> for Expression { + fn from(list: [T; N]) -> Self { + let inner = list.map(|i| i.into()); + Self::List(inner.to_vec()) + } +} + +impl From for Expression { + fn from(value: String) -> Self { + Self::Str(value) + } +} + impl From<&str> for Expression { fn from(value: &str) -> Self { Self::Str(value.into()) diff --git a/anathema-templates/src/statements/eval.rs b/anathema-templates/src/statements/eval.rs index 6227399b..c4852626 100644 --- a/anathema-templates/src/statements/eval.rs +++ b/anathema-templates/src/statements/eval.rs @@ -261,12 +261,12 @@ impl Scope { mod test { use super::*; use crate::document::Document; - use crate::{ToSourceKind, single}; + use crate::{ToSourceKind, Variables, single}; #[test] fn eval_node() { let mut doc = Document::new("node"); - let (bp, _) = doc.compile().unwrap(); + let bp = doc.compile(&mut Variables::new()).unwrap(); assert_eq!(bp, single!("node")); } @@ -277,7 +277,7 @@ mod test { b "; let mut doc = Document::new(src); - let (blueprint, _) = doc.compile().unwrap(); + let blueprint = doc.compile(&mut Variables::new()).unwrap(); assert_eq!(blueprint, single!(children @ "a", vec![single!("b")])); } @@ -289,7 +289,7 @@ mod test { "; let mut doc = Document::new(src); - let (blueprint, _) = doc.compile().unwrap(); + let blueprint = doc.compile(&mut Variables::new()).unwrap(); assert!(matches!(blueprint, Blueprint::Single(Single { value: Some(_), .. }))); } @@ -298,7 +298,7 @@ mod test { let src = "let state = 1"; let mut doc = Document::new(src); - let response = doc.compile(); + let response = doc.compile(&mut Variables::new()); assert_eq!( response.err().unwrap().to_string(), "invalid statement: state is a reserved identifier" @@ -313,7 +313,7 @@ mod test { "; let mut doc = Document::new(src); - let response = doc.compile(); + let response = doc.compile(&mut Variables::new()); assert_eq!( response.err().unwrap().to_string(), "invalid statement: state is a reserved identifier" @@ -327,7 +327,7 @@ mod test { node "; let mut doc = Document::new(src); - let (blueprint, _) = doc.compile().unwrap(); + let blueprint = doc.compile(&mut Variables::new()).unwrap(); assert!(matches!(blueprint, Blueprint::For(For { .. }))); } @@ -341,7 +341,7 @@ mod test { "; let mut doc = Document::new(src); - let (blueprint, _) = doc.compile().unwrap(); + let blueprint = doc.compile(&mut Variables::new()).unwrap(); let Blueprint::ControlFlow(controlflow) = blueprint else { panic!() }; assert!(matches!(controlflow.elses[0], Else { .. })); assert!(!controlflow.elses.is_empty()); @@ -356,7 +356,7 @@ mod test { "; let mut doc = Document::new(src); - let (blueprint, _) = doc.compile().unwrap(); + let blueprint = doc.compile(&mut Variables::new()).unwrap(); let Blueprint::ControlFlow(controlflow) = blueprint else { panic!() }; assert!(matches!(controlflow.elses[0], Else { .. })); assert!(!controlflow.elses.is_empty()); @@ -374,7 +374,7 @@ mod test { "; let mut doc = Document::new(src); - let (blueprint, _) = doc.compile().unwrap(); + let blueprint = doc.compile(&mut Variables::new()).unwrap(); let Blueprint::ControlFlow(controlflow) = blueprint else { panic!() }; assert!(matches!(controlflow.elses[0], Else { .. })); assert!(!controlflow.elses.is_empty()); @@ -387,7 +387,7 @@ mod test { let mut doc = Document::new(src); doc.add_component("comp", comp_src.to_template()).unwrap(); - let (blueprint, _) = doc.compile().unwrap(); + let blueprint = doc.compile(&mut Variables::new()).unwrap(); assert!(matches!(blueprint, Blueprint::Component(Component { .. }))); } @@ -412,7 +412,7 @@ mod test { let mut doc = Document::new(src); doc.add_component("comp", comp_src.to_template()).unwrap(); - let (blueprint, _) = doc.compile().unwrap(); + let blueprint = doc.compile(&mut Variables::new()).unwrap(); assert!(matches!(blueprint, Blueprint::Component(Component { .. }))); } @@ -426,7 +426,7 @@ mod test { let mut doc = Document::new(src); doc.add_component("comp", "node a".to_template()).unwrap(); - let _ = doc.compile().unwrap(); + let _ = doc.compile(&mut Variables::new()).unwrap(); } #[test] @@ -437,7 +437,7 @@ mod test { let mut doc = Document::new(src); doc.add_component("comp", "node a".to_template()).unwrap(); - let _ = doc.compile().unwrap(); + let _ = doc.compile(&mut Variables::new()).unwrap(); } #[test] @@ -449,7 +449,7 @@ mod test { let mut doc = Document::new(src); doc.add_component("comp", "node a".to_template()).unwrap(); - let (blueprint, _) = doc.compile().unwrap(); + let blueprint = doc.compile(&mut Variables::new()).unwrap(); assert!(matches!(blueprint, Blueprint::With(With { .. }))); } } diff --git a/anathema-templates/src/variables.rs b/anathema-templates/src/variables.rs index 7448abd6..1ec7afe8 100644 --- a/anathema-templates/src/variables.rs +++ b/anathema-templates/src/variables.rs @@ -7,7 +7,7 @@ use crate::error::ErrorKind; use crate::expressions::Expression; #[derive(Debug, Default, Clone)] -pub(crate) struct Globals(HashMap); +pub struct Globals(HashMap); impl Globals { pub fn empty() -> Self { @@ -22,7 +22,7 @@ impl Globals { self.0.get(ident) } - pub(crate) fn set(&mut self, ident: String, value: Expression) { + pub fn set(&mut self, ident: String, value: Expression) { if self.0.contains_key(&ident) { return; } @@ -229,6 +229,12 @@ pub struct Variables { impl Default for Variables { fn default() -> Self { + Self::new() + } +} + +impl Variables { + pub fn new() -> Self { let root = RootScope::default(); Self { globals: Globals::empty(), @@ -239,16 +245,6 @@ impl Default for Variables { declarations: Declarations::new(), } } -} - -impl Variables { - pub fn new() -> Self { - Self::default() - } - - pub fn take(&mut self) -> Self { - std::mem::take(self) - } fn declare_at(&mut self, ident: impl Into, var_id: VarId, id: ScopeId) -> VarId { let ident = ident.into(); @@ -262,8 +258,7 @@ impl Variables { return Err(ErrorKind::GlobalAlreadyAssigned(ident)); } - let value = value.into(); - self.globals.set(ident, value); + self.globals.set(ident, value.into()); Ok(()) } diff --git a/anathema-testutils/Cargo.toml b/anathema-testutils/Cargo.toml index 7a1e6a8c..e4ee4795 100644 --- a/anathema-testutils/Cargo.toml +++ b/anathema-testutils/Cargo.toml @@ -9,7 +9,7 @@ homepage = "https://github.com/togglebyte/anathema" repository = "https://github.com/togglebyte/anathema" [dependencies] -anathema = { path = "../", version = "0.2.11" } +anathema = { path = "../", version = "0.2.12-beta" } [lints] workspace = true diff --git a/anathema-value-resolver/Cargo.toml b/anathema-value-resolver/Cargo.toml index 7fdedb27..5fa45266 100644 --- a/anathema-value-resolver/Cargo.toml +++ b/anathema-value-resolver/Cargo.toml @@ -13,6 +13,12 @@ anathema-state = { workspace = true } anathema-templates = { workspace = true } anathema-store = { workspace = true } unicode-width = { workspace = true } +puffin = { version = "0.19.1", features = ["web"], optional = true } +puffin_http = { version = "0.16.1", optional = true } + +[features] +default = [] +profile = ["puffin", "puffin_http"] [lints] workspace = true diff --git a/anathema-value-resolver/src/attributes.rs b/anathema-value-resolver/src/attributes.rs index 56d46ac5..8441a520 100644 --- a/anathema-value-resolver/src/attributes.rs +++ b/anathema-value-resolver/src/attributes.rs @@ -120,17 +120,38 @@ impl<'bp> Attributes<'bp> { /// ``` pub fn set(&mut self, key: &'bp str, value: impl Into>) { let key = ValueKey::Attribute(key); - let value = value.into(); - let value = Value { - expr: ValueExpr::Null, - kind: value, - sub: anathema_state::Subscriber::MAX, - sub_to: anathema_state::SubTo::Zero, - }; - + let value = Value::static_val(value); self.attribs.set(key, value); } + /// Set an attribute value. + /// ``` + /// # use anathema_value_resolver::{Attributes, ValueKind}; + /// + /// let mut attributes = Attributes::empty(); + /// attributes.set_value("Nonsense"); + /// attributes.value_as::<&str>().unwrap(); + /// ``` + pub fn set_value(&mut self, value: impl Into>) { + let value = Value::static_val(value); + + match self.value { + Some(index) => match self.attribs.get_mut_with_index(index) { + Some(current) => *current = value, + None => { + let index = self.attribs.insert_with(ValueKey::Value, |_| value); + self.value = Some(index); + } + }, + None => { + let index = self.attribs.insert_with(ValueKey::Value, |_| value); + self.value = Some(index); + } + } + } + + // This is only used for inserting values during widget creation where values originate from + // expressions. #[doc(hidden)] pub fn insert_with(&mut self, key: ValueKey<'bp>, f: F) -> SmallIndex where @@ -152,6 +173,17 @@ impl<'bp> Attributes<'bp> { self.attribs.get_with_index(idx).map(|val| &val.kind) } + /// Get a value as a specific type + pub fn value_as<'a, T>(&'a self) -> Option + where + T: TryFrom<&'a ValueKind<'bp>>, + { + let idx = self.value?; + self.attribs + .get_with_index(idx) + .and_then(|val| (&val.kind).try_into().ok()) + } + pub fn get(&self, key: &str) -> Option<&ValueKind<'bp>> { self.attribs.get(key).map(|val| &val.kind) } @@ -246,6 +278,7 @@ mod test { attributes.set("float", 1.23); attributes.set("bool", true); attributes.set("char", 'a'); + attributes.set_value(123u8); attributes } @@ -312,4 +345,11 @@ mod test { let attributes = attribs(); assert_eq!('a', attributes.get_as::("char").unwrap()); } + + #[test] + fn get_value_as() { + let attributes = attribs(); + let value = attributes.value_as::().unwrap(); + assert_eq!(value, 123); + } } diff --git a/anathema-value-resolver/src/expression.rs b/anathema-value-resolver/src/expression.rs index 77a0d342..49c78a15 100644 --- a/anathema-value-resolver/src/expression.rs +++ b/anathema-value-resolver/src/expression.rs @@ -19,18 +19,72 @@ macro_rules! or_null { }; } +#[derive(Debug, Default, Copy, Clone)] +pub(crate) enum ResolvedState { + Resolved, + PartiallyResolved, + #[default] + Unresolved, +} + pub struct ValueResolutionContext<'a, 'bp> { pub(crate) sub: Subscriber, - pub(crate) sub_to: &'a mut SubTo, attribute_storage: &'a AttributeStorage<'bp>, + pub(crate) resolved_state: ResolvedState, + pub(crate) sub_keys: SubTo, } impl<'a, 'bp> ValueResolutionContext<'a, 'bp> { - pub fn new(attribute_storage: &'a AttributeStorage<'bp>, sub: Subscriber, sub_to: &'a mut SubTo) -> Self { + pub fn new(attribute_storage: &'a AttributeStorage<'bp>, sub: Subscriber, resolved_state: ResolvedState) -> Self { Self { attribute_storage, sub, - sub_to, + resolved_state, + sub_keys: SubTo::empty(), + } + } + + pub(crate) fn force_sub(&mut self, pending: &PendingValue) { + pending.subscribe(self.sub); + self.sub_keys.push(pending.sub_key()); + } + + fn maybe_subscribe(&mut self, pending: &PendingValue) { + match self.resolved_state { + ResolvedState::Resolved => (), + ResolvedState::PartiallyResolved => (), + ResolvedState::Unresolved => { + pending.subscribe(self.sub); + self.sub_keys.push(pending.sub_key()); + } + } + } + + fn resolved(&mut self, pending: &PendingValue) { + match self.resolved_state { + ResolvedState::Resolved => (), + ResolvedState::PartiallyResolved | ResolvedState::Unresolved => pending.subscribe(self.sub), + } + + self.resolved_state = ResolvedState::Resolved; + } + + fn partially_resolved(&mut self, pending: &PendingValue) { + self.resolved_state = ResolvedState::PartiallyResolved; + self.sub_keys.push(pending.sub_key()); + pending.subscribe(self.sub); + } + + fn is_partially_resolved(&self) -> bool { + match self.resolved_state { + ResolvedState::PartiallyResolved => true, + ResolvedState::Resolved | ResolvedState::Unresolved => false, + } + } + + pub(crate) fn done(&mut self) { + if let ResolvedState::Unresolved = self.resolved_state { + self.resolved_state = ResolvedState::Resolved; } } } @@ -120,8 +174,7 @@ pub(crate) fn resolve_value<'a, 'bp>( // ----------------------------------------------------------------------------- ValueExpr::Bool(Kind::Static(b)) => ValueKind::Bool(*b), ValueExpr::Bool(Kind::Dyn(pending)) => { - pending.subscribe(ctx.sub); - ctx.sub_to.push(pending.sub_key()); + ctx.maybe_subscribe(pending); let Some(state) = pending.as_state() else { return ValueKind::Null }; match state.as_bool() { Some(b) => ValueKind::Bool(b), @@ -130,8 +183,7 @@ pub(crate) fn resolve_value<'a, 'bp>( } ValueExpr::Char(Kind::Static(c)) => ValueKind::Char(*c), ValueExpr::Char(Kind::Dyn(pending)) => { - pending.subscribe(ctx.sub); - ctx.sub_to.push(pending.sub_key()); + ctx.maybe_subscribe(pending); let Some(state) = pending.as_state() else { return ValueKind::Null }; match state.as_char() { Some(c) => ValueKind::Char(c), @@ -140,8 +192,7 @@ pub(crate) fn resolve_value<'a, 'bp>( } ValueExpr::Int(Kind::Static(i)) => ValueKind::Int(*i), ValueExpr::Int(Kind::Dyn(pending)) => { - pending.subscribe(ctx.sub); - ctx.sub_to.push(pending.sub_key()); + ctx.maybe_subscribe(pending); let Some(state) = pending.as_state() else { return ValueKind::Null }; match state.as_int() { Some(i) => ValueKind::Int(i), @@ -150,8 +201,7 @@ pub(crate) fn resolve_value<'a, 'bp>( } ValueExpr::Float(Kind::Static(f)) => ValueKind::Float(*f), ValueExpr::Float(Kind::Dyn(pending)) => { - pending.subscribe(ctx.sub); - ctx.sub_to.push(pending.sub_key()); + ctx.maybe_subscribe(pending); let Some(state) = pending.as_state() else { return ValueKind::Null }; match state.as_float() { Some(f) => ValueKind::Float(f), @@ -160,8 +210,7 @@ pub(crate) fn resolve_value<'a, 'bp>( } ValueExpr::Hex(Kind::Static(h)) => ValueKind::Hex(*h), ValueExpr::Hex(Kind::Dyn(pending)) => { - pending.subscribe(ctx.sub); - ctx.sub_to.push(pending.sub_key()); + ctx.maybe_subscribe(pending); let Some(state) = pending.as_state() else { return ValueKind::Null }; match state.as_hex() { Some(h) => ValueKind::Hex(h), @@ -170,8 +219,7 @@ pub(crate) fn resolve_value<'a, 'bp>( } ValueExpr::Color(Kind::Static(h)) => ValueKind::Color(*h), ValueExpr::Color(Kind::Dyn(pending)) => { - pending.subscribe(ctx.sub); - ctx.sub_to.push(pending.sub_key()); + ctx.maybe_subscribe(pending); let Some(state) = pending.as_state() else { return ValueKind::Null }; match state.as_color() { Some(h) => ValueKind::Color(h), @@ -180,8 +228,7 @@ pub(crate) fn resolve_value<'a, 'bp>( } ValueExpr::Str(Kind::Static(s)) => ValueKind::Str(Cow::Borrowed(s)), ValueExpr::Str(Kind::Dyn(pending)) => { - pending.subscribe(ctx.sub); - ctx.sub_to.push(pending.sub_key()); + ctx.maybe_subscribe(pending); let Some(state) = pending.as_state() else { return ValueKind::Null }; match state.as_str() { Some(s) => ValueKind::Str(Cow::Owned(s.to_owned())), @@ -257,8 +304,7 @@ pub(crate) fn resolve_value<'a, 'bp>( ValueExpr::DynMap(map) => ValueKind::DynMap(*map), ValueExpr::Attributes(_) => ValueKind::Attributes, ValueExpr::DynList(value) => { - value.subscribe(ctx.sub); - ctx.sub_to.push(value.sub_key()); + ctx.maybe_subscribe(value); ValueKind::DynList(*value) } ValueExpr::List(l) => { @@ -305,8 +351,7 @@ fn resolve_pending<'bp>(val: PendingValue, ctx: &mut ValueResolutionContext<'_, let inner = match maybe.get() { Some(inner) => inner, None => { - val.subscribe(ctx.sub); - ctx.sub_to.push(val.sub_key()); + ctx.maybe_subscribe(&val); return ValueExpr::Null; } }; @@ -325,41 +370,35 @@ fn resolve_index<'bp>( let state = or_null!(value.as_state()); let map = match state.as_any_map() { Some(map) => map, - None => { - // This will happen in the event of an `Option` - // where the `Option` is `None` - value.subscribe(ctx.sub); - ctx.sub_to.push(value.sub_key()); - return ValueExpr::Null; - } + None => return ValueExpr::Null, }; let key = or_null!(resolve_str(index, ctx)); let val = map.lookup(&key); let val = match val { - Some(key) => key, + Some(key) => { + if ctx.is_partially_resolved() { + value.unsubscribe(ctx.sub); + } + key + } None => { - value.subscribe(ctx.sub); - ctx.sub_to.push(value.sub_key()); + ctx.partially_resolved(value); return ValueExpr::Null; } }; + ctx.resolved(&val); resolve_pending(val, ctx) } ValueExpr::DynList(value) => { - value.subscribe(ctx.sub); - ctx.sub_to.push(value.sub_key()); let state = or_null!(value.as_state()); let list = match state.as_any_list() { Some(list) => list, - None => { - value.subscribe(ctx.sub); - ctx.sub_to.push(value.sub_key()); - return ValueExpr::Null; - } + None => return ValueExpr::Null, }; + let index = or_null!(resolve_int(index, ctx)); let val = list.lookup(index); @@ -368,10 +407,14 @@ fn resolve_index<'bp>( // // If the value does exist unsubscribe from the underlying map / state let val = match val { - Some(val) => val, + Some(val) => { + if ctx.is_partially_resolved() { + value.unsubscribe(ctx.sub); + } + val + } None => { - value.subscribe(ctx.sub); - ctx.sub_to.push(value.sub_key()); + ctx.partially_resolved(value); return ValueExpr::Null; } }; @@ -516,9 +559,10 @@ mod test { #[test] fn list_preceding_value_removed() { let mut changes = Changes::empty(); - let mut states = States::new(); + setup(&mut states, Default::default(), |test| { + // state.list[1] let expr = index(index(ident("state"), strlit("list")), num(1)); test.with_state(|state| { diff --git a/anathema-value-resolver/src/functions/list.rs b/anathema-value-resolver/src/functions/list.rs index df7070f5..f74cd89d 100644 --- a/anathema-value-resolver/src/functions/list.rs +++ b/anathema-value-resolver/src/functions/list.rs @@ -31,6 +31,28 @@ pub(super) fn contains<'bp>(args: &[ValueKind<'bp>]) -> ValueKind<'bp> { } } +pub(super) fn len<'bp>(args: &[ValueKind<'bp>]) -> ValueKind<'bp> { + if args.len() != 1 { + return ValueKind::Null; + } + + let len = match &args[0] { + ValueKind::Str(string) => string.len(), + ValueKind::List(list) => list.len(), + ValueKind::Range(from, to) => to - from, + ValueKind::DynList(pending) => match pending.as_state() { + None => return ValueKind::Null, + Some(state) => match state.as_any_list() { + None => return ValueKind::Null, + Some(list) => list.len(), + }, + }, + _ => return ValueKind::Null, + }; + + ValueKind::Int(len as i64) +} + #[cfg(test)] mod test { use anathema_state::{List, Value}; @@ -84,4 +106,38 @@ mod test { let result = contains(&args); assert!(matches!(result, ValueKind::Bool(true))); } + + #[test] + fn list_len() { + let list = value(vec![1, 2, 3]); + let args = [list]; + let result = len(&args); + assert!(matches!(result, ValueKind::Int(3))); + } + + #[test] + fn dyn_list_len() { + let list = List::from_iter([1, 2, 3]); + let list = Value::new(list); + let list = ValueKind::DynList(list.reference()); + let args = [list]; + let result = len(&args); + assert!(matches!(result, ValueKind::Int(3))); + } + + #[test] + fn string_len() { + let value = value("hello"); + let args = [value]; + let result = len(&args); + assert!(matches!(result, ValueKind::Int(5))); + } + + #[test] + fn int_len() { + let value = value(123); + let args = [value]; + let result = len(&args); + assert!(matches!(result, ValueKind::Null)); + } } diff --git a/anathema-value-resolver/src/functions/mod.rs b/anathema-value-resolver/src/functions/mod.rs index edb0c74a..34c0bbbc 100644 --- a/anathema-value-resolver/src/functions/mod.rs +++ b/anathema-value-resolver/src/functions/mod.rs @@ -65,6 +65,7 @@ impl FunctionTable { inner.insert("to_float".into(), Function::from(number::to_float)); inner.insert("round".into(), Function::from(number::round)); inner.insert("contains".into(), Function::from(list::contains)); + inner.insert("len".into(), Function::from(list::len)); Self { inner } } diff --git a/anathema-value-resolver/src/immediate.rs b/anathema-value-resolver/src/immediate.rs index 234975dd..fb25d450 100644 --- a/anathema-value-resolver/src/immediate.rs +++ b/anathema-value-resolver/src/immediate.rs @@ -21,6 +21,7 @@ impl<'a, 'frame, 'bp> Resolver<'a, 'frame, 'bp> { value.into() } "attributes" => { + // TODO: unwrap? Why is this okay? let component = self.ctx.scope.get_attributes().unwrap(); ValueExpr::Attributes(component) } diff --git a/anathema-value-resolver/src/scope.rs b/anathema-value-resolver/src/scope.rs index 041bb79c..1f4db87c 100644 --- a/anathema-value-resolver/src/scope.rs +++ b/anathema-value-resolver/src/scope.rs @@ -118,7 +118,6 @@ impl<'parent, 'bp> Scope<'parent, 'bp> { } &ValueKind::Range(from, to) => (from..to) .skip(index) - .take(1) .map(|num| ValueExpr::Int(Kind::Static(num as i64))) .next(), _ => unreachable!("none of the other values can be a collection"), diff --git a/anathema-value-resolver/src/value.rs b/anathema-value-resolver/src/value.rs index a45d7742..abe26af3 100644 --- a/anathema-value-resolver/src/value.rs +++ b/anathema-value-resolver/src/value.rs @@ -6,7 +6,7 @@ use anathema_store::smallmap::SmallMap; use anathema_templates::Expression; use crate::attributes::ValueKey; -use crate::expression::{ValueExpr, ValueResolutionContext, resolve_value}; +use crate::expression::{ResolvedState, ValueExpr, ValueResolutionContext, resolve_value}; use crate::immediate::Resolver; use crate::{AttributeStorage, ResolverCtx}; @@ -15,7 +15,7 @@ pub type Values<'bp> = SmallMap, Value<'bp>>; pub fn resolve<'bp>(expr: &'bp Expression, ctx: &ResolverCtx<'_, 'bp>, sub: impl Into) -> Value<'bp> { let resolver = Resolver::new(ctx); let value_expr = resolver.resolve(expr); - Value::new(value_expr, sub.into(), ctx.attribute_storage) + Value::resolve(value_expr, sub.into(), ctx.attribute_storage) } pub fn resolve_collection<'bp>( @@ -67,13 +67,23 @@ pub struct Value<'bp> { pub(crate) expr: ValueExpr<'bp>, pub(crate) sub: Subscriber, pub(crate) kind: ValueKind<'bp>, - pub(crate) sub_to: SubTo, + pub(crate) resolved: ResolvedState, + sub_keys: SubTo, } impl<'bp> Value<'bp> { - pub fn new(expr: ValueExpr<'bp>, sub: Subscriber, attribute_storage: &AttributeStorage<'bp>) -> Self { - let mut sub_to = SubTo::Zero; - let mut ctx = ValueResolutionContext::new(attribute_storage, sub, &mut sub_to); + pub(crate) fn static_val(value: impl Into>) -> Self { + Self { + expr: ValueExpr::Null, + kind: value.into(), + sub: anathema_state::Subscriber::MAX, + resolved: ResolvedState::Resolved, + sub_keys: SubTo::empty(), + } + } + + pub fn resolve(expr: ValueExpr<'bp>, sub: Subscriber, attribute_storage: &AttributeStorage<'bp>) -> Self { + let mut ctx = ValueResolutionContext::new(attribute_storage, sub, ResolvedState::Unresolved); let kind = resolve_value(&expr, &mut ctx); // NOTE @@ -87,28 +97,32 @@ impl<'bp> Value<'bp> { // ``` match kind { ValueKind::DynMap(pending) | ValueKind::Composite(pending) => { - pending.subscribe(ctx.sub); - ctx.sub_to.push(pending.sub_key()); + ctx.force_sub(&pending); } _ => {} } + + ctx.done(); Self { expr, sub, kind, - sub_to, + resolved: ctx.resolved_state, + sub_keys: ctx.sub_keys, } } + /// ```text + /// None = false + /// 0 = false + /// Some("") = false + /// Some(0) = false + /// [] = false + /// {} = false + /// Some(bool) = bool + /// _ = true + /// ``` pub fn truthiness(&self) -> bool { - // None = false - // 0 = false - // Some("") = false - // Some(0) = false - // [] = false - // {} = false - // Some(bool) = bool - // _ = true self.kind.truthiness() } @@ -118,9 +132,15 @@ impl<'bp> Value<'bp> { } pub fn reload(&mut self, attribute_storage: &AttributeStorage<'bp>) { - self.sub_to.unsubscribe(self.sub); - let mut ctx = ValueResolutionContext::new(attribute_storage, self.sub, &mut self.sub_to); - self.kind = resolve_value(&self.expr, &mut ctx); + #[cfg(feature = "profile")] + puffin::profile_function!(); + + let mut ctx = ValueResolutionContext::new(attribute_storage, self.sub, self.resolved); + let kind = resolve_value(&self.expr, &mut ctx); + ctx.done(); + self.sub_keys.merge(ctx.sub_keys); + self.kind = kind; + self.resolved = ctx.resolved_state; } pub fn try_as(&self) -> Option @@ -140,7 +160,7 @@ impl<'bp> Value<'bp> { impl Drop for Value<'_> { fn drop(&mut self) { - self.sub_to.unsubscribe(self.sub); + self.sub_keys.unsubscribe(self.sub); } } @@ -285,7 +305,6 @@ impl ValueKind<'_> { let Some(state) = state.as_any_map() else { return false }; !state.is_empty() } - // ValueKind::Map => ??, _ => true, } } diff --git a/anathema-widgets/src/container.rs b/anathema-widgets/src/container.rs index 3d15d7b0..a101cf52 100644 --- a/anathema-widgets/src/container.rs +++ b/anathema-widgets/src/container.rs @@ -12,7 +12,7 @@ use crate::{LayoutForEach, PaintChildren, Style, WidgetId}; pub struct Cache { pub(super) size: Size, pub(super) pos: Option, - constraints: Option, + constraints: Constraints, pub(super) child_count: usize, valid: bool, } @@ -22,7 +22,7 @@ impl Cache { size: Size::ZERO, pos: None, // Constraints are `None` for the root node - constraints: None, + constraints: Constraints::ZERO, child_count: 0, valid: false, }; @@ -31,7 +31,7 @@ impl Cache { Self { size, pos: None, - constraints: Some(constraints), + constraints, child_count: 0, valid: true, } @@ -43,11 +43,6 @@ impl Cache { self.valid.then_some(self.size) } - pub(super) fn invalidate(&mut self) { - self.valid = false; - self.pos = None; - } - fn changed(&mut self, mut cache: Cache) -> bool { let changed = self.size != cache.size; cache.child_count = self.child_count; @@ -55,15 +50,9 @@ impl Cache { changed } - pub(crate) fn constraints(&self) -> Option { + pub fn constraints(&self) -> Constraints { self.constraints } - - pub(crate) fn count_check(&mut self, count: usize) -> bool { - let c = self.child_count; - self.child_count = count; - c != count - } } /// Wraps a widget and retain some geometry for the widget @@ -82,8 +71,6 @@ impl Container { constraints: Constraints, ctx: &mut LayoutCtx<'_, 'bp>, ) -> Result { - // NOTE: The layout is possibly skipped in the Element::layout call - let size = self.inner.any_layout(children, constraints, self.id, ctx)?; let cache = Cache::new(size, constraints); @@ -104,6 +91,7 @@ impl Container { }, false => Layout::Unchanged(self.cache.size), }; + Ok(layout) } diff --git a/anathema-widgets/src/layout/mod.rs b/anathema-widgets/src/layout/mod.rs index 3cbd15c9..70115479 100644 --- a/anathema-widgets/src/layout/mod.rs +++ b/anathema-widgets/src/layout/mod.rs @@ -67,7 +67,11 @@ impl<'frame, 'bp> LayoutCtx<'frame, 'bp> { self.attribute_storage.get(node_id) } - pub fn eval_ctx(&mut self, parent_component: Option) -> EvalCtx<'_, 'bp> { + pub fn eval_ctx( + &mut self, + parent_component: Option, + parent_element: Option, + ) -> EvalCtx<'_, 'bp> { EvalCtx { floating_widgets: self.floating_widgets, attribute_storage: self.attribute_storage, @@ -77,6 +81,7 @@ impl<'frame, 'bp> LayoutCtx<'frame, 'bp> { globals: self.globals, factory: self.factory, parent_component, + parent_widget: parent_element, new_components: &mut self.new_components, function_table: self.function_table, } @@ -114,6 +119,7 @@ pub struct EvalCtx<'frame, 'bp> { pub(super) factory: &'frame Factory, pub(super) function_table: &'bp FunctionTable, pub(super) parent_component: Option, + pub(super) parent_widget: Option, } impl<'frame, 'bp> EvalCtx<'frame, 'bp> { diff --git a/anathema-widgets/src/layout/text.rs b/anathema-widgets/src/layout/text.rs index 0b11f6f1..a0b81ce7 100644 --- a/anathema-widgets/src/layout/text.rs +++ b/anathema-widgets/src/layout/text.rs @@ -50,15 +50,15 @@ impl From for ValueKind<'_> { } #[derive(Debug)] -pub(crate) struct LineWidth(usize); +pub(crate) struct LineWidth(u16); impl LineWidth { pub(crate) const ZERO: Self = Self(0); // update the current value and return the old value - pub(crate) fn swap(&mut self, mut new_value: usize) -> u16 { + pub(crate) fn swap(&mut self, mut new_value: u16) -> u16 { std::mem::swap(&mut self.0, &mut new_value); - new_value as u16 + new_value } } @@ -69,15 +69,15 @@ impl Default for LineWidth { } impl Deref for LineWidth { - type Target = usize; + type Target = u16; fn deref(&self) -> &Self::Target { &self.0 } } -impl AddAssign for LineWidth { - fn add_assign(&mut self, rhs: usize) { +impl AddAssign for LineWidth { + fn add_assign(&mut self, rhs: u16) { self.0 += rhs; } } @@ -275,22 +275,22 @@ impl Strings { self.update_width(); self.line = match self.chomper { Chomper::Continuous(idx) => { - self.layout - .push((idx as u32, Entry::LineWidth(self.current_width.swap(0)))); - self.layout.push((idx as u32, Entry::Newline)); - idx + self.layout.push((idx, Entry::LineWidth(self.current_width.swap(0)))); + self.layout.push((idx, Entry::Newline)); + idx as usize } Chomper::WordBoundary { word_boundary, current_index, } => { - let diff = self.line(current_index).width() - self.line(word_boundary).width(); + let diff = + (self.line(current_index as usize).width() - self.line(word_boundary as usize).width()) as u16; let width = *self.current_width - diff; - self.layout.push((word_boundary as u32, Entry::LineWidth(width as u16))); - self.layout.push((word_boundary as u32, Entry::Newline)); + self.layout.push((word_boundary, Entry::LineWidth(width))); + self.layout.push((word_boundary, Entry::Newline)); let _ = self.current_width.swap(diff); self.chomper = Chomper::Continuous(current_index); - word_boundary + word_boundary as usize } }; } @@ -304,7 +304,7 @@ impl Strings { } fn update_width(&mut self) { - self.size.width = self.size.width.max(*self.current_width as u16); + self.size.width = self.size.width.max(*self.current_width); } fn chomp(&mut self, c: char) -> ProcessResult { @@ -336,7 +336,7 @@ impl Strings { // NOTE // If the trailing whitespace should be removed, do so here - while width + *self.current_width as u16 > self.max.width { + while width + *self.current_width > self.max.width { if c.is_whitespace() { // 1. Make this the next word boundary // 2. Insert a newline here @@ -360,7 +360,7 @@ impl Strings { } self.chomper.chomp(c, self.wrap); - self.current_width += width as usize; + self.current_width += width; ProcessResult::Continue } @@ -372,17 +372,16 @@ impl Default for Strings { } } -// TODO: move this into string2 #[derive(Debug)] pub(crate) enum Chomper { - Continuous(usize), - WordBoundary { word_boundary: usize, current_index: usize }, + Continuous(u32), + WordBoundary { word_boundary: u32, current_index: u32 }, } impl Chomper { pub(crate) fn index(&self) -> usize { match self { - Chomper::Continuous(current_index) | Chomper::WordBoundary { current_index, .. } => *current_index, + Chomper::Continuous(current_index) | Chomper::WordBoundary { current_index, .. } => *current_index as usize, } } @@ -397,7 +396,7 @@ impl Chomper { } pub(crate) fn chomp(&mut self, c: char, wrap: Wrap) { - let c_len = c.len_utf8(); + let c_len = c.len_utf8() as u32; if c.is_whitespace() && wrap.is_word_wrap() { match self { diff --git a/anathema-widgets/src/lib.rs b/anathema-widgets/src/lib.rs index e3b7283d..1aa280e7 100644 --- a/anathema-widgets/src/lib.rs +++ b/anathema-widgets/src/lib.rs @@ -1,11 +1,13 @@ use anathema_state::Subscriber; pub use crate::nodes::component::Component; +pub use crate::nodes::element::Layout; pub use crate::nodes::{Element, WidgetContainer, WidgetGenerator, WidgetKind, eval_blueprint, update_widget}; pub use crate::paint::{GlyphMap, WidgetRenderer}; pub use crate::widget::{ - AnyWidget, Attributes, ComponentParents, Components, Factory, FloatingWidgets, ForEach, LayoutChildren, - LayoutForEach, PaintChildren, PositionChildren, Style, Widget, WidgetId, WidgetTree, WidgetTreeView, + AnyWidget, Attributes, ComponentParents, Components, DirtyWidgets, Factory, FloatingWidgets, ForEach, + LayoutChildren, LayoutForEach, PaintChildren, PositionChildren, Style, Widget, WidgetId, WidgetTree, + WidgetTreeView, }; pub type ChangeList = anathema_store::regionlist::RegionList<32, WidgetId, Subscriber>; diff --git a/anathema-widgets/src/nodes/controlflow.rs b/anathema-widgets/src/nodes/controlflow.rs index d242132a..5408fa4f 100644 --- a/anathema-widgets/src/nodes/controlflow.rs +++ b/anathema-widgets/src/nodes/controlflow.rs @@ -4,6 +4,7 @@ use anathema_value_resolver::Value; use crate::layout::LayoutCtx; use crate::widget::WidgetTreeView; +use crate::{DirtyWidgets, WidgetId}; #[derive(Debug)] pub struct ControlFlow<'bp> { @@ -17,6 +18,8 @@ impl<'bp> ControlFlow<'bp> { branch_id: u16, mut tree: WidgetTreeView<'_, 'bp>, ctx: &mut LayoutCtx<'_, 'bp>, + parent_widget: Option, + dirty_widgets: &mut DirtyWidgets, ) { match change { Change::Changed | Change::Dropped => { @@ -26,6 +29,10 @@ impl<'bp> ControlFlow<'bp> { cond.reload(ctx.attribute_storage); if cond.truthiness() != current { ctx.truncate_children(&mut tree); + + if let Some(widget) = parent_widget { + dirty_widgets.push(widget); + } } } // TODO: diff --git a/anathema-widgets/src/nodes/element.rs b/anathema-widgets/src/nodes/element.rs index 1e79fee7..acf70223 100644 --- a/anathema-widgets/src/nodes/element.rs +++ b/anathema-widgets/src/nodes/element.rs @@ -1,5 +1,3 @@ -use std::ops::ControlFlow; - use anathema_geometry::{Pos, Region, Size}; use anathema_value_resolver::AttributeStorage; @@ -43,63 +41,18 @@ impl<'bp> Element<'bp> { pub fn layout( &mut self, - mut children: LayoutForEach<'_, 'bp>, + children: LayoutForEach<'_, 'bp>, constraints: Constraints, ctx: &mut LayoutCtx<'_, 'bp>, ) -> Result { - // 1. Check cache - // 2. Check cache of children - // - // If one of the children returns a `Changed` layout result - // the transition the widget into full layout mode - - let count = children.len(); - let mut rebuild = self.container.cache.count_check(count); - - if let Some(size) = self.cached_size() { - _ = children.each(ctx, |ctx, node, children| { - // If we are here it's because the current node has a valid cache. - // We need to use the constraint for the given node in this case as - // the constraint is not managed by the current node. - // - // Example: - // If the current node is a border with a fixed width and height, - // it would create a new constraint for the child node that is the - // width and height - the border size. - // - // However the border does not store this constraint, it's stored - // on the node itself. - // Therefore we pass the nodes its own constraint. - - let constraints = match node.container.cache.constraints() { - None => constraints, - Some(constraints) => constraints, - }; - - match node.layout(children, constraints, ctx)? { - Layout::Changed(_) => { - rebuild = true; - Ok(ControlFlow::Break(())) - } - Layout::Floating(_) | Layout::Unchanged(_) => Ok(ControlFlow::Continue(())), - } - })?; - - if !self.container.cache.count_check(count) { - rebuild = true; - } - - if !rebuild { - return Ok(Layout::Unchanged(size)); - } - } - + #[cfg(feature = "profile")] + puffin::profile_function!(); self.container.layout(children, constraints, ctx) } - pub fn invalidate_cache(&mut self) { - self.container.cache.invalidate(); - } + // pub fn invalidate_cache(&mut self) { + // self.container.cache.invalidate(); + // } /// Position the element pub fn position( @@ -139,7 +92,7 @@ impl<'bp> Element<'bp> { /// Bounds in global space pub fn bounds(&self) -> Region { - let pos = self.get_pos(); + let pos = self.pos(); let size = self.size(); Region::from((pos, size)) } @@ -173,6 +126,12 @@ impl<'bp> Element<'bp> { } /// Get the position of the container. + pub fn pos(&self) -> Pos { + self.container.cache.pos.unwrap_or(Pos::ZERO) + } + + /// Get the position of the container. + #[deprecated(note = "use `pos` instead")] pub fn get_pos(&self) -> Pos { self.container.cache.pos.unwrap_or(Pos::ZERO) } @@ -181,4 +140,8 @@ impl<'bp> Element<'bp> { pub(crate) fn is_floating(&self) -> bool { self.container.inner.any_floats() } + + pub fn constraints(&self) -> Constraints { + self.container.cache.constraints() + } } diff --git a/anathema-widgets/src/nodes/eval.rs b/anathema-widgets/src/nodes/eval.rs index 232e3c79..5a150e47 100644 --- a/anathema-widgets/src/nodes/eval.rs +++ b/anathema-widgets/src/nodes/eval.rs @@ -98,7 +98,7 @@ impl Evaluator for SingleEval { // Widget let widget = WidgetKind::Element(Element::new(&single.ident, container)); - let widget = WidgetContainer::new(widget, &single.children); + let widget = WidgetContainer::new(widget, &single.children, ctx.parent_widget); transaction .commit_child(widget) @@ -141,7 +141,7 @@ impl Evaluator for ForLoopEval { let body = for_loop.body; let widget = WidgetKind::For(for_loop); - let widget = WidgetContainer::new(widget, body); + let widget = WidgetContainer::new(widget, body, ctx.parent_widget); transaction .commit_child(widget) .ok_or_else(|| ctx.error(ErrorKind::TreeTransactionFailed))?; @@ -182,7 +182,7 @@ impl Evaluator for WithEval { let body = with.body; let widget = WidgetKind::With(with); - let widget = WidgetContainer::new(widget, body); + let widget = WidgetContainer::new(widget, body, ctx.parent_widget); transaction .commit_child(widget) .ok_or_else(|| ctx.error(ErrorKind::TreeTransactionFailed))?; @@ -231,7 +231,7 @@ impl Evaluator for ControlFlowEval { }) .collect(), }); - let widget = WidgetContainer::new(widget, &[]); + let widget = WidgetContainer::new(widget, &[], ctx.parent_widget); transaction .commit_child(widget) .ok_or_else(|| ctx.error(ErrorKind::TreeTransactionFailed))?; @@ -297,7 +297,7 @@ impl Evaluator for ComponentEval { ); let widget = WidgetKind::Component(comp_widget); - let widget = WidgetContainer::new(widget, &input.body); + let widget = WidgetContainer::new(widget, &input.body, ctx.parent_widget); let widget_id = transaction .commit_child(widget) .ok_or_else(|| ctx.error(ErrorKind::TreeTransactionFailed))?; @@ -323,7 +323,7 @@ impl Evaluator for SlotEval { tree: &mut WidgetTreeView<'_, 'bp>, ) -> Result<()> { let transaction = tree.insert(parent); - let widget = WidgetContainer::new(WidgetKind::Slot, input); + let widget = WidgetContainer::new(WidgetKind::Slot, input, ctx.parent_widget); transaction .commit_child(widget) .ok_or_else(|| ctx.error(ErrorKind::TreeTransactionFailed))?; diff --git a/anathema-widgets/src/nodes/loops.rs b/anathema-widgets/src/nodes/loops.rs index 1790af17..6e608cc7 100644 --- a/anathema-widgets/src/nodes/loops.rs +++ b/anathema-widgets/src/nodes/loops.rs @@ -6,6 +6,7 @@ use super::{WidgetContainer, WidgetKind}; use crate::error::{Error, Result}; use crate::layout::LayoutCtx; use crate::widget::WidgetTreeView; +use crate::{DirtyWidgets, WidgetId}; #[derive(Debug)] pub struct For<'bp> { @@ -24,6 +25,8 @@ impl<'bp> For<'bp> { change: &Change, mut tree: WidgetTreeView<'_, 'bp>, ctx: &mut LayoutCtx<'_, 'bp>, + parent_widget: Option, + dirty_widgets: &mut DirtyWidgets, ) -> Result<()> { match change { Change::Inserted(index) => { @@ -41,7 +44,7 @@ impl<'bp> For<'bp> { loop_index: anathema_state::Value::new(*index as i64), binding: self.binding, }); - let widget = WidgetContainer::new(widget, self.body); + let widget = WidgetContainer::new(widget, self.body, parent_widget); let _ = transaction.commit_at(widget).ok_or_else(Error::transaction_failed)?; for child in &tree.layout[*index as usize + 1..] { @@ -60,6 +63,10 @@ impl<'bp> For<'bp> { } } Change::Removed(index) => { + if let Some(widget) = parent_widget { + dirty_widgets.push(widget); + } + for child in &tree.layout[*index as usize + 1..] { let iter_widget = tree.values.get_mut(child.value()); let Some(( @@ -82,6 +89,9 @@ impl<'bp> For<'bp> { // then truncate the tree self.collection.reload(ctx.attribute_storage); ctx.truncate_children(&mut tree); + if let Some(widget) = parent_widget { + dirty_widgets.push(widget); + } } } diff --git a/anathema-widgets/src/nodes/mod.rs b/anathema-widgets/src/nodes/mod.rs index 3425613c..bdb16fec 100644 --- a/anathema-widgets/src/nodes/mod.rs +++ b/anathema-widgets/src/nodes/mod.rs @@ -5,6 +5,7 @@ use eval::SlotEval; pub use self::element::Element; use self::eval::{ComponentEval, ControlFlowEval, Evaluator, ForLoopEval, SingleEval}; pub use self::update::update_widget; +use crate::WidgetId; use crate::error::Result; use crate::layout::EvalCtx; use crate::nodes::eval::WithEval; @@ -46,13 +47,15 @@ pub enum WidgetKind<'bp> { pub struct WidgetContainer<'bp> { pub kind: WidgetKind<'bp>, pub(crate) children: &'bp [Blueprint], + pub parent_widget: Option, } impl<'bp> WidgetContainer<'bp> { - pub fn new(kind: WidgetKind<'bp>, blueprints: &'bp [Blueprint]) -> Self { + pub fn new(kind: WidgetKind<'bp>, blueprints: &'bp [Blueprint], parent_widget: Option) -> Self { Self { kind, children: blueprints, + parent_widget, } } } diff --git a/anathema-widgets/src/nodes/scope.rs b/anathema-widgets/src/nodes/scope.rs deleted file mode 100644 index 34b5967b..00000000 --- a/anathema-widgets/src/nodes/scope.rs +++ /dev/null @@ -1,319 +0,0 @@ -use std::fmt::{self, Debug, Write}; - -use anathema_debug::DebugWriter; -use anathema_state::{Path, PendingValue, StateId, States}; - -use crate::expressions::{Downgraded, EvalValue}; -use crate::values::{Collection, ValueId}; -use crate::WidgetId; - -#[derive(Debug)] -pub struct ScopeLookup<'bp> { - path: Path<'bp>, - id: ValueId, -} - -impl<'bp> ScopeLookup<'bp> { - /// Get and subscribe to a value - pub(crate) fn new(path: impl Into>, value_id: ValueId) -> Self { - Self { - path: path.into(), - id: value_id, - } - } -} - -#[derive(Default)] -enum Entry<'bp> { - /// Scope(size of previous scope) - Scope(usize), - Downgraded(Path<'bp>, Downgraded<'bp>), - Pending(Path<'bp>, PendingValue), - State(StateId), - ComponentAttributes(WidgetId), - /// This is marking the entry as free, and another entry can be written here. - /// This is not indicative of a missing value - #[default] - Empty, -} - -impl<'bp> Entry<'bp> { - fn get(&self, lookup: &ScopeLookup<'bp>) -> Option<&Self> { - match self { - Self::Downgraded(path, _) if *path == lookup.path => Some(self), - Self::Pending(path, _) if *path == lookup.path => Some(self), - Self::State(_) => Some(self), - _ => None, - } - } -} - -impl Debug for Entry<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Entry::Scope(scope) => f.debug_tuple("Scope").field(scope).finish(), - Entry::Pending(path, pending_value) => f.debug_tuple("Pending").field(path).field(pending_value).finish(), - Entry::Downgraded(path, value) => f.debug_tuple("Downgraded").field(path).field(value).finish(), - Entry::State(state) => f.debug_tuple("State").field(&state).finish(), - Entry::ComponentAttributes(component_id) => { - f.debug_tuple("ComponentAttributes").field(&component_id).finish() - } - Entry::Empty => f.debug_tuple("Empty").finish(), - } - } -} - -/// `Scope` should be created once and then re-used by the runtime -/// to avoid unnecessary allocations. -/// -/// The scope is recreated for the update path of the nodes. -#[derive(Debug, Default)] -pub struct Scope<'bp> { - storage: Vec>, - current_scope_size: usize, - storage_index: usize, - level: usize, -} - -impl<'bp> Scope<'bp> { - pub fn new() -> Self { - Self { - storage: vec![], - current_scope_size: 0, - storage_index: 0, - level: 0, - } - } - - pub fn len(&self) -> usize { - self.storage_index - } - - pub fn with_capacity(cap: usize) -> Self { - let mut storage = Vec::with_capacity(cap); - storage.fill_with(Default::default); - Self { - storage, - current_scope_size: 0, - storage_index: 0, - level: 0, - } - } - - /// Clear the storage by writing `Entry::Empty` over every - /// existing entry, and reset the storage index - pub fn clear(&mut self) { - self.storage[..self.storage_index].fill_with(Default::default); - self.storage_index = 0; - } - - fn insert_entry(&mut self, entry: Entry<'bp>) { - match self.storage_index == self.storage.len() { - true => self.storage.push(entry), - false => self.storage[self.storage_index] = entry, - } - self.current_scope_size += 1; - self.storage_index += 1; - } - - fn inner_get( - &self, - lookup: &ScopeLookup<'bp>, - offset: &mut Option, - _states: &States, - ) -> Option> { - let mut current_offset = offset.unwrap_or(self.storage.len()); - - loop { - let (new_offset, entry) = self.storage[..current_offset] - .iter() - .enumerate() - .rev() - .find_map(|(i, e)| e.get(lookup).map(|e| (i, e)))?; - - current_offset = new_offset; - *offset = Some(new_offset); - - match entry { - // Pending - Entry::Pending(_, pending) => break Some(EvalValue::Dyn(pending.to_value(lookup.id))), - - // Downgraded - Entry::Downgraded(_, downgrade) => break Some(downgrade.upgrade(lookup.id)), - - // State value - // &Entry::State(state_id) => { - // let state = states.get(state_id)?; - // if let Some(value) = state.state_get(lookup.path, lookup.id) { - // break Some(EvalValue::Dyn(value)); - // } - // } - _ => continue, - } - } - } - - // There is always a state for each component - // (if no explicit state is given a unit is assumed) - // - // TODO: This is not entirely correct given that the root template - // has no component, perhaps this should change so there is always - // a component in the root. - pub(crate) fn get_state(&self) -> EvalValue<'bp> { - self.storage - .iter() - .rev() - .find_map(|e| match e { - Entry::State(state) => Some(EvalValue::State(*state)), - _ => None, - }) - // Note that this `expect` is false until we force a root component - .expect("there should always be at least one state entry") - } - - pub(crate) fn get_component_attributes(&self) -> EvalValue<'bp> { - self.storage - .iter() - .rev() - .find_map(|e| match e { - Entry::ComponentAttributes(component_id) => Some(EvalValue::ComponentAttributes(*component_id)), - _ => None, - }) - // Note that this `expect` is false until we force a root component - .expect("there should always be at least one state entry") - } - - /// Get can never return an eval value that is downgraded or pending - pub(crate) fn get( - &self, - lookup: ScopeLookup<'bp>, - offset: &mut Option, - states: &States, - ) -> Option> { - self.inner_get(&lookup, offset, states) - } - - pub(crate) fn push(&mut self) { - self.insert_entry(Entry::Scope(self.current_scope_size)); - self.current_scope_size = 0; - self.level += 1; - } - - pub(crate) fn pop(&mut self) { - if self.storage_index == 0 { - return; - } - - let index = self.storage_index - 1 - self.current_scope_size; - let &Entry::Scope(size) = &self.storage[index] else { panic!() }; - self.storage[index..].fill_with(|| Entry::Empty); - self.storage_index = index; - self.current_scope_size = size; - self.level -= 1; - } - - // ----------------------------------------------------------------------------- - // - Scope values - - // ----------------------------------------------------------------------------- - - pub(crate) fn scope_pending(&mut self, key: &'bp str, iter_value: PendingValue) { - let entry = Entry::Pending(Path::from(key), iter_value); - self.insert_entry(entry); - } - - pub(crate) fn scope_component_attributes(&mut self, widget_id: WidgetId) { - let entry = Entry::ComponentAttributes(widget_id); - self.insert_entry(entry); - } - - pub(crate) fn scope_downgrade(&mut self, binding: &'bp str, downgrade: Downgraded<'bp>) { - let entry = Entry::Downgraded(Path::from(binding), downgrade); - self.insert_entry(entry); - } - - // ----------------------------------------------------------------------------- - // - Insert - - // ----------------------------------------------------------------------------- - - pub fn insert_state(&mut self, state_id: StateId) { - let entry = Entry::State(state_id); - self.insert_entry(entry); - } - - pub fn insert_collection(&mut self, binding: &'bp str, collection: &Collection<'bp>) { - panic!(); - // let entry = Entry::State(state_id); - // self.insert_entry(entry); - } - -} - -pub struct DebugScope<'a, 'b>(pub &'a Scope<'b>); - -impl DebugWriter for DebugScope<'_, '_> { - fn write(&mut self, output: &mut impl Write) -> std::fmt::Result { - for (i, entry) in self.0.storage.iter().enumerate() { - writeln!(output, "{i:02} {entry:?}")?; - } - Ok(()) - } -} - -// #[cfg(test)] -impl<'bp> Scope<'bp> { - pub fn debug(&self) -> String { - let mut s = String::new(); - - for (i, entry) in self.storage.iter().enumerate() { - s += &format!("{i:02} {entry:?}\n"); - } - - s += "-----------------------\n"; - s += &format!( - "current scope size: {} | level: {}\n", - self.current_scope_size, self.level - ); - - s - } -} - -#[cfg(test)] -mod test { - use anathema_state::{List, Map, Value}; - use anathema_templates::{Expression, Globals}; - - use super::*; - use crate::expressions::eval_collection; - use crate::AttributeStorage; - - #[test] - fn scope_collection() { - let mut map = Map::>>::empty(); - map.insert("list", Value::>::from_iter([1u8, 2, 3])); - - let states = States::new(); - let scope = Scope::new(); - let attributes = AttributeStorage::empty(); - let expr = Expression::Ident("list".into()); - let globals = Globals::new(Default::default()); - eval_collection(&expr, &globals, &scope, &states, &attributes, ValueId::ZERO); - - // let one = [Expression::Primitive(1i64.into())]; - - // scope.scope_pending("a", &one); - // scope.scope_downgraded("a", 0); - - // scope.push(); - // let two = [Expression::Primitive(2i64.into())]; - // scope.scope_static_collection("a", &two); // |_ a = 2i64 - // scope.scope_index_lookup("a", 0); // | - // scope.pop(); - - // let ScopeValue::Dyn(expr) = scope.get(ScopeLookup::lookup("a"), &mut None).unwrap() else { - // panic!() - // }; - // let val = eval(expr, &scope, Subscriber::ZERO); - // assert_eq!(1, val.load_number().unwrap().as_int()); - } -} diff --git a/anathema-widgets/src/nodes/update.rs b/anathema-widgets/src/nodes/update.rs index 7d20ff24..714723b1 100644 --- a/anathema-widgets/src/nodes/update.rs +++ b/anathema-widgets/src/nodes/update.rs @@ -1,10 +1,10 @@ use anathema_state::{Change, Subscriber}; use super::WidgetContainer; -use crate::WidgetKind; use crate::error::Result; use crate::layout::LayoutCtx; use crate::widget::WidgetTreeView; +use crate::{DirtyWidgets, WidgetKind}; pub fn update_widget<'bp>( widget: &mut WidgetContainer<'bp>, @@ -12,29 +12,31 @@ pub fn update_widget<'bp>( change: &Change, tree: WidgetTreeView<'_, 'bp>, ctx: &mut LayoutCtx<'_, 'bp>, + dirty_widgets: &mut DirtyWidgets, ) -> Result<()> { match &mut widget.kind { WidgetKind::Element(element) => { - ctx.attribute_storage - .with_mut(element.container.id, |attributes, storage| { - let Some(value) = attributes.get_mut_with_index(value_id.index()) else { return }; - value.reload(storage); - }); + ctx.attribute_storage.with_mut(element.id(), |attributes, storage| { + let Some(value) = attributes.get_mut_with_index(value_id.index()) else { return }; + value.reload(storage); + dirty_widgets.push(element.id()); + }); if let Change::Dropped = change { - // TODO: figure out why this is still here? - // ctx.attribute_storage - // .with_mut(value_id.key(), |attributes, attribute_storage| { - // if let Some(value) = attributes.get_mut_with_index(value_id.index()) { - // value.reload_val(value_id, ctx.globals, ctx.scope, ctx.states, attribute_storage); - // } - // }); + // TODO: Is there anything that needs to be done here given that the value will } } - WidgetKind::For(for_loop) => for_loop.update(change, tree, ctx)?, + WidgetKind::For(for_loop) => for_loop.update(change, tree, ctx, widget.parent_widget, dirty_widgets)?, WidgetKind::With(with) => with.update(change, tree, ctx.attribute_storage)?, WidgetKind::Iteration(_) => todo!(), - WidgetKind::ControlFlow(controlflow) => controlflow.update(change, value_id.index().into(), tree, ctx), + WidgetKind::ControlFlow(controlflow) => controlflow.update( + change, + value_id.index().into(), + tree, + ctx, + widget.parent_widget, + dirty_widgets, + ), WidgetKind::ControlFlowContainer(_) => unreachable!("control flow containers have no values"), WidgetKind::Component(_) => (), WidgetKind::Slot => todo!(), diff --git a/anathema-widgets/src/query/components.rs b/anathema-widgets/src/query/components.rs index 2750d304..6fc5d94d 100644 --- a/anathema-widgets/src/query/components.rs +++ b/anathema-widgets/src/query/components.rs @@ -112,7 +112,7 @@ where let mut elements = Nodes::new( children, self.query.elements.attributes, - self.query.elements.needs_layout, + self.query.elements.dirty_widgets, ); let query = ComponentQuery { diff --git a/anathema-widgets/src/query/elements.rs b/anathema-widgets/src/query/elements.rs index 686020df..17d59b32 100644 --- a/anathema-widgets/src/query/elements.rs +++ b/anathema-widgets/src/query/elements.rs @@ -124,8 +124,7 @@ where if self.query.filter.filter(element, self.query.elements.attributes) { let attributes = self.query.elements.attributes.get_mut(element.id()); let ret_val = f(element, attributes); - element.invalidate_cache(); - *self.query.elements.needs_layout = true; + self.query.elements.dirty_widgets.push(element.id()); if !continuous { return ControlFlow::Break(ret_val); @@ -136,7 +135,7 @@ where let mut elements = Nodes::new( children, self.query.elements.attributes, - self.query.elements.needs_layout, + self.query.elements.dirty_widgets, ); let query = ElementQuery { @@ -180,7 +179,7 @@ impl<'bp, 'a> Filter<'bp> for Kind<'a> { attribs.get(key).map(|attribute| value.eq(attribute)).unwrap_or(false) } Kind::AtPosition(pos) => { - let region = Region::from((el.get_pos(), el.size())); + let region = Region::from((el.pos(), el.size())); region.contains(*pos) } Kind::ById(id) => el.id() == *id, diff --git a/anathema-widgets/src/query/mod.rs b/anathema-widgets/src/query/mod.rs index a5cc9e13..35c957b9 100644 --- a/anathema-widgets/src/query/mod.rs +++ b/anathema-widgets/src/query/mod.rs @@ -3,7 +3,7 @@ use anathema_value_resolver::{AttributeStorage, ValueKind}; pub use self::components::Components; pub use self::elements::Elements; -use crate::WidgetTreeView; +use crate::{DirtyWidgets, WidgetTreeView}; mod components; mod elements; @@ -14,9 +14,9 @@ impl<'tree, 'bp> Children<'tree, 'bp> { pub fn new( children: WidgetTreeView<'tree, 'bp>, attribute_storage: &'tree mut AttributeStorage<'bp>, - needs_layout: &'tree mut bool, + dirty_widgets: &'tree mut DirtyWidgets, ) -> Self { - Self(Nodes::new(children, attribute_storage, needs_layout)) + Self(Nodes::new(children, attribute_storage, dirty_widgets)) } pub fn elements(&mut self) -> Elements<'_, 'tree, 'bp> { @@ -117,19 +117,19 @@ impl PartialEq> for QueryValue<'_> { pub struct Nodes<'tree, 'bp> { children: WidgetTreeView<'tree, 'bp>, attributes: &'tree mut AttributeStorage<'bp>, - needs_layout: &'tree mut bool, + dirty_widgets: &'tree mut DirtyWidgets, } impl<'tree, 'bp> Nodes<'tree, 'bp> { pub fn new( children: WidgetTreeView<'tree, 'bp>, attribute_storage: &'tree mut AttributeStorage<'bp>, - needs_layout: &'tree mut bool, + dirty_widgets: &'tree mut DirtyWidgets, ) -> Self { Self { children, attributes: attribute_storage, - needs_layout, + dirty_widgets, } } } @@ -209,8 +209,8 @@ mod test { "; crate::testing::with_template(tpl, |tree, attributes| { - let mut changed = false; - let mut children = Children::new(tree, attributes, &mut changed); + let mut dirty = DirtyWidgets::empty(); + let mut children = Children::new(tree, attributes, &mut dirty); let mut cntr = 0; children.elements().by_tag("text").each(|el, _| { assert_eq!(el.ident, "text"); @@ -234,8 +234,8 @@ mod test { "; crate::testing::with_template(tpl, |tree, attributes| { - let mut changed = false; - let mut children = Children::new(tree, attributes, &mut changed); + let mut dirty = DirtyWidgets::empty(); + let mut children = Children::new(tree, attributes, &mut dirty); let mut cntr = 0; children.elements().by_tag("text").by_attribute("a", 1).each(|el, _| { diff --git a/anathema-widgets/src/testing.rs b/anathema-widgets/src/testing.rs index ee498a26..c2b492a7 100644 --- a/anathema-widgets/src/testing.rs +++ b/anathema-widgets/src/testing.rs @@ -19,8 +19,9 @@ where { let mut tree = WidgetTree::empty(); let mut doc = Document::new(tpl); - let (blueprint, globals) = doc.compile().unwrap(); - let globals = Box::leak(Box::new(globals)); + let mut variables = Default::default(); + let blueprint = doc.compile(&mut variables).unwrap(); + let variables = Box::leak(Box::new(variables)); let blueprint = Box::leak(Box::new(blueprint)); let function_table = Box::leak(Box::new(FunctionTable::new())); @@ -41,7 +42,7 @@ where let mut glyph_map = GlyphMap::empty(); let mut layout_ctx = LayoutCtx::new( - globals, + variables, &factory, &mut states, &mut attribute_storage, @@ -53,13 +54,13 @@ where function_table, ); - let mut ctx = layout_ctx.eval_ctx(None); + let mut ctx = layout_ctx.eval_ctx(None, None); let scope = Scope::root(); eval_blueprint(blueprint, &mut ctx, &scope, root_node(), &mut tree.view()).unwrap(); let filter = crate::layout::LayoutFilter; - let mut for_each = LayoutForEach::new(tree.view(), &scope, filter, None); + let mut for_each = LayoutForEach::new(tree.view(), &scope, filter); _ = for_each .each(&mut layout_ctx, |ctx, widget, children| { _ = widget.layout(children, ctx.viewport.constraints(), ctx)?; diff --git a/anathema-widgets/src/tree/mod.rs b/anathema-widgets/src/tree/mod.rs index f7089283..f0625d51 100644 --- a/anathema-widgets/src/tree/mod.rs +++ b/anathema-widgets/src/tree/mod.rs @@ -15,6 +15,7 @@ use anathema_value_resolver::{AttributeStorage, Scope}; use crate::error::Result; use crate::layout::{LayoutCtx, LayoutFilter}; +use crate::nodes::controlflow::Else; use crate::nodes::loops::Iteration; use crate::nodes::{controlflow, eval_blueprint}; use crate::widget::WidgetTreeView; @@ -100,21 +101,18 @@ pub struct LayoutForEach<'a, 'bp> { scope: &'a Scope<'a, 'bp>, generator: Option>, parent_component: Option, + pub parent_element: Option, filter: LayoutFilter, } impl<'a, 'bp> LayoutForEach<'a, 'bp> { - pub fn new( - tree: WidgetTreeView<'a, 'bp>, - scope: &'a Scope<'a, 'bp>, - filter: LayoutFilter, - parent_component: Option, - ) -> Self { + pub fn new(tree: WidgetTreeView<'a, 'bp>, scope: &'a Scope<'a, 'bp>, filter: LayoutFilter) -> Self { Self { tree, scope, generator: None, - parent_component, + parent_component: None, + parent_element: None, filter, } } @@ -125,6 +123,7 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { generator: Generator<'a, 'bp>, filter: LayoutFilter, parent_component: Option, + parent_element: Option, ) -> Self { Self { tree, @@ -132,16 +131,10 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { generator: Some(generator), filter, parent_component, + parent_element, } } - // pub fn first(&mut self, ctx: &mut LayoutCtx<'_, 'bp>, mut f: F) -> Result> - // where - // F: FnMut(&mut LayoutCtx<'_, 'bp>, &mut Element<'bp>, LayoutForEach<'_, 'bp>) -> Result>, - // { - // self.inner_each(ctx, &mut f) - // } - pub fn each(&mut self, ctx: &mut LayoutCtx<'_, 'bp>, mut f: F) -> Result> where F: FnMut(&mut LayoutCtx<'_, 'bp>, &mut Element<'bp>, LayoutForEach<'_, 'bp>) -> Result>, @@ -167,7 +160,14 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { // Therefore there is no need to worry about excessive creation of `Iter`s for loops. loop { let index = self.tree.layout_len(); - if !generate(parent, &mut self.tree, ctx, self.scope, self.parent_component)? { + if !generate( + parent, + &mut self.tree, + ctx, + self.scope, + self.parent_component, + self.parent_element, + )? { break; } match self.process(index, ctx, f)? { @@ -210,6 +210,7 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { }, self.filter, self.parent_component, + Some(el.id()), ); f(ctx, el, children) } @@ -221,6 +222,7 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { generator, self.filter, self.parent_component, + self.parent_element, ); children.inner_each(ctx, f) } @@ -237,6 +239,7 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { Generator::from_loop(widget.children, for_loop.binding, len), self.filter, self.parent_component, + self.parent_element, ); children.inner_each(ctx, f) @@ -255,6 +258,7 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { Generator::from(&*widget), self.filter, self.parent_component, + self.parent_element, ); children.inner_each(ctx, f) } @@ -266,6 +270,7 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { Generator::from_with(widget.children, with.binding), self.filter, self.parent_component, + self.parent_element, ); children.inner_each(ctx, f) @@ -280,6 +285,7 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { Generator::from(&*widget), self.filter, Some(parent_component), + self.parent_element, ); children.inner_each(ctx, f) } @@ -290,6 +296,7 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { Generator::from(&*widget), self.filter, self.parent_component, + self.parent_element, ); children.inner_each(ctx, f) } @@ -300,6 +307,7 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { Generator::from(&*widget), self.filter, self.parent_component, + self.parent_element, ); children.inner_each(ctx, f) } @@ -307,10 +315,6 @@ impl<'a, 'bp> LayoutForEach<'a, 'bp> { }) .unwrap_or(Ok(ControlFlow::Continue(()))) } - - pub(crate) fn len(&self) -> usize { - self.tree.layout_len() - } } // Generate the next available widget into the tree @@ -322,11 +326,13 @@ fn generate<'bp>( ctx: &mut LayoutCtx<'_, 'bp>, scope: &Scope<'_, 'bp>, parent_component: Option, + parent_widget: Option, ) -> Result { match parent { Generator::Single { body: blueprints, .. } | Generator::Iteration { body: blueprints, .. } | Generator::With { body: blueprints, .. } + | Generator::Slot(blueprints) | Generator::ControlFlowContainer(blueprints) => { if blueprints.is_empty() { return Ok(false); @@ -337,27 +343,10 @@ fn generate<'bp>( return Ok(false); } - let mut ctx = ctx.eval_ctx(parent_component); - // TODO: unwrap. - // this should propagate somewhere useful + let mut ctx = ctx.eval_ctx(parent_component, parent_widget); eval_blueprint(&blueprints[index], &mut ctx, scope, tree.offset, tree)?; Ok(true) } - - Generator::Slot(blueprints) => { - if blueprints.is_empty() { - return Ok(false); - } - - let index = tree.layout_len(); - if index >= blueprints.len() { - return Ok(false); - } - - let mut ctx = ctx.eval_ctx(parent_component); - eval_blueprint(&blueprints[index], &mut ctx, scope, tree.offset, tree).unwrap(); - Ok(true) - } Generator::Loop { len, .. } if len == tree.layout_len() => Ok(false), Generator::Loop { binding, body, .. } => { let loop_index = tree.layout_len(); @@ -367,7 +356,7 @@ fn generate<'bp>( loop_index: StateValue::new(loop_index as i64), binding, }); - let widget = WidgetContainer::new(widget, body); + let widget = WidgetContainer::new(widget, body, parent_widget); // NOTE: for this to fail one of the values along the path would have to // have been removed transaction.commit_child(widget).unwrap(); @@ -377,9 +366,6 @@ fn generate<'bp>( let child_count = tree.layout_len(); assert_eq!(child_count.saturating_sub(1), 0, "too many branches have been created"); - // TODO: this could probably be replaced with the functionality in - // ControlFlow::has_changed - let should_create = { if child_count == 0 { true @@ -408,34 +394,29 @@ fn generate<'bp>( return Ok(false); } - let thing = controlflow - .elses - .iter() - .enumerate() - .filter_map(|(id, node)| { - // If there is a condition but it's not a bool, then it's false - // If there is no condition then it's true (a conditionless else) - // Everything else is down to the value - let cond = match node.cond.as_ref() { - Some(val) => val.truthiness(), - None => true, - }; - match cond { - true => Some((id, node.body)), - false => None, - } - }) - .next(); - - match thing { - Some((id, body)) => { - let kind = WidgetKind::ControlFlowContainer(id as u16); - let widget = WidgetContainer::new(kind, body); - let transaction = tree.insert(tree.offset); - transaction.commit_child(widget); + let cond = |(id, node): (usize, &Else<'bp>)| { + // If there is a condition but it's not a bool, then it's false + // If there is no condition then it's true (a conditionless else) + // Everything else is down to the value + let cond = match node.cond.as_ref() { + Some(val) => val.truthiness(), + None => true, + }; + match cond { + true => Some((id, node.body)), + false => None, } + }; + + let (id, body) = match controlflow.elses.iter().enumerate().filter_map(cond).next() { + Some(val) => val, None => return Ok(false), - } + }; + + let kind = WidgetKind::ControlFlowContainer(id as u16); + let widget = WidgetContainer::new(kind, body, parent_widget); + let transaction = tree.insert(tree.offset); + transaction.commit_child(widget); Ok(true) } diff --git a/anathema-widgets/src/widget/mod.rs b/anathema-widgets/src/widget/mod.rs index f87a3085..27c0cc24 100644 --- a/anathema-widgets/src/widget/mod.rs +++ b/anathema-widgets/src/widget/mod.rs @@ -40,6 +40,10 @@ pub struct CompEntry { component_id: ComponentBlueprintId, } +// ----------------------------------------------------------------------------- +// - Components - +// ----------------------------------------------------------------------------- + /// Store a list of components currently in the tree pub struct Components { inner: Vec, @@ -161,6 +165,10 @@ impl ComponentParents { } } +// ----------------------------------------------------------------------------- +// - Any widget - +// ----------------------------------------------------------------------------- + /// Any widget should never be implemented directly /// as it's implemented for any type that implements `Widget` pub trait AnyWidget { @@ -305,3 +313,44 @@ impl Debug for dyn Widget { write!(f, "") } } + +// ----------------------------------------------------------------------------- +// - Dirty widgets collection - +// ----------------------------------------------------------------------------- +pub struct DirtyWidgets { + pub inner: Vec, + last_id: Option, +} + +impl DirtyWidgets { + pub fn empty() -> Self { + Self { + inner: vec![], + last_id: None, + } + } + + pub fn push(&mut self, widget_id: WidgetId) { + if let Some(last) = self.last_id + && last == widget_id + { + return; + } + + self.last_id.replace(widget_id); + self.inner.push(widget_id); + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + pub fn clear(&mut self) { + self.inner.clear(); + } + + pub fn drain(&mut self) -> impl Iterator { + _ = self.last_id.take(); + self.inner.drain(..) + } +} diff --git a/tests/if-bug-component-reuse.rs b/tests/if-bug-component-reuse.rs index c896e4d5..09263c44 100644 --- a/tests/if-bug-component-reuse.rs +++ b/tests/if-bug-component-reuse.rs @@ -14,10 +14,11 @@ use anathema_testutils::{BasicComp, BasicState, character}; // ``` static TEMPLATE: &str = " +container if state.number == 0 - @comp [val: !state.boolean] + @comp [val: true] else - @comp [val: state.boolean] + @comp [val: false] "; fn keypress(_: KeyEvent, state: &mut BasicState, _: Children<'_, '_>, _: Context<'_, '_, BasicState>) { @@ -25,18 +26,11 @@ fn keypress(_: KeyEvent, state: &mut BasicState, _: Children<'_, '_>, _: Context } #[test] -fn bug_component_reuse_bug() { +fn component_reuse_bug() { let doc = Document::new("@index"); let mut backend = TestBackend::new((10, 3)); - backend - .events() - .next() - .press(character('x')) - .next() - .press(character('x')) - .next() - .stop(); + backend.events().next().press(character('x')).next().stop(); let mut builder = Runtime::builder(doc, &backend); builder diff --git a/tests/nested_collections.rs b/tests/nested_collections.rs index 71b68b4b..c8880967 100644 --- a/tests/nested_collections.rs +++ b/tests/nested_collections.rs @@ -33,8 +33,9 @@ fn keypress(_: KeyEvent, state: &mut Outer, _: Children<'_, '_>, _: Context<'_, #[test] fn nested_collections() { let tpl = " - for i in state.inner.list - text i + container + for i in state.inner.list + text i "; let doc = Document::new("@index");