Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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::<T>` is now available on `Attributes`
* Performance improvements
* 0.2.11
* FEATURE: ranges
* `padding` function
Expand Down
26 changes: 13 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down Expand Up @@ -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"]

Expand All @@ -40,24 +40,24 @@ workspace = true

[workspace.package]
edition = "2024"
version = "0.2.11"
version = "0.2.12-beta"

[workspace.dependencies]
bitflags = "2.4.1"
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 = [
Expand Down
120 changes: 99 additions & 21 deletions anathema-backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 -
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -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(())
}

Expand Down
2 changes: 1 addition & 1 deletion anathema-default-widgets/src/overflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<bool>(CLAMP).unwrap_or_default() {
match attributes.get_as::<bool>(CLAMP).unwrap_or(true) {
false => (),
true => self.clamp(self.inner_size, ctx.inner_size),
}
Expand Down
28 changes: 15 additions & 13 deletions anathema-default-widgets/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -159,15 +159,16 @@ 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,
backend: TestBackend::new(size),
states,
component_registry,
blueprint,
variables: globals,
variables,
components: Components::new(),
function_table: FunctionTable::new(),
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
})
Expand Down Expand Up @@ -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);

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion anathema-default-widgets/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ impl Widget for Text {
let Some(_span) = child.try_to_ref::<Span>() 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) {
Expand Down
9 changes: 8 additions & 1 deletion anathema-geometry/src/region.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 12 additions & 4 deletions anathema-runtime/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,6 +28,7 @@ pub struct Builder<G> {
global_event_handler: G,
hot_reload: bool,
function_table: FunctionTable,
variables: Variables,
}

impl<G: GlobalEventHandler> Builder<G> {
Expand All @@ -54,6 +55,7 @@ impl<G: GlobalEventHandler> Builder<G> {
global_event_handler,
hot_reload: true,
function_table: FunctionTable::new(),
variables: Variables::new(),
}
}

Expand Down Expand Up @@ -164,9 +166,15 @@ impl<G: GlobalEventHandler> Builder<G> {
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<String>, value: impl Into<Expression>) -> Result<()> {
self.variables.define_global(key, value).map_err(|e| e.to_error(None))?;
Ok(())
}

pub fn finish<F, B>(mut self, backend: &mut B, mut f: F) -> Result<()>
where
F: FnMut(&mut Runtime<G>, &mut B) -> Result<()>,
Expand All @@ -180,8 +188,8 @@ impl<G: GlobalEventHandler> Builder<G> {
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.
Expand All @@ -199,7 +207,7 @@ impl<G: GlobalEventHandler> Builder<G> {

let mut inst = Runtime::new(
blueprint,
globals,
self.variables,
self.component_registry,
self.document,
self.factory,
Expand Down
Loading
Loading