Skip to content

Commit 907021d

Browse files
authored
Merge pull request #142 from togglebyte/dev
0.2.12-beta
2 parents 64a2a3b + 16087a2 commit 907021d

File tree

48 files changed

+865
-869
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+865
-869
lines changed

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
* 0.2.12
2+
* Clamping is now true by default on overflow
3+
* Globals can now be registered with the runtime
4+
* `attributes.value_as::<T>` is now available on `Attributes`
5+
* Performance improvements
16
* 0.2.11
27
* FEATURE: ranges
38
* `padding` function

Cargo.toml

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "anathema"
33
edition = "2024"
4-
version = "0.2.11"
4+
version = "0.2.12-beta"
55
license = "MIT"
66
description = "Create beautiful, easily customisable terminal applications"
77
keywords = ["tui", "terminal", "widgets", "ui", "layout"]
@@ -31,7 +31,7 @@ anathema-testutils = { path = "anathema-testutils" }
3131

3232
[features]
3333
default = []
34-
profile = ["anathema-runtime/profile", "anathema-widgets/profile", "anathema-backend/profile"]
34+
profile = ["anathema-runtime/profile", "anathema-widgets/profile", "anathema-backend/profile", "anathema-value-resolver/profile"]
3535
serde = ["anathema-state/serde", "anathema-store/serde"]
3636
# filelog = ["anathema-debug/filelog", "anathema-widgets/filelog", "anathema-runtime/filelog"]
3737

@@ -40,24 +40,24 @@ workspace = true
4040

4141
[workspace.package]
4242
edition = "2024"
43-
version = "0.2.11"
43+
version = "0.2.12-beta"
4444

4545
[workspace.dependencies]
4646
bitflags = "2.4.1"
4747
crossterm = "0.28.1"
4848
unicode-width = "0.1.11"
4949
flume = "0.11.0"
5050
notify = "6.1.1"
51-
anathema-default-widgets = { path = "./anathema-default-widgets", version = "0.2.11" }
52-
anathema-backend = { path = "./anathema-backend", version = "0.2.11" }
53-
anathema-runtime = { path = "./anathema-runtime", version = "0.2.11" }
54-
anathema-state = { path = "./anathema-state", version = "0.2.11" }
55-
anathema-state-derive = { path = "./anathema-state-derive", version = "0.2.11" }
56-
anathema-store = { path = "./anathema-store", version = "0.2.11" }
57-
anathema-templates = { path = "./anathema-templates", version = "0.2.11" }
58-
anathema-widgets = { path = "./anathema-widgets", version = "0.2.11" }
59-
anathema-geometry = { path = "./anathema-geometry", version = "0.2.11" }
60-
anathema-value-resolver = { path = "./anathema-value-resolver", version = "0.2.11" }
51+
anathema-default-widgets = { path = "./anathema-default-widgets", version = "0.2.12-beta" }
52+
anathema-backend = { path = "./anathema-backend", version = "0.2.12-beta" }
53+
anathema-runtime = { path = "./anathema-runtime", version = "0.2.12-beta" }
54+
anathema-state = { path = "./anathema-state", version = "0.2.12-beta" }
55+
anathema-state-derive = { path = "./anathema-state-derive", version = "0.2.12-beta" }
56+
anathema-store = { path = "./anathema-store", version = "0.2.12-beta" }
57+
anathema-templates = { path = "./anathema-templates", version = "0.2.12-beta" }
58+
anathema-widgets = { path = "./anathema-widgets", version = "0.2.12-beta" }
59+
anathema-geometry = { path = "./anathema-geometry", version = "0.2.12-beta" }
60+
anathema-value-resolver = { path = "./anathema-value-resolver", version = "0.2.12-beta" }
6161

6262
[workspace]
6363
members = [

anathema-backend/src/lib.rs

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use anathema_widgets::components::events::Event;
77
use anathema_widgets::error::Result;
88
use anathema_widgets::layout::{Constraints, LayoutCtx, LayoutFilter, PositionFilter, Viewport};
99
use anathema_widgets::paint::PaintFilter;
10-
use anathema_widgets::{GlyphMap, LayoutForEach, PaintChildren, PositionChildren, WidgetTreeView};
10+
use anathema_widgets::{
11+
DirtyWidgets, GlyphMap, Layout, LayoutForEach, PaintChildren, PositionChildren, WidgetTreeView,
12+
};
1113

1214
pub mod testing;
1315
pub mod tui;
@@ -55,14 +57,7 @@ impl<'rt, 'bp, T: Backend> WidgetCycle<'rt, 'bp, T> {
5557
}
5658
}
5759

58-
fn fixed(&mut self, ctx: &mut LayoutCtx<'_, 'bp>, needs_layout: bool) -> Result<()> {
59-
// -----------------------------------------------------------------------------
60-
// - Layout -
61-
// -----------------------------------------------------------------------------
62-
if needs_layout {
63-
self.layout(ctx, LayoutFilter)?;
64-
}
65-
60+
fn fixed(&mut self, ctx: &mut LayoutCtx<'_, 'bp>) -> Result<()> {
6661
// -----------------------------------------------------------------------------
6762
// - Position -
6863
// -----------------------------------------------------------------------------
@@ -90,24 +85,107 @@ impl<'rt, 'bp, T: Backend> WidgetCycle<'rt, 'bp, T> {
9085
Ok(())
9186
}
9287

93-
pub fn run(&mut self, ctx: &mut LayoutCtx<'_, 'bp>, needs_layout: bool) -> Result<()> {
94-
self.fixed(ctx, needs_layout)?;
88+
pub fn run(
89+
&mut self,
90+
ctx: &mut LayoutCtx<'_, 'bp>,
91+
force_layout: bool,
92+
dirty_widgets: &mut DirtyWidgets,
93+
) -> Result<()> {
94+
// -----------------------------------------------------------------------------
95+
// - Layout -
96+
// -----------------------------------------------------------------------------
97+
self.layout(ctx, LayoutFilter, dirty_widgets, force_layout)?;
98+
99+
// -----------------------------------------------------------------------------
100+
// - Position and paint -
101+
// -----------------------------------------------------------------------------
102+
self.fixed(ctx)?;
95103
self.floating(ctx)?;
96104
Ok(())
97105
}
98106

99-
fn layout(&mut self, ctx: &mut LayoutCtx<'_, 'bp>, filter: LayoutFilter) -> Result<()> {
107+
fn layout(
108+
&mut self,
109+
ctx: &mut LayoutCtx<'_, 'bp>,
110+
filter: LayoutFilter,
111+
dirty_widgets: &mut DirtyWidgets,
112+
force_layout: bool,
113+
) -> Result<()> {
100114
#[cfg(feature = "profile")]
101115
puffin::profile_function!();
102-
let tree = self.tree.view();
103-
104-
let scope = Scope::root();
105-
let mut for_each = LayoutForEach::new(tree, &scope, filter, None);
106-
let constraints = self.constraints;
107-
_ = for_each.each(ctx, |ctx, widget, children| {
108-
_ = widget.layout(children, constraints, ctx)?;
109-
Ok(ControlFlow::Break(()))
110-
})?;
116+
117+
let mut tree = self.tree.view();
118+
119+
if force_layout {
120+
// Perform a layout across the entire tree
121+
let scope = Scope::root();
122+
let mut for_each = LayoutForEach::new(tree, &scope, filter);
123+
let constraints = self.constraints;
124+
_ = for_each.each(ctx, |ctx, widget, children| {
125+
_ = widget.layout(children, constraints, ctx)?;
126+
Ok(ControlFlow::Break(()))
127+
})?;
128+
return Ok(());
129+
}
130+
131+
// If a widget has changed, mark the parent as dirty
132+
133+
// Layout only changed widgets.
134+
// These are the parents of changed widgets.
135+
//
136+
// Investigate the possibility of attaching an offset as existing widgets don't need
137+
// to reflow unless the constraint has changed.
138+
//
139+
// This means `additional_widgets` needs to store (key, offset) where offset can be None
140+
//
141+
// If this is going to work we need to consider `expand` and `spacer`
142+
//
143+
// Since widgets can be made by anyone and they are always guaranteed to give
144+
// access to all their children this might not be a possibility.
145+
//
146+
// parent
147+
// widget 0
148+
// widget 1
149+
// widget 2 | <- if this changes, only reflow this, three and four
150+
// widget 3 |-- reflow
151+
// widget 4 |
152+
153+
// TODO: make `additional_widgets` a scratch buffer part of `DirtyWidgets`.
154+
// Also ensure that it tracks last id as well
155+
// ... and removes it when done!
156+
let mut additional_widgets = vec![];
157+
158+
loop {
159+
for widget_id in dirty_widgets.drain() {
160+
if !tree.contains(widget_id) {
161+
continue;
162+
}
163+
tree.with_value_mut(widget_id, |_, widget, children| {
164+
let scope = Scope::root();
165+
let mut children = LayoutForEach::new(children, &scope, filter);
166+
children.parent_element = Some(widget_id);
167+
let parent_id = widget.parent_widget;
168+
let anathema_widgets::WidgetKind::Element(widget) = &mut widget.kind else { return };
169+
170+
let constraints = widget.constraints();
171+
if let Ok(Layout::Changed(_)) = widget.layout(children, constraints, ctx) {
172+
// write into scratch buffer
173+
if let Some(id) = parent_id {
174+
additional_widgets.push(id);
175+
}
176+
}
177+
});
178+
}
179+
180+
// merge the scratch if it's not empty
181+
182+
if additional_widgets.is_empty() {
183+
break;
184+
}
185+
186+
dirty_widgets.inner.append(&mut additional_widgets);
187+
}
188+
111189
Ok(())
112190
}
113191

anathema-default-widgets/src/overflow.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ impl Widget for Overflow {
163163
let mut pos = ctx.pos;
164164

165165
// If the value is clamped, update the offset
166-
match attributes.get_as::<bool>(CLAMP).unwrap_or_default() {
166+
match attributes.get_as::<bool>(CLAMP).unwrap_or(true) {
167167
false => (),
168168
true => self.clamp(self.inner_size, ctx.inner_size),
169169
}

anathema-default-widgets/src/testing.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use anathema_widgets::layout::{Constraints, LayoutCtx, Viewport};
1111
use anathema_widgets::paint::{Glyph, paint};
1212
use anathema_widgets::query::{Children, Elements};
1313
use anathema_widgets::{
14-
Components, Factory, FloatingWidgets, GlyphMap, Style, WidgetKind, WidgetRenderer, WidgetTree, eval_blueprint,
14+
Components, DirtyWidgets, Factory, FloatingWidgets, GlyphMap, Style, WidgetRenderer, WidgetTree, eval_blueprint,
1515
update_widget,
1616
};
1717

@@ -159,15 +159,16 @@ impl TestRunner {
159159
let main = doc.add_component("main", src.to_template()).unwrap();
160160
component_registry.add_component(main, (), TestState::new());
161161

162-
let (blueprint, globals) = doc.compile().unwrap();
162+
let mut variables = Default::default();
163+
let blueprint = doc.compile(&mut variables).unwrap();
163164

164165
Self {
165166
factory,
166167
backend: TestBackend::new(size),
167168
states,
168169
component_registry,
169170
blueprint,
170-
variables: globals,
171+
variables,
171172
components: Components::new(),
172173
function_table: FunctionTable::new(),
173174
}
@@ -234,7 +235,7 @@ impl<'bp> TestInstance<'bp> {
234235
function_table,
235236
);
236237

237-
let mut ctx = ctx.eval_ctx(None);
238+
let mut ctx = ctx.eval_ctx(None, None);
238239
let mut view = tree.view();
239240

240241
eval_blueprint(blueprint, &mut ctx, &scope, &[], &mut view).unwrap();
@@ -290,11 +291,12 @@ impl<'bp> TestInstance<'bp> {
290291
sub.iter().for_each(|value_id| {
291292
let widget_id = value_id.key();
292293

293-
if let Some(widget) = tree.get_mut(widget_id) {
294-
if let WidgetKind::Element(element) = &mut widget.kind {
295-
element.invalidate_cache();
296-
}
297-
}
294+
// TODO: review if this is still needed: - TB 2025-08-27
295+
// if let Some(widget) = tree.get_mut(widget_id) {
296+
// if let WidgetKind::Element(element) = &mut widget.kind {
297+
// element.invalidate_cache();
298+
// }
299+
// }
298300

299301
// check that the node hasn't already been removed
300302
if !tree.contains(widget_id) {
@@ -303,7 +305,7 @@ impl<'bp> TestInstance<'bp> {
303305

304306
_ = tree
305307
.with_value_mut(value_id.key(), |_path, widget, tree| {
306-
update_widget(widget, value_id, change, tree, &mut ctx)
308+
update_widget(widget, value_id, change, tree, &mut ctx, &mut DirtyWidgets::empty())
307309
})
308310
.unwrap();
309311
})
@@ -332,7 +334,7 @@ impl<'bp> TestInstance<'bp> {
332334
);
333335

334336
let mut cycle = WidgetCycle::new(self.backend, self.tree.view(), constraints);
335-
_ = cycle.run(&mut ctx, true);
337+
_ = cycle.run(&mut ctx, true, &mut DirtyWidgets::empty());
336338

337339
self.backend.render(&mut self.glyph_map);
338340

@@ -354,8 +356,8 @@ impl<'bp> TestInstance<'bp> {
354356
// let path = &[0, 0, 0];
355357

356358
let tree = self.tree.view();
357-
let mut update = true;
358-
let mut children = Children::new(tree, &mut self.attribute_storage, &mut update);
359+
let mut dirty_widgets = DirtyWidgets::empty();
360+
let mut children = Children::new(tree, &mut self.attribute_storage, &mut dirty_widgets);
359361
let elements = children.elements();
360362
f(elements);
361363
self

anathema-default-widgets/src/text.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ impl Widget for Text {
9797
let Some(_span) = child.try_to_ref::<Span>() else {
9898
return Ok(ControlFlow::Continue(()));
9999
};
100-
self.strings.set_style(child.id());
101100

101+
self.strings.set_style(child.id());
102102
let attributes = ctx.attributes(child.id());
103103
if let Some(text) = attributes.value() {
104104
text.strings(|s| match self.strings.add_str(s) {

anathema-geometry/src/region.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,19 @@ impl Region {
4949
}
5050

5151
/// Check if a region contains a position.
52-
/// Regions are exclusive, so a region from 0,0 to 10, 10 contains `Pos::ZERO`
52+
/// The check is exclusive, so a region from 0,0 to 10, 10 contains `Pos::ZERO`
5353
/// but not `Pos::New(10, 10)`
5454
pub const fn contains(&self, pos: Pos) -> bool {
5555
pos.x >= self.from.x && pos.x < self.to.x && pos.y >= self.from.y && pos.y < self.to.y
5656
}
5757

58+
/// Check if a region contains a position.
59+
/// The check is inclusive, so a region from 0,0 to 10, 10 contains `Pos::ZERO`
60+
/// as well as `Pos::New(10, 10)`
61+
pub const fn icontains(&self, pos: Pos) -> bool {
62+
pos.x >= self.from.x && pos.x <= self.to.x && pos.y >= self.from.y && pos.y <= self.to.y
63+
}
64+
5865
/// Constrain a region to fit within another region
5966
pub fn constrain(&mut self, other: &Region) {
6067
self.from.x = self.from.x.max(other.from.x);

anathema-runtime/src/builder.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::sync::atomic::Ordering;
33
use anathema_backend::Backend;
44
use anathema_default_widgets::register_default_widgets;
55
use anathema_geometry::Size;
6-
use anathema_templates::{Document, ToSourceKind};
6+
use anathema_templates::{Document, Expression, ToSourceKind, Variables};
77
use anathema_value_resolver::{Function, FunctionTable};
88
use anathema_widgets::components::deferred::DeferredComponents;
99
use anathema_widgets::components::events::Event;
@@ -28,6 +28,7 @@ pub struct Builder<G> {
2828
global_event_handler: G,
2929
hot_reload: bool,
3030
function_table: FunctionTable,
31+
variables: Variables,
3132
}
3233

3334
impl<G: GlobalEventHandler> Builder<G> {
@@ -54,6 +55,7 @@ impl<G: GlobalEventHandler> Builder<G> {
5455
global_event_handler,
5556
hot_reload: true,
5657
function_table: FunctionTable::new(),
58+
variables: Variables::new(),
5759
}
5860
}
5961

@@ -164,9 +166,15 @@ impl<G: GlobalEventHandler> Builder<G> {
164166
global_event_handler,
165167
hot_reload: self.hot_reload,
166168
function_table: self.function_table,
169+
variables: Variables::new(),
167170
}
168171
}
169172

173+
pub fn register_global(&mut self, key: impl Into<String>, value: impl Into<Expression>) -> Result<()> {
174+
self.variables.define_global(key, value).map_err(|e| e.to_error(None))?;
175+
Ok(())
176+
}
177+
170178
pub fn finish<F, B>(mut self, backend: &mut B, mut f: F) -> Result<()>
171179
where
172180
F: FnMut(&mut Runtime<G>, &mut B) -> Result<()>,
@@ -180,8 +188,8 @@ impl<G: GlobalEventHandler> Builder<G> {
180188
server
181189
};
182190

183-
let (blueprint, globals) = loop {
184-
match self.document.compile() {
191+
let blueprint = loop {
192+
match self.document.compile(&mut self.variables) {
185193
Ok(val) => break val,
186194
// This can only show template errors.
187195
// Widget errors doesn't become available until after the first tick.
@@ -199,7 +207,7 @@ impl<G: GlobalEventHandler> Builder<G> {
199207

200208
let mut inst = Runtime::new(
201209
blueprint,
202-
globals,
210+
self.variables,
203211
self.component_registry,
204212
self.document,
205213
self.factory,

0 commit comments

Comments
 (0)