Skip to content

Commit a4b7f5d

Browse files
committed
Performance improvements
1 parent 70c01ac commit a4b7f5d

File tree

40 files changed

+767
-806
lines changed

40 files changed

+767
-806
lines changed

CHANGES.md

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

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/testing.rs

Lines changed: 12 additions & 11 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

@@ -235,7 +235,7 @@ impl<'bp> TestInstance<'bp> {
235235
function_table,
236236
);
237237

238-
let mut ctx = ctx.eval_ctx(None);
238+
let mut ctx = ctx.eval_ctx(None, None);
239239
let mut view = tree.view();
240240

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

294-
if let Some(widget) = tree.get_mut(widget_id) {
295-
if let WidgetKind::Element(element) = &mut widget.kind {
296-
element.invalidate_cache();
297-
}
298-
}
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+
// }
299300

300301
// check that the node hasn't already been removed
301302
if !tree.contains(widget_id) {
@@ -304,7 +305,7 @@ impl<'bp> TestInstance<'bp> {
304305

305306
_ = tree
306307
.with_value_mut(value_id.key(), |_path, widget, tree| {
307-
update_widget(widget, value_id, change, tree, &mut ctx)
308+
update_widget(widget, value_id, change, tree, &mut ctx, &mut DirtyWidgets::empty())
308309
})
309310
.unwrap();
310311
})
@@ -333,7 +334,7 @@ impl<'bp> TestInstance<'bp> {
333334
);
334335

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

338339
self.backend.render(&mut self.glyph_map);
339340

@@ -355,8 +356,8 @@ impl<'bp> TestInstance<'bp> {
355356
// let path = &[0, 0, 0];
356357

357358
let tree = self.tree.view();
358-
let mut update = true;
359-
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);
360361
let elements = children.elements();
361362
f(elements);
362363
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);

0 commit comments

Comments
 (0)