perf: retain sparse column composition and exact box projections

This commit is contained in:
Kinneyzhang 2026-09-05 15:21:10 +08:00
parent 14c1698c41
commit 39d079b2cc
8 changed files with 4126 additions and 369 deletions

606
native/src/composition.rs Normal file
View File

@ -0,0 +1,606 @@
//! Current Column slots and their persistent composition. Shape contains only
//! original structural paths; target nodes are read from the current document.
use std::sync::Arc;
use super::evaluation::{record, EvalRecord, EvalWork, RenderScope, RenderedChange};
use super::line_plan::{ChangeKind, LineOp, LinePlan, LineSplice, PlanChange, ProjectionState};
use super::{LayoutContext, LayoutNode, Line, LocalStep, Rendered};
#[derive(Debug, Default)]
struct ShapeRoute {
slot: Option<usize>,
next: Box<[(LocalStep, ShapeRoute)]>,
}
#[derive(Debug)]
struct ColumnShape {
paths: Box<[Arc<[LocalStep]>]>,
routes: ShapeRoute,
}
impl ColumnShape {
fn new(paths: Vec<Arc<[LocalStep]>>) -> Arc<Self> {
// column_leaves supplies paths in original lexicographic order. Freeze
// each prefix group once; updates only binary-search these shared arrays.
fn build(paths: &[Arc<[LocalStep]>], first: usize, depth: usize) -> ShapeRoute {
if paths.first().is_some_and(|path| path.len() == depth) {
assert_eq!(paths.len(), 1, "one leaf at each original Column path");
return ShapeRoute {
slot: Some(first),
next: Box::new([]),
};
}
let mut next = Vec::new();
let mut start = 0;
while start < paths.len() {
let step = paths[start][depth];
let mut end = start + 1;
while end < paths.len() && paths[end][depth] == step {
end += 1;
}
record(EvalWork {
column_shape_steps_visited: (end - start) as u64,
..EvalWork::default()
});
next.push((step, build(&paths[start..end], first + start, depth + 1)));
start = end;
}
ShapeRoute {
slot: None,
next: next.into_boxed_slice(),
}
}
let routes = build(&paths, 0, 0);
Arc::new(Self {
paths: paths.into_boxed_slice(),
routes,
})
}
fn dirty_slots(&self, scope: &RenderScope<'_>) -> Vec<usize> {
fn collect(
route: &ShapeRoute,
scope: &RenderScope<'_>,
path: &mut Vec<LocalStep>,
all: bool,
slots: &mut Vec<usize>,
) {
record(EvalWork {
column_shape_steps_visited: 1,
..EvalWork::default()
});
if let Some(slot) = route.slot {
slots.push(slot);
return;
}
let dirty = scope.dirty_route(path);
if all || dirty.local {
for (step, child) in &route.next {
path.push(*step);
collect(child, scope, path, true, slots);
path.pop();
}
} else {
for step in dirty.next {
if let Ok(index) = route.next.binary_search_by_key(&step, |(key, _)| *key) {
path.push(step);
collect(&route.next[index].1, scope, path, false, slots);
path.pop();
}
}
}
}
let mut slots = Vec::new();
collect(&self.routes, scope, &mut Vec::new(), false, &mut slots);
slots
}
fn scope<'a>(&self, scope: &RenderScope<'a>, index: usize) -> Result<RenderScope<'a>, String> {
let mut result = scope.clone();
for step in self.paths[index].iter() {
record(EvalWork {
column_shape_steps_visited: 1,
..EvalWork::default()
});
let (LayoutNode::Column { children, .. }, LocalStep::ColumnChild(index)) =
(result.view.node, step)
else {
return Err("Native retained Column shape lost its original path".to_owned());
};
let child = children
.get(*index)
.ok_or_else(|| "Native retained Column shape lost its child slot".to_owned())?;
result = result.child(*step, child);
}
Ok(result)
}
}
#[derive(Debug)]
struct ColumnSlot {
record: Option<Arc<EvalRecord>>,
raw: LinePlan,
normalized: LinePlan,
projection: Option<Arc<ProjectionState>>,
}
impl ColumnSlot {
fn normalize(
raw: LinePlan,
child: Option<Arc<EvalRecord>>,
proof: Option<&PlanChange>,
target: i64,
previous: Option<&Self>,
) -> (Arc<Self>, Option<PlanChange>) {
let extra = (target - raw.first_width()).max(0);
let (normalized, projection, change) = if extra == 0 {
let change = previous.map(|old| {
proof
.filter(|proof| proof.applies_to(&old.normalized, &raw))
.cloned()
.unwrap_or_else(|| PlanChange::replace_all(&old.normalized, &raw))
});
(raw.clone(), None, change)
} else {
let ops = Arc::from([LineOp::AppendSpace(extra), LineOp::ClearBreaks]);
if let Some(old) = previous.and_then(|old| old.projection.as_ref()) {
let update = old.update(raw.clone(), proof, ops);
(
update.state.plan().clone(),
Some(update.state),
Some(update.change),
)
} else {
let projection = ProjectionState::new(raw.clone(), ops);
let normalized = projection.plan().clone();
let change =
previous.map(|old| PlanChange::replace_all(&old.normalized, &normalized));
(normalized, Some(projection), change)
}
};
(
Arc::new(Self {
record: child,
raw,
normalized,
projection,
}),
change,
)
}
}
#[derive(Clone, Copy, Debug, Default)]
struct ColumnMeasure {
slots: usize,
lines: usize,
max_first: i64,
max_width: i64,
max_width_minus_first: Option<i64>,
first_nonempty: Option<i64>,
}
impl ColumnMeasure {
fn leaf(slot: &ColumnSlot) -> Self {
let first = slot.raw.first_width();
let width = slot.raw.max_width();
Self {
slots: 1,
lines: slot.raw.len(),
max_first: first,
max_width: width,
max_width_minus_first: (!slot.raw.is_empty()).then_some(width - first),
first_nonempty: (!slot.raw.is_empty()).then_some(first),
}
}
fn combine(self, other: Self) -> Self {
Self {
slots: self.slots + other.slots,
lines: self.lines + other.lines,
max_first: self.max_first.max(other.max_first),
max_width: self.max_width.max(other.max_width),
max_width_minus_first: self.max_width_minus_first.max(other.max_width_minus_first),
first_nonempty: self.first_nonempty.or(other.first_nonempty),
}
}
fn normalized_max(self, target: i64) -> i64 {
self.max_width_minus_first
.map_or(0, |delta| self.max_width.max(target + delta))
}
}
#[derive(Debug)]
enum ColumnNode {
Leaf {
slot: Arc<ColumnSlot>,
measure: ColumnMeasure,
},
Branch {
left: Arc<Self>,
right: Arc<Self>,
measure: ColumnMeasure,
lines: LinePlan,
},
}
impl ColumnNode {
fn leaf(slot: Arc<ColumnSlot>) -> Arc<Self> {
record(EvalWork {
column_tree_nodes_created: 1,
..EvalWork::default()
});
Arc::new(Self::Leaf {
measure: ColumnMeasure::leaf(&slot),
slot,
})
}
fn branch(left: Arc<Self>, right: Arc<Self>, previous: Option<&Self>) -> Arc<Self> {
record(EvalWork {
column_tree_nodes_created: 1,
..EvalWork::default()
});
let lines = match previous {
Some(Self::Branch {
left: old_left,
right: old_right,
lines,
..
}) if left.lines().ptr_eq(old_left.lines())
&& right.lines().ptr_eq(old_right.lines()) =>
{
lines.clone()
}
_ => left.lines().concat(right.lines()),
};
Arc::new(Self::Branch {
measure: left.measure().combine(right.measure()),
left,
right,
lines,
})
}
fn build(slots: &[Arc<ColumnSlot>]) -> Option<Arc<Self>> {
match slots.len() {
0 => None,
1 => Some(Self::leaf(Arc::clone(&slots[0]))),
count => {
let mid = count / 2;
Some(Self::branch(
Self::build(&slots[..mid]).unwrap(),
Self::build(&slots[mid..]).unwrap(),
None,
))
}
}
}
fn measure(&self) -> ColumnMeasure {
match self {
Self::Leaf { measure, .. } | Self::Branch { measure, .. } => *measure,
}
}
fn lines(&self) -> &LinePlan {
match self {
Self::Leaf { slot, .. } => &slot.normalized,
Self::Branch { lines, .. } => lines,
}
}
fn slot(&self, index: usize) -> &Arc<ColumnSlot> {
record(EvalWork {
column_tree_nodes_visited: 1,
..EvalWork::default()
});
match self {
Self::Leaf { slot, .. } => {
assert_eq!(index, 0);
slot
}
Self::Branch { left, right, .. } => {
let left_len = left.measure().slots;
if index < left_len {
left.slot(index)
} else {
right.slot(index - left_len)
}
}
}
}
fn prefix_lines(&self, end: usize) -> usize {
record(EvalWork {
column_tree_nodes_visited: 1,
..EvalWork::default()
});
if end == 0 {
return 0;
}
if end == self.measure().slots {
return self.measure().lines;
}
match self {
Self::Leaf { .. } => unreachable!("validated Column prefix"),
Self::Branch { left, right, .. } => {
let left_len = left.measure().slots;
if end <= left_len {
left.prefix_lines(end)
} else {
left.measure().lines + right.prefix_lines(end - left_len)
}
}
}
}
fn replace(&self, index: usize, slot: Arc<ColumnSlot>) -> Arc<Self> {
record(EvalWork {
column_tree_nodes_visited: 1,
..EvalWork::default()
});
match self {
Self::Leaf { .. } => {
assert_eq!(index, 0);
Self::leaf(slot)
}
Self::Branch { left, right, .. } => {
let left_len = left.measure().slots;
if index < left_len {
Self::branch(left.replace(index, slot), Arc::clone(right), Some(self))
} else {
Self::branch(
Arc::clone(left),
right.replace(index - left_len, slot),
Some(self),
)
}
}
}
}
fn retarget(&self, target: i64) -> Arc<Self> {
record(EvalWork {
column_tree_nodes_visited: 1,
..EvalWork::default()
});
match self {
Self::Leaf { slot, .. } => {
record(EvalWork {
column_slots_updated: 1,
..EvalWork::default()
});
let same = PlanChange::same(&slot.raw);
Self::leaf(
ColumnSlot::normalize(
slot.raw.clone(),
slot.record.clone(),
Some(&same),
target,
Some(slot),
)
.0,
)
}
Self::Branch { left, right, .. } => {
Self::branch(left.retarget(target), right.retarget(target), Some(self))
}
}
}
}
#[derive(Debug)]
pub(super) struct ColumnState {
shape: Arc<ColumnShape>,
root: Option<Arc<ColumnNode>>,
target: i64,
lines: LinePlan,
}
#[cfg(test)]
impl ColumnState {
pub(super) fn shares_slot(&self, other: &Self, index: usize) -> bool {
Arc::ptr_eq(
self.root.as_ref().unwrap().slot(index),
other.root.as_ref().unwrap().slot(index),
)
}
pub(super) fn shares_shape(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.shape, &other.shape)
}
}
fn target_width(context: LayoutContext, intrinsic: bool, maximum: i64) -> i64 {
if intrinsic || context.inline_auto_width_intrinsic || !context.viewport_width_known {
maximum
} else {
context.viewport_width.max(0)
}
}
fn output(root: Option<&Arc<ColumnNode>>, previous: Option<&ColumnState>) -> LinePlan {
if let Some(root) = root.filter(|root| root.measure().lines > 0) {
return root.lines().clone();
}
if let Some(previous) = previous.filter(|previous| {
previous
.root
.as_ref()
.is_none_or(|root| root.measure().lines == 0)
}) {
return previous.lines.clone();
}
LinePlan::from_lines([Line::default()])
}
// A concatenation owns its joining break, including the former last line when
// an empty boundary moves. Include that predecessor in each changed span.
fn include_join_boundaries(splices: Vec<LineSplice>) -> Vec<LineSplice> {
let mut result: Vec<LineSplice> = Vec::with_capacity(splices.len());
for mut splice in splices {
if splice.old.start == 0 || splice.new.start == 0 {
splice.old.start = 0;
splice.new.start = 0;
} else {
splice.old.start -= 1;
splice.new.start -= 1;
}
if let Some(previous) = result.last_mut().filter(|previous| {
splice.old.start <= previous.old.end || splice.new.start <= previous.new.end
}) {
previous.old.end = previous.old.end.max(splice.old.end);
previous.new.end = previous.new.end.max(splice.new.end);
} else {
result.push(splice);
}
}
result
}
pub(super) fn render_column<'a>(
scope: &RenderScope<'a>,
children: &'a [LayoutNode],
context: LayoutContext,
intrinsic: bool,
) -> Result<Rendered, String> {
let state = if let Some(previous) = scope.previous_column() {
let dirty = previous.shape.dirty_slots(scope);
scope.inherit_column_uses();
let mut root = previous.root.clone();
let mut changes = Vec::with_capacity(dirty.len());
for index in dirty {
let child_scope = previous.shape.scope(scope, index)?;
let result = child_scope.render_with_change(context, intrinsic, None)?;
let old = previous
.root
.as_ref()
.expect("dirty Column has slots")
.slot(index);
let (slot, change) = ColumnSlot::normalize(
result.rendered.lines,
result.record,
result.change.as_ref(),
previous.target,
Some(old),
);
record(EvalWork {
column_slots_updated: 1,
..EvalWork::default()
});
root = Some(
root.as_ref()
.expect("dirty Column root")
.replace(index, slot),
);
changes.push((index, change.expect("previous normalized child")));
}
let target = target_width(
context,
intrinsic,
root.as_ref().map_or(0, |root| root.measure().max_first),
);
if target != previous.target {
record(EvalWork {
column_full_rebuilds: 1,
..EvalWork::default()
});
root = root.map(|root| root.retarget(target));
}
let lines = output(root.as_ref(), Some(&previous));
let change = if lines.ptr_eq(&previous.lines) {
PlanChange::same(&lines)
} else if target != previous.target
|| previous
.root
.as_ref()
.is_none_or(|root| root.measure().lines == 0)
|| root.as_ref().is_none_or(|root| root.measure().lines == 0)
{
PlanChange::replace_all(&previous.lines, &lines)
} else {
let old_root = previous.root.as_ref().unwrap();
let new_root = root.as_ref().unwrap();
let mut splices = Vec::new();
for (index, change) in changes {
let old_offset = old_root.prefix_lines(index);
let new_offset = new_root.prefix_lines(index);
match change.kind() {
ChangeKind::Same => {}
ChangeKind::Splices(parts) => {
splices.extend(parts.iter().map(|part| LineSplice {
old: old_offset + part.old.start..old_offset + part.old.end,
new: new_offset + part.new.start..new_offset + part.new.end,
}));
}
ChangeKind::ReplaceAll => splices.push(LineSplice {
old: old_offset..old_offset + old_root.slot(index).raw.len(),
new: new_offset..new_offset + new_root.slot(index).raw.len(),
}),
}
}
PlanChange::splices(&previous.lines, &lines, include_join_boundaries(splices))
};
scope.publish_change(change);
Arc::new(ColumnState {
shape: Arc::clone(&previous.shape),
root,
target,
lines,
})
} else {
record(EvalWork {
column_full_rebuilds: 1,
..EvalWork::default()
});
let mut paths = Vec::new();
let mut results = Vec::new();
for child in super::column_leaves(scope, children) {
let path = &child.view.address.path[scope.view.address.path.len()..];
record(EvalWork {
source_path_steps_copied: path.len() as u64,
column_slots_built: 1,
..EvalWork::default()
});
paths.push(Arc::from(path));
results.push(child.render_with_change(context, intrinsic, None)?);
}
let maximum = results
.iter()
.map(|result| result.rendered.first_width())
.max()
.unwrap_or(0);
let target = target_width(context, intrinsic, maximum);
let slots = results
.into_iter()
.map(
|RenderedChange {
rendered, record, ..
}| {
ColumnSlot::normalize(rendered.lines, record, None, target, None).0
},
)
.collect::<Vec<_>>();
let root = ColumnNode::build(&slots);
let lines = output(root.as_ref(), None);
Arc::new(ColumnState {
shape: ColumnShape::new(paths),
root,
target,
lines,
})
};
debug_assert_eq!(
state
.root
.as_ref()
.map_or(0, |root| root.measure().normalized_max(state.target)),
state.lines.max_width()
);
let rendered = Rendered::from_line_plan(state.lines.clone());
scope.store_column(state);
Ok(rendered)
}

View File

@ -0,0 +1,206 @@
mod composition_regressions {
use super::*;
fn expected_text_line(
text: &str,
properties: &AtomProperties,
break_after: Option<AtomProperties>,
) -> TapeLine {
TapeLine {
width: text.chars().count() as i64,
atoms: text
.chars()
.map(|character| TapeAtom::Text {
text: character.to_string(),
width: 1,
properties: properties.clone(),
})
.collect(),
break_after,
}
}
#[test]
fn ownership_before_wrap_and_slice_keeps_its_source_index_anchor() {
let mut line = Line::from_clusters(&[
cluster("a", 1, Some(3)),
cluster("b", 1, Some(3)),
cluster("c", 1, Some(3)),
cluster("d", 1, Some(3)),
]);
line.own_content(11, 7);
line.apply_style(Some(2));
line.apply_property_template(Some(4));
let source = Rendered::from_lines(vec![line]);
let anchored = AtomProperties {
style_ids: vec![2],
content: Some(11),
content_idx: Some(7),
owner: Some(11),
owners: vec![11],
property_template_ids: vec![3, 4],
..AtomProperties::default()
};
assert_eq!(
source.clone().into_tape(6),
LayoutTape {
style_count: 6,
lines: vec![expected_text_line("abcd", &anchored, None)],
}
);
let wrapped = wrap_rendered(source, 2, WrapMode::Char);
let expected_wrapped = LayoutTape {
style_count: 6,
lines: vec![
expected_text_line("ab", &anchored, Some(AtomProperties::default())),
expected_text_line("cd", &anchored, None),
],
};
assert_eq!(wrapped.clone().into_tape(6), expected_wrapped);
let mut selected = slice_rendered(wrapped.clone(), 1, 1);
assert_eq!(
selected.clone().into_tape(6),
LayoutTape {
style_count: 6,
lines: vec![expected_text_line("cd", &anchored, None)],
}
);
selected.lines = selected.lines.map_lines(|index, mut line| {
line.own_content(22, 99 + index as i64);
line.apply_style(Some(5));
line.apply_property_template(Some(8));
line
});
let outer = AtomProperties {
style_ids: vec![2, 5],
content: Some(11),
content_idx: Some(7),
owner: Some(22),
owners: vec![11, 22],
property_template_ids: vec![3, 4, 8],
..AtomProperties::default()
};
assert_eq!(
selected.into_tape(6),
LayoutTape {
style_count: 6,
lines: vec![expected_text_line("cd", &outer, None)],
}
);
assert_eq!(wrapped.into_tape(6), expected_wrapped);
}
fn expected_column_with_prefix(prefix_lines: usize) -> LayoutTape {
let parent_only = AtomProperties {
owner: Some(1),
owners: vec![1],
..AtomProperties::default()
};
let owned_line = |region_id, content_index| TapeLine {
width: 4,
atoms: vec![
TapeAtom::Text {
text: "x".to_owned(),
width: 1,
properties: AtomProperties {
content: Some(region_id),
content_idx: Some(content_index),
owner: Some(1),
owners: vec![region_id, 1],
..AtomProperties::default()
},
},
TapeAtom::Space {
width: 3,
properties: parent_only.clone(),
},
],
break_after: Some(AtomProperties::default()),
};
let mut lines = (0..prefix_lines)
.map(|index| owned_line(10, index as i64))
.collect::<Vec<_>>();
lines.push(TapeLine {
width: 4,
atoms: vec![TapeAtom::Space {
width: 4,
properties: AtomProperties {
content: Some(1),
content_idx: Some(prefix_lines as i64),
owner: Some(1),
owners: vec![1],
..AtomProperties::default()
},
}],
break_after: Some(AtomProperties::default()),
});
let mut suffix = owned_line(30, 0);
suffix.break_after = None;
lines.push(suffix);
LayoutTape {
style_count: 0,
lines,
}
}
#[test]
fn prefix_line_growth_and_shrink_update_only_the_inherited_suffix_content_index() {
let mut empty_suffix = nonuniform_text(20, &[0]);
let LayoutNode::Text { content, .. } = &mut empty_suffix else {
unreachable!()
};
*content = Arc::new(measured_text(vec![Vec::new()]));
let root = identified(
child_box(
1,
LayoutNode::Column {
node_id: None,
node_revision: None,
children: Arc::new(vec![
identified(nonuniform_text(10, &[1]), 2, 1),
identified(empty_suffix, 3, 1),
identified(nonuniform_text(30, &[1]), 4, 1),
]),
},
None,
),
1,
1,
);
let mut document = RetainedDocument::bootstrap(retained_document(root))
.unwrap()
.0;
let context = LayoutContext {
viewport_width: 4,
..test_context()
};
let mut frame = document.render_frame(None, None, context, None).unwrap();
assert_eq!(frame.materialize_tape(), expected_column_with_prefix(1));
for (revision, prefix_lines) in [(1, 3), (2, 1)] {
let lines = vec![
serde_json::json!({"clusters":[{
"text":"x","width":1,"cjk":false,"space":false
}]});
prefix_lines
];
let applied = document
.apply_delta(context_test_delta(vec![serde_json::json!({
"node-id":2,"expected-revision":revision,"target-revision":revision+1,
"slot-patches":[{"slot":0,"local":{"content":{"lines":lines}}}]
})]))
.unwrap();
frame = applied
.document
.render_frame(Some(&frame), Some(&applied.changes), context, None)
.unwrap();
assert_eq!(
frame.materialize_tape(),
expected_column_with_prefix(prefix_lines)
);
document = applied.document;
}
}
}

View File

@ -4,6 +4,8 @@ use std::cell::{Cell, RefCell};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use super::composition::ColumnState;
use super::line_plan::{BoxProjectionSlot, PlanChange, ProjectionState};
use super::source_topology::SourceAddress;
use super::{
BoxOverride, LayoutContext, LayoutNode, LayoutTape, LocalStep, Rendered, RetainedDocument,
@ -28,6 +30,16 @@ pub(crate) struct EvalWork {
pub(crate) topology_lookups: u64,
pub(crate) incoming_uses_visited: u64,
pub(crate) height_queries: u64,
pub(crate) child_use_nodes_created: u64,
pub(crate) child_use_nodes_visited: u64,
pub(crate) dirty_routes_written: u64,
pub(crate) dirty_routes_visited: u64,
pub(crate) column_shape_steps_visited: u64,
pub(crate) column_slots_built: u64,
pub(crate) column_slots_updated: u64,
pub(crate) column_tree_nodes_created: u64,
pub(crate) column_tree_nodes_visited: u64,
pub(crate) column_full_rebuilds: u64,
}
thread_local! {
@ -55,7 +67,17 @@ impl EvalWork {
dirty_addresses_inserted,
topology_lookups,
incoming_uses_visited,
height_queries
height_queries,
child_use_nodes_created,
child_use_nodes_visited,
dirty_routes_written,
dirty_routes_visited,
column_shape_steps_visited,
column_slots_built,
column_slots_updated,
column_tree_nodes_created,
column_tree_nodes_visited,
column_full_rebuilds
);
}
}
@ -67,7 +89,7 @@ pub(crate) fn work() -> EvalWork {
WORK.get()
}
fn record(delta: EvalWork) {
pub(super) fn record(delta: EvalWork) {
let mut work = WORK.get();
work.accumulate(delta);
WORK.set(work);
@ -123,11 +145,247 @@ struct EvalRequest {
}
#[derive(Debug)]
struct EvalRecord {
pub(super) struct EvalRecord {
source: SourceAddress,
request: EvalRequest,
rendered: Rendered,
children: BTreeMap<UseSlot, Arc<EvalRecord>>,
children: ChildUses,
column: Option<Arc<ColumnState>>,
projections: BTreeMap<BoxProjectionSlot, Arc<ProjectionState>>,
}
/// Persistent caller-local map. Path copying writes only the search path; keys
/// and current child records remain shared. There is no historical use table.
#[derive(Clone, Debug, Default)]
struct ChildUses(Option<Arc<UseNode>>);
#[derive(Debug)]
struct UseNode {
slot: Arc<UseSlot>,
value: Arc<EvalRecord>,
left: ChildUses,
right: ChildUses,
height: u32,
len: usize,
}
impl ChildUses {
fn from_fresh(entries: BTreeMap<UseSlot, Arc<EvalRecord>>) -> Self {
fn build(
entries: &mut impl Iterator<Item = (UseSlot, Arc<EvalRecord>)>,
count: usize,
) -> ChildUses {
if count == 0 {
return ChildUses::default();
}
let middle = count / 2;
let left = build(entries, middle);
let (slot, value) = entries.next().expect("fresh caller-use count");
let right = build(entries, count - middle - 1);
ChildUses::node(Arc::new(slot), value, left, right)
}
let count = entries.len();
build(&mut entries.into_iter(), count)
}
fn height(&self) -> u32 {
self.0.as_ref().map_or(0, |node| node.height)
}
fn len(&self) -> usize {
self.0.as_ref().map_or(0, |node| node.len)
}
fn node(slot: Arc<UseSlot>, value: Arc<EvalRecord>, left: Self, right: Self) -> Self {
record(EvalWork {
child_use_nodes_created: 1,
..EvalWork::default()
});
Self(Some(Arc::new(UseNode {
slot,
value,
height: 1 + left.height().max(right.height()),
len: 1 + left.len() + right.len(),
left,
right,
})))
}
fn balanced(slot: Arc<UseSlot>, value: Arc<EvalRecord>, left: Self, right: Self) -> Self {
if left.height() > right.height() + 1 {
let root = left.0.as_ref().expect("left-heavy caller tree");
if root.left.height() >= root.right.height() {
return Self::node(
Arc::clone(&root.slot),
Arc::clone(&root.value),
root.left.clone(),
Self::node(slot, value, root.right.clone(), right),
);
}
let middle = root.right.0.as_ref().expect("left-right caller tree");
return Self::node(
Arc::clone(&middle.slot),
Arc::clone(&middle.value),
Self::node(
Arc::clone(&root.slot),
Arc::clone(&root.value),
root.left.clone(),
middle.left.clone(),
),
Self::node(slot, value, middle.right.clone(), right),
);
}
if right.height() > left.height() + 1 {
let root = right.0.as_ref().expect("right-heavy caller tree");
if root.right.height() >= root.left.height() {
return Self::node(
Arc::clone(&root.slot),
Arc::clone(&root.value),
Self::node(slot, value, left, root.left.clone()),
root.right.clone(),
);
}
let middle = root.left.0.as_ref().expect("right-left caller tree");
return Self::node(
Arc::clone(&middle.slot),
Arc::clone(&middle.value),
Self::node(slot, value, left, middle.left.clone()),
Self::node(
Arc::clone(&root.slot),
Arc::clone(&root.value),
middle.right.clone(),
root.right.clone(),
),
);
}
Self::node(slot, value, left, right)
}
fn get(&self, slot: &UseSlot) -> Option<&Arc<EvalRecord>> {
let mut current = self.0.as_deref();
while let Some(node) = current {
record(EvalWork {
child_use_nodes_visited: 1,
..EvalWork::default()
});
match slot.cmp(&node.slot) {
std::cmp::Ordering::Less => current = node.left.0.as_deref(),
std::cmp::Ordering::Greater => current = node.right.0.as_deref(),
std::cmp::Ordering::Equal => return Some(&node.value),
}
}
None
}
fn inserted(&self, slot: Arc<UseSlot>, value: Arc<EvalRecord>) -> Self {
let Some(node) = &self.0 else {
return Self::node(slot, value, Self::default(), Self::default());
};
record(EvalWork {
child_use_nodes_visited: 1,
..EvalWork::default()
});
match slot.cmp(&node.slot) {
std::cmp::Ordering::Less => Self::balanced(
Arc::clone(&node.slot),
Arc::clone(&node.value),
node.left.inserted(slot, value),
node.right.clone(),
),
std::cmp::Ordering::Greater => Self::balanced(
Arc::clone(&node.slot),
Arc::clone(&node.value),
node.left.clone(),
node.right.inserted(slot, value),
),
std::cmp::Ordering::Equal => Self::node(
Arc::clone(&node.slot),
value,
node.left.clone(),
node.right.clone(),
),
}
}
#[cfg(test)]
fn iter(&self) -> impl Iterator<Item = (&UseSlot, &Arc<EvalRecord>)> {
let mut stack = Vec::new();
let mut next = self.0.as_deref();
std::iter::from_fn(move || {
while let Some(node) = next {
stack.push(node);
next = node.left.0.as_deref();
}
let node = stack.pop()?;
next = node.right.0.as_deref();
Some((node.slot.as_ref(), &node.value))
})
}
#[cfg(test)]
fn values(&self) -> impl Iterator<Item = &Arc<EvalRecord>> {
self.iter().map(|(_, value)| value)
}
#[cfg(test)]
fn keys(&self) -> impl Iterator<Item = &UseSlot> {
self.iter().map(|(slot, _)| slot)
}
}
/// Full evaluation uses a mutable fresh collector, then freezes exactly N nodes.
/// A proven sparse Column starts from an immutable current root and path-copies.
#[derive(Debug)]
enum BuildingUses {
Fresh(BTreeMap<UseSlot, Arc<EvalRecord>>),
Sparse(ChildUses),
}
impl Default for BuildingUses {
fn default() -> Self {
Self::Fresh(BTreeMap::new())
}
}
impl BuildingUses {
fn len(&self) -> usize {
match self {
Self::Fresh(entries) => entries.len(),
Self::Sparse(entries) => entries.len(),
}
}
fn insert(&mut self, slot: UseSlot, value: Arc<EvalRecord>) {
match self {
Self::Fresh(entries) => assert!(
entries.insert(slot, value).is_none(),
"unique caller use slot"
),
Self::Sparse(entries) => *entries = entries.inserted(Arc::new(slot), value),
}
}
fn finish(self) -> ChildUses {
match self {
Self::Fresh(entries) => ChildUses::from_fresh(entries),
Self::Sparse(entries) => entries,
}
}
#[cfg(test)]
fn values(&self) -> Box<dyn Iterator<Item = &Arc<EvalRecord>> + '_> {
match self {
Self::Fresh(entries) => Box::new(entries.values()),
Self::Sparse(entries) => Box::new(entries.values()),
}
}
}
#[derive(Debug)]
pub(super) struct RenderedChange {
pub(super) rendered: Rendered,
pub(super) change: Option<PlanChange>,
pub(super) record: Option<Arc<EvalRecord>>,
}
/// Owns the current document and current evaluation tree, never a previous frame.
@ -152,14 +410,18 @@ struct BuildingRecord {
source: SourceAddress,
request: EvalRequest,
previous: Option<Arc<EvalRecord>>,
children: BTreeMap<UseSlot, Arc<EvalRecord>>,
children: BuildingUses,
occurrences: BTreeMap<Vec<UseStep>, usize>,
column: Option<Arc<ColumnState>>,
projections: BTreeMap<BoxProjectionSlot, Arc<ProjectionState>>,
pending_change: Option<PlanChange>,
}
#[derive(Debug)]
struct RenderTxn {
previous: Option<Arc<EvalRecord>>,
dirty: BTreeSet<SourceAddress>,
dirty: DirtySources,
same_shape: bool,
stack: Vec<BuildingRecord>,
root: Option<Arc<EvalRecord>>,
}
@ -170,7 +432,7 @@ impl RenderTxn {
source: &SourceAddress,
path: &[UseStep],
request: EvalRequest,
) -> Option<Rendered> {
) -> Option<RenderedChange> {
record(EvalWork {
lookups: 1,
..EvalWork::default()
@ -203,15 +465,19 @@ impl RenderTxn {
)
};
if let Some(old) = previous.as_ref().filter(|old| {
old.source == *source && old.request == request && !self.dirty.contains(source)
old.source == *source && old.request == request && !self.dirty.contains_key(source)
}) {
record(EvalWork {
hits: 1,
..EvalWork::default()
});
let rendered = old.rendered.clone();
let result = RenderedChange {
rendered: old.rendered.clone(),
change: Some(PlanChange::same(&old.rendered.lines)),
record: Some(Arc::clone(old)),
};
self.attach(slot, Arc::clone(old));
return Some(rendered);
return Some(result);
}
record(EvalWork {
body_runs: 1,
@ -222,38 +488,50 @@ impl RenderTxn {
source: source.clone(),
request,
previous,
children: BTreeMap::new(),
children: BuildingUses::default(),
occurrences: BTreeMap::new(),
column: None,
projections: BTreeMap::new(),
pending_change: None,
});
None
}
fn finish(&mut self, rendered: &Result<Rendered, String>) {
fn finish(&mut self, rendered: Result<Rendered, String>) -> Result<RenderedChange, String> {
let current = self.stack.pop().expect("native evaluation builder stack");
if let Ok(rendered) = rendered {
record(EvalWork {
records_created: 1,
..EvalWork::default()
});
self.attach(
current.slot,
Arc::new(EvalRecord {
source: current.source,
request: current.request,
rendered: rendered.clone(),
children: current.children,
}),
);
}
let rendered = rendered?;
let change = current.previous.as_ref().map(|previous| {
current
.pending_change
.filter(|change| change.applies_to(&previous.rendered.lines, &rendered.lines))
.unwrap_or_else(|| {
PlanChange::replace_all(&previous.rendered.lines, &rendered.lines)
})
});
record(EvalWork {
records_created: 1,
..EvalWork::default()
});
let retained = Arc::new(EvalRecord {
source: current.source,
request: current.request,
rendered: rendered.clone(),
children: current.children.finish(),
column: current.column,
projections: current.projections,
});
self.attach(current.slot, Arc::clone(&retained));
Ok(RenderedChange {
rendered,
change,
record: Some(retained),
})
}
fn attach(&mut self, slot: UseSlot, record: Arc<EvalRecord>) {
if let Some(parent) = self.stack.last_mut() {
record_work_edge();
assert!(
parent.children.insert(slot, record).is_none(),
"unique caller use slot"
);
parent.children.insert(slot, record);
} else {
self.root = Some(record);
}
@ -377,6 +655,16 @@ impl<'a> RenderScope<'a> {
intrinsic: bool,
size_override: Option<BoxOverride>,
) -> Result<Rendered, String> {
self.render_with_change(context, intrinsic, size_override)
.map(|result| result.rendered)
}
pub(super) fn render_with_change(
&self,
context: LayoutContext,
intrinsic: bool,
size_override: Option<BoxOverride>,
) -> Result<RenderedChange, String> {
let source = self.resolve()?;
if let Some(transaction) = source.transaction {
let request = EvalRequest {
@ -399,14 +687,131 @@ impl<'a> RenderScope<'a> {
};
let rendered = super::render_node_body(&body_scope, context, intrinsic, size_override);
if let Some(transaction) = body_scope.transaction {
transaction.borrow_mut().finish(&rendered);
return transaction.borrow_mut().finish(rendered);
}
rendered
rendered.map(|rendered| RenderedChange {
rendered,
change: None,
record: None,
})
}
pub(super) fn is_retained(&self) -> bool {
self.transaction.is_some()
}
pub(super) fn publish_change(&self, change: PlanChange) {
if let Some(transaction) = self.transaction {
transaction
.borrow_mut()
.stack
.last_mut()
.expect("current evaluation")
.pending_change = Some(change);
}
}
pub(super) fn previous_projection(
&self,
slot: BoxProjectionSlot,
) -> Option<Arc<ProjectionState>> {
let transaction = self.transaction?.borrow();
let current = transaction.stack.last()?;
let previous = current
.previous
.as_ref()
.filter(|old| old.source == current.source)?;
previous.projections.get(&slot).cloned()
}
pub(super) fn store_projection(&self, slot: BoxProjectionSlot, state: Arc<ProjectionState>) {
if let Some(transaction) = self.transaction {
transaction
.borrow_mut()
.stack
.last_mut()
.expect("current evaluation")
.projections
.insert(slot, state);
}
}
pub(super) fn previous_column(&self) -> Option<Arc<ColumnState>> {
let transaction = self.transaction?.borrow();
let current = transaction.stack.last()?;
let previous = current.previous.as_ref().filter(|old| {
transaction.same_shape
&& old.source == current.source
&& old.request == current.request
&& !transaction
.dirty
.get(&current.source)
.is_some_and(|route| route.local)
})?;
previous.column.clone()
}
pub(super) fn inherit_column_uses(&self) {
let mut transaction = self.transaction.expect("retained Column").borrow_mut();
let current = transaction.stack.last_mut().expect("current evaluation");
assert_eq!(
current.children.len(),
0,
"inherit before evaluating dirty Column slots"
);
current.children = BuildingUses::Sparse(
current
.previous
.as_ref()
.expect("previous Column")
.children
.clone(),
);
}
pub(super) fn store_column(&self, state: Arc<ColumnState>) {
self.transaction
.expect("retained Column")
.borrow_mut()
.stack
.last_mut()
.expect("current evaluation")
.column = Some(state);
}
pub(super) fn dirty_route(&self, relative: &[LocalStep]) -> DirtyRoute {
let Some(transaction) = self.transaction else {
return DirtyRoute::default();
};
let mut path = self.view.address.path.to_vec();
path.extend_from_slice(relative);
record(EvalWork {
source_path_steps_copied: path.len() as u64,
dirty_routes_visited: 1,
..EvalWork::default()
});
transaction
.borrow()
.dirty
.get(&SourceAddress {
owner_id: self.view.address.owner_id,
path: path.into(),
})
.cloned()
.unwrap_or_default()
}
}
fn dirty_sources(document: &RetainedDocument, changes: &SourceChanges) -> BTreeSet<SourceAddress> {
fn mark(address: SourceAddress, dirty: &mut BTreeSet<SourceAddress>, owners: &mut Vec<u64>) {
#[derive(Clone, Debug, Default)]
pub(super) struct DirtyRoute {
pub(super) local: bool,
pub(super) next: BTreeSet<LocalStep>,
}
type DirtySources = BTreeMap<SourceAddress, DirtyRoute>;
fn dirty_sources(document: &RetainedDocument, changes: &SourceChanges) -> DirtySources {
fn mark(address: SourceAddress, dirty: &mut DirtySources, owners: &mut Vec<u64>) {
for length in (0..=address.path.len()).rev() {
record(EvalWork {
dirty_prefixes_visited: 1,
@ -417,7 +822,17 @@ fn dirty_sources(document: &RetainedDocument, changes: &SourceChanges) -> BTreeS
owner_id: address.owner_id,
path: Arc::from(&address.path[..length]),
};
if dirty.insert(prefix) {
let is_new = !dirty.contains_key(&prefix);
let route = dirty.entry(prefix).or_default();
if length == address.path.len() {
route.local = true;
} else if route.next.insert(address.path[length]) {
record(EvalWork {
dirty_routes_written: 1,
..EvalWork::default()
});
}
if is_new {
record(EvalWork {
dirty_addresses_inserted: 1,
..EvalWork::default()
@ -428,7 +843,7 @@ fn dirty_sources(document: &RetainedDocument, changes: &SourceChanges) -> BTreeS
}
}
}
let mut dirty = BTreeSet::new();
let mut dirty = BTreeMap::new();
let mut owners = Vec::new();
changes.visit_changed_slots(|owner_id, _, _, slot, _| {
record(EvalWork {
@ -500,11 +915,13 @@ impl RetainedDocument {
};
let dirty = match (reusable, changes) {
(Some(_), Some(changes)) => dirty_sources(self, changes),
_ => BTreeSet::new(),
_ => BTreeMap::new(),
};
let transaction = RefCell::new(RenderTxn {
previous: reusable.map(|previous| Arc::clone(&previous.root)),
dirty,
same_shape: reusable
.is_some_and(|previous| Arc::ptr_eq(&previous.document.topology, &self.topology)),
stack: Vec::new(),
root: None,
});

View File

@ -294,8 +294,8 @@ fn nonzero_mixed_leaf_delta_reuses_unchanged_subtree_bodies() {
"only root Box/Column, changed Row/Box/Text bodies run"
);
assert_eq!(
measured.hits, 8,
"seven unchanged rows plus the changed row's unchanged Flex"
measured.hits, 1,
"the changed row's unchanged Flex is looked up; other rows stay in shared slots"
);
assert_eq!(measured.records_created, 5);
}
@ -333,14 +333,9 @@ fn fixed_changed_subtree_reuses_bodies_across_sibling_sizes() {
measured.body_runs <= 20,
"N={size}, offset={offset}: {measured:?}"
);
assert!(measured.hits >= size as u64 - 1);
assert!(
measured.caller_edges_written >= size as u64,
"parent sibling edge writes remain N"
);
assert!(
line_work.lines_mapped >= size as u64,
"parent line maps remain explicit N work"
measured.caller_edges_written <= 64,
"only changed-row calls write caller edges: {measured:?}"
);
assert_eq!(measured.dirty_seeds, 1);
assert!(measured.dirty_addresses_inserted <= 9);
@ -357,6 +352,496 @@ fn fixed_changed_subtree_reuses_bodies_across_sibling_sizes() {
}
}
#[test]
fn sparse_column_update_does_not_visit_resolve_or_rewrite_unchanged_siblings() {
for size in [32, 128, 512] {
let document = mixed(size);
let baseline = document
.render_frame(None, None, test_context(), None)
.unwrap();
let update = document
.apply_delta(context_test_delta(vec![text_update(
row_id(size / 2) + 1,
1,
1,
"changed",
1,
)]))
.unwrap();
reset_work();
super::super::reset_resolver_lookups();
let target = update
.document
.render_frame(Some(&baseline), Some(&update.changes), test_context(), None)
.unwrap();
let measured = work();
let resolvers = super::super::resolver_lookups();
eprintln!("sparse Column N={size}, eval={measured:?}, resolvers={resolvers}");
assert_oracle(&target, test_context(), None);
assert_eq!(measured.body_runs, 5);
assert!(measured.child_slots_visited <= 12, "N={size}: {measured:?}");
assert!(
measured.caller_edges_written <= 12,
"N={size}: {measured:?}"
);
assert!(resolvers <= 12, "N={size}: resolvers={resolvers}");
assert!(Arc::ptr_eq(
owner_record(&baseline.root, row_id(size - 1)),
owner_record(&target.root, row_id(size - 1))
));
}
}
fn root_column_record(frame: &RetainedFrame) -> &Arc<EvalRecord> {
frame
.root
.children
.values()
.find(|child| child.column.is_some())
.expect("fixture root has a complete retained Column")
}
fn root_column(frame: &RetainedFrame) -> &Arc<ColumnState> {
root_column_record(frame).column.as_ref().unwrap()
}
#[test]
fn front_line_growth_and_shrink_share_column_suffix_slots_and_release_old_state() {
for size in [32, 128, 512] {
let mut document = mixed(size);
let mut frame = document
.render_frame(None, None, test_context(), None)
.unwrap();
let original_height = owner_record(&frame.root, row_id(0)).rendered.height();
for (revision, line_count) in [(1, original_height + 3), (2, 1)] {
let lines = (0..line_count)
.map(|_| {
serde_json::json!({"clusters":[{
"text":"x", "width":1, "cjk":false, "space":false
}]})
})
.collect::<Vec<_>>();
let update = document.apply_delta(context_test_delta(vec![serde_json::json!({
"node-id":row_id(0)+1, "expected-revision":revision, "target-revision":revision+1,
"slot-patches":[{"slot":1,"local":{"content":{"lines":lines}}}]
})])).unwrap();
let old_document = Arc::downgrade(&document);
let old_column = Arc::downgrade(root_column(&frame));
reset_work();
super::super::reset_resolver_lookups();
super::super::reset_line_plan_work();
let target = update
.document
.render_frame(Some(&frame), Some(&update.changes), test_context(), None)
.unwrap();
let measured = work();
let resolvers = super::super::resolver_lookups();
let line_work = super::super::line_plan_work();
assert!(measured.child_slots_visited <= 12, "N={size}: {measured:?}");
assert!(
measured.caller_edges_written <= 12,
"N={size}: {measured:?}"
);
assert!(resolvers <= 12, "N={size}: {resolvers}");
assert_eq!(measured.column_slots_updated, 1);
assert_eq!(measured.column_slots_built, 0);
assert_eq!(measured.column_full_rebuilds, 0);
assert!(
measured.child_use_nodes_created <= 32,
"N={size}: {measured:?}"
);
assert!(
measured.column_tree_nodes_created <= 12,
"N={size}: {measured:?}"
);
assert!(root_column(&frame).shares_shape(root_column(&target)));
assert!(root_column(&frame).shares_slot(root_column(&target), size - 1));
assert!(!root_column(&frame).shares_slot(root_column(&target), 0));
assert!(
Arc::ptr_eq(
root_column_record(&frame)
.children
.0
.as_ref()
.unwrap()
.right
.0
.as_ref()
.unwrap(),
root_column_record(&target)
.children
.0
.as_ref()
.unwrap()
.right
.0
.as_ref()
.unwrap(),
),
"the unchanged right half of the caller-use map must be shared"
);
assert_oracle(&target, test_context(), None);
assert_ne!(frame.materialize_tape(), target.materialize_tape());
eprintln!("front Column N={size}, lines={line_count}, eval={measured:?}, line_work={line_work:?}");
document = Arc::clone(&update.document);
drop(update);
frame = target;
assert!(
old_document.upgrade().is_none(),
"current slots must not retain the previous source document"
);
assert!(
old_column.upgrade().is_none(),
"current slots/projections must not retain old parent state"
);
}
}
}
#[test]
fn padded_box_same_parameter_updates_bound_metric_work_and_release_previous_frames() {
use super::super::VerticalAlign;
let mut excessive_work = Vec::new();
for size in [32, 128, 512] {
for align in [
VerticalAlign::Top,
VerticalAlign::Center,
VerticalAlign::Bottom,
] {
let children = (0..size)
.map(|index| {
let id = index as u64 + 2;
identified(nonuniform_text(id as i64, &[1]), id, 1)
})
.collect();
let mut root = child_box(
1,
LayoutNode::Column {
node_id: None,
node_revision: None,
children: Arc::new(children),
},
None,
);
let LayoutNode::Box {
width,
height,
padding_top,
padding_bottom,
padding_left,
padding_right,
margin_top,
margin_bottom,
margin_left,
margin_right,
foreground_style,
vertical_align,
..
} = &mut root
else {
unreachable!()
};
*width = Size::Pixels { value: 80 };
*height = if align == VerticalAlign::Top {
Size::Auto
} else {
Size::Lines {
value: size as i64 + 12,
}
};
*padding_top = 2;
*padding_bottom = 3;
*padding_left = 1;
*padding_right = 2;
*margin_top = 1;
*margin_bottom = 2;
*margin_left = 1;
*margin_right = 1;
*foreground_style = Some(0);
*vertical_align = align;
let mut input = retained_document(identified(root, 1, 1));
input.style_count = 1;
input.styles.push(
serde_json::from_value(serde_json::json!({
"mode":"set", "face":{"foreground":"red"}
}))
.unwrap(),
);
let mut document = RetainedDocument::bootstrap(input).unwrap().0;
let mut frame = document
.render_frame(None, None, test_context(), None)
.unwrap();
assert_oracle(&frame, test_context(), None);
for (revision, line_count, text) in
[(1, 1, "same-height"), (2, 4, "grow"), (3, 1, "shrink")]
{
let lines = (0..line_count)
.map(|_| {
serde_json::json!({"clusters":[{
"text":text, "width":1, "cjk":false, "space":false
}]})
})
.collect::<Vec<_>>();
let mut delta = context_test_delta(vec![serde_json::json!({
"node-id":2,"expected-revision":revision,"target-revision":revision+1,
"slot-patches":[{"slot":0,"local":{"content":{"lines":lines}}}]
})]);
delta.style_base_count = 1;
let update = document.apply_delta(delta).unwrap();
let old_document = Arc::downgrade(&document);
let old_frame = Arc::downgrade(&frame);
let old_column = Arc::downgrade(root_column(&frame));
let old_projections = frame
.root
.projections
.values()
.map(Arc::downgrade)
.collect::<Vec<_>>();
super::super::reset_line_plan_work();
reset_work();
let target = update
.document
.render_frame(Some(&frame), Some(&update.changes), test_context(), None)
.unwrap();
let line_work = super::super::line_plan_work();
let evaluated = work();
assert_eq!(evaluated.column_slots_updated, 1);
assert_eq!(evaluated.column_full_rebuilds, 0);
assert!(root_column(&frame).shares_slot(root_column(&target), size - 1));
assert_oracle(&target, test_context(), None);
assert_ne!(frame.materialize_tape(), target.materialize_tape());
eprintln!("padded Box N={size}, align={align:?}, update={text}, eval={evaluated:?}, line_work={line_work:?}");
if line_work.projection_lines_measured > 48
|| line_work.projection_fallback_lines_measured > 16
{
excessive_work.push(format!("N={size}, align={align:?}, update={text}: projection_lines_measured={}, fallback_lines_measured={}",
line_work.projection_lines_measured, line_work.projection_fallback_lines_measured));
}
document = Arc::clone(&update.document);
drop(update);
frame = target;
assert!(
old_document.upgrade().is_none(),
"framing must not retain the old document"
);
assert!(
old_frame.upgrade().is_none(),
"framing must not retain the old frame"
);
assert!(
old_column.upgrade().is_none(),
"framing must not retain the old Column state"
);
assert!(
old_projections
.iter()
.all(|state| state.upgrade().is_none()),
"framing must not retain old projection states"
);
}
}
}
assert!(
excessive_work.is_empty(),
"same-parameter padded Box work must remain bounded:\n{}",
excessive_work.join("\n")
);
}
#[test]
fn cold_column_and_child_uses_freeze_linear_nodes_and_request_changes_rebuild() {
for size in [32, 128, 512] {
let document = mixed(size);
reset_work();
let baseline = document
.render_frame(None, None, test_context(), None)
.unwrap();
let cold = work();
assert_eq!(
cold.child_use_nodes_created + 1,
cold.records_created,
"fresh maps freeze once per current caller edge: {cold:?}"
);
assert_eq!(cold.column_slots_built, size as u64);
assert_eq!(cold.column_tree_nodes_created, 2 * size as u64 - 1);
let changed_context = LayoutContext {
viewport_width: 79,
..test_context()
};
reset_work();
let target = document
.render_frame(Some(&baseline), None, changed_context, None)
.unwrap();
let changed = work();
assert_eq!(changed.column_slots_built, size as u64);
assert_eq!(changed.column_slots_updated, 0);
assert!(!root_column(&baseline).shares_shape(root_column(&target)));
assert_oracle(&target, changed_context, None);
}
}
#[test]
fn intrinsic_column_target_changes_retarget_constant_child_extra_and_count_full_work() {
let document = RetainedDocument::bootstrap(retained_document(identified(
LayoutNode::Column {
node_id: None,
node_revision: None,
children: Arc::new(vec![
identified(nonuniform_text(2, &[1, 9]), 2, 1),
identified(nonuniform_text(3, &[5, 1]), 3, 1),
]),
},
1,
1,
)))
.unwrap()
.0;
let context = LayoutContext {
viewport_width_known: false,
..test_context()
};
let baseline = document.render_frame(None, None, context, None).unwrap();
assert_eq!(
baseline
.materialize_tape()
.lines
.iter()
.map(|line| line.width)
.collect::<Vec<_>>(),
[5, 13, 5, 1]
);
let lines = [7, 9]
.into_iter()
.map(|width| {
serde_json::json!({"clusters":[{
"text":"x", "width":width, "cjk":false, "space":false
}]})
})
.collect::<Vec<_>>();
let update = document
.apply_delta(context_test_delta(vec![serde_json::json!({
"node-id":2,"expected-revision":1,"target-revision":2,
"slot-patches":[{"slot":0,"local":{"content":{"lines":lines}}}]
})]))
.unwrap();
reset_work();
let target = update
.document
.render_frame(Some(&baseline), Some(&update.changes), context, None)
.unwrap();
let measured = work();
assert_eq!(
target
.materialize_tape()
.lines
.iter()
.map(|line| line.width)
.collect::<Vec<_>>(),
[7, 9, 7, 3]
);
assert_eq!(measured.column_full_rebuilds, 1);
assert_eq!(
measured.column_slots_updated, 3,
"one raw replacement and both normalized slots"
);
assert_eq!(measured.column_slots_built, 0);
assert!(Arc::ptr_eq(
owner_record(&baseline.root, 3),
owner_record(&target.root, 3)
));
assert_oracle(&target, context, None);
}
#[test]
fn sparse_routes_keep_nested_literal_paths_zero_width_lines_and_node_ref_boundaries() {
let column = |children| LayoutNode::Column {
node_id: None,
node_revision: None,
children: Arc::new(children),
};
let document = RetainedDocument::bootstrap(retained_document(identified(
child_box(
1,
column(vec![
column(vec![
identified(child_box(2, nonuniform_text(12, &[1]), None), 2, 1),
column(vec![nonuniform_text(99, &[0])]),
identified(nonuniform_text(3, &[1]), 3, 1),
]),
identified(
column(vec![identified(
child_box(5, nonuniform_text(15, &[1]), None),
5,
1,
)]),
4,
1,
),
]),
None,
),
1,
1,
)))
.unwrap()
.0;
let baseline = document
.render_frame(None, None, test_context(), None)
.unwrap();
assert_oracle(&baseline, test_context(), None);
let update = document
.apply_delta(context_test_delta(vec![
text_update(2, 1, 1, "left", 2),
text_update(5, 1, 1, "right", 3),
]))
.unwrap();
reset_work();
let target = update
.document
.render_frame(Some(&baseline), Some(&update.changes), test_context(), None)
.unwrap();
let measured = work();
assert_eq!(
measured.column_slots_updated, 3,
"two outer slots and one NodeRef Column slot"
);
assert_eq!(measured.column_slots_built, 0);
assert_eq!(measured.column_full_rebuilds, 0);
assert!(root_column(&baseline).shares_shape(root_column(&target)));
assert!(root_column(&baseline).shares_slot(root_column(&target), 1));
assert!(root_column(&baseline).shares_slot(root_column(&target), 2));
let paths = root_column_record(&target)
.children
.keys()
.map(|slot| slot.path.clone())
.collect::<Vec<_>>();
assert_eq!(
paths,
vec![
vec![
UseStep::Child(LocalStep::ColumnChild(0)),
UseStep::Child(LocalStep::ColumnChild(0))
],
vec![
UseStep::Child(LocalStep::ColumnChild(0)),
UseStep::Child(LocalStep::ColumnChild(1)),
UseStep::Child(LocalStep::ColumnChild(0))
],
vec![
UseStep::Child(LocalStep::ColumnChild(0)),
UseStep::Child(LocalStep::ColumnChild(2))
],
vec![UseStep::Child(LocalStep::ColumnChild(1))],
]
);
assert!(
owner_record(&target.root, 4).column.is_some(),
"NodeRef Column remains its own caller"
);
assert_oracle(&target, test_context(), None);
}
#[test]
fn parent_and_child_source_updates_share_only_clean_descendants() {
let document = mixed(4);
@ -478,7 +963,8 @@ fn render_request(
) -> Arc<RetainedFrame> {
let transaction = RefCell::new(RenderTxn {
previous: previous.map(|frame| Arc::clone(&frame.root)),
dirty: BTreeSet::new(),
dirty: BTreeMap::new(),
same_shape: true,
stack: Vec::new(),
root: None,
});
@ -805,7 +1291,14 @@ fn repeated_calls(
};
let transaction = RefCell::new(RenderTxn {
previous: previous.map(|frame| Arc::clone(&frame.root)),
dirty: BTreeSet::from([source.clone()]),
dirty: BTreeMap::from([(
source.clone(),
DirtyRoute {
local: true,
next: BTreeSet::new(),
},
)]),
same_shape: true,
stack: Vec::new(),
root: None,
});
@ -854,7 +1347,8 @@ fn repeated_calls(
.collect();
transaction
.borrow_mut()
.finish(&Ok(super::super::stack_vertical(parts)));
.finish(Ok(super::super::stack_vertical(parts)))
.unwrap();
Arc::new(RetainedFrame {
document: Arc::clone(document),
root: transaction.into_inner().root.unwrap(),
@ -874,7 +1368,7 @@ fn repeated_callsite_uses_have_local_occurrences_and_prune_unused_contexts() {
let keys = baseline.root.children.keys().collect::<Vec<_>>();
assert_eq!(keys[0].path, keys[1].path);
assert_eq!((keys[0].occurrence, keys[1].occurrence), (0, 1));
let removed = Arc::downgrade(&baseline.root.children[keys[1]]);
let removed = Arc::downgrade(baseline.root.children.get(keys[1]).unwrap());
reset_work();
let one = repeated_calls(&document, Some(&baseline), &[10]);
assert_eq!(work().hits, 1);
@ -971,7 +1465,14 @@ fn unwinding_discards_current_builder_records_without_mutating_previous_frame()
let source = baseline.root.source.clone();
let transaction = RefCell::new(RenderTxn {
previous: Some(Arc::clone(&baseline.root)),
dirty: BTreeSet::from([source.clone()]),
dirty: BTreeMap::from([(
source.clone(),
DirtyRoute {
local: true,
next: BTreeSet::new(),
},
)]),
same_shape: true,
stack: Vec::new(),
root: None,
});

View File

@ -25,8 +25,8 @@ pub(crate) fn atom_plan_work() -> AtomPlanWork {
#[path = "line_plan.rs"]
mod line_plan;
use line_plan::LinePlan;
pub(crate) use line_plan::LinePlanWork;
use line_plan::{BoxProjectionSlot, FrameSpec, LineOp, LinePlan, PlanChange, ProjectionState};
pub(crate) fn reset_line_plan_work() {
line_plan::reset_work();
@ -36,6 +36,8 @@ pub(crate) fn line_plan_work() -> LinePlanWork {
line_plan::work()
}
#[path = "composition.rs"]
mod composition;
#[path = "evaluation.rs"]
mod evaluation;
pub(crate) use evaluation::{EvalWork, RetainedFrame};
@ -2203,7 +2205,7 @@ impl Rendered {
}
fn first_width(&self) -> i64 {
self.lines.first().map_or(0, |line| line.width)
self.lines.first_width()
}
fn max_width(&self) -> i64 {
@ -2214,27 +2216,21 @@ impl Rendered {
if wrap_mode == WrapMode::None {
return self.max_width();
}
self.lines
.iter()
.map(|line| line.atoms.min_content_width())
.max()
.unwrap_or(0)
self.lines.min_content_width()
}
fn height(&self) -> i64 {
self.lines.height()
}
#[cfg(test)]
fn apply_scroll_window(&mut self, region_id: i64) {
self.lines = self
.lines
.map_lines(|_, mut line| {
line.atoms = line.atoms.apply_scroll_window(region_id);
line
})
.map_breaks(|properties| {
properties.scroll_window = Some(region_id);
});
self.lines = ProjectionState::new(
self.lines.clone(),
Arc::from([LineOp::ScrollWindow(region_id)]),
)
.plan()
.clone();
}
fn into_tape(self, style_count: u32) -> LayoutTape {
@ -2244,29 +2240,32 @@ impl Rendered {
lines: self
.lines
.iter_with_breaks()
.map(|(line, break_after)| TapeLine {
width: line.width,
atoms: line
.atoms
.to_vec()
.into_iter()
.map(|atom| match atom {
Atom::Text {
text,
width,
properties,
..
} => TapeAtom::Text {
text,
width,
properties,
},
Atom::Space { width, properties } => {
TapeAtom::Space { width, properties }
}
})
.collect(),
break_after: break_after.cloned(),
.map(|(view, break_after)| {
let line = view.materialize();
TapeLine {
width: view.width(),
atoms: line
.atoms
.to_vec()
.into_iter()
.map(|atom| match atom {
Atom::Text {
text,
width,
properties,
..
} => TapeAtom::Text {
text,
width,
properties,
},
Atom::Space { width, properties } => {
TapeAtom::Space { width, properties }
}
})
.collect(),
break_after: break_after.map(|view| view.materialize().into_owned()),
}
})
.collect(),
}
@ -5472,13 +5471,16 @@ fn wrap_rendered(rendered: Rendered, max_width: i64, mode: WrapMode) -> Rendered
let mut lines = LinePlan::default();
let mut joining_break = AtomProperties::default();
for (line, break_after) in rendered.lines.iter_with_breaks() {
let pieces = LinePlan::from_lines(wrap_rendered_line(line, max_width, mode));
let pieces = LinePlan::from_lines(wrap_rendered_line(&line.materialize(), max_width, mode));
lines = lines.concat_with_break(&pieces, joining_break);
joining_break = break_after.cloned().unwrap_or_default();
joining_break = break_after
.map(|view| view.materialize().into_owned())
.unwrap_or_default();
}
Rendered { lines }
}
#[cfg(test)]
fn vertical_align(lines: LinePlan, height: usize, align: VerticalAlign, width: i64) -> LinePlan {
let lines = lines.slice(0..lines.len().min(height));
if lines.len() >= height {
@ -6338,7 +6340,7 @@ fn concat_horizontal_sized(parts: Vec<(Rendered, i64)>, target_height: i64) -> R
let mut line = Line::default();
for (lines, width) in &mut parts {
if let Some(part) = lines.next() {
line.append(part);
line.append(&part.materialize());
} else {
line.push_space(*width);
}
@ -6919,6 +6921,47 @@ fn render_node_with_override(
RenderScope::uncached(node, resolver).render(context, intrinsic, size_override)
}
fn project_box_lines(
scope: &RenderScope<'_>,
slot: BoxProjectionSlot,
input: LinePlan,
change: &mut Option<PlanChange>,
ops: Vec<LineOp>,
) -> LinePlan {
let ops: Arc<[LineOp]> = ops.into();
let state = if let Some(previous) = scope.previous_projection(slot) {
let update = previous.update(input, change.as_ref(), ops);
*change = Some(update.change);
update.state
} else {
*change = None;
ProjectionState::new(input, ops)
};
let lines = state.plan().clone();
scope.store_projection(slot, state);
lines
}
fn frame_box_lines(
scope: &RenderScope<'_>,
slot: BoxProjectionSlot,
input: LinePlan,
change: &mut Option<PlanChange>,
frame: FrameSpec,
) -> LinePlan {
let state = if let Some(previous) = scope.previous_projection(slot) {
let update = previous.update_frame(input, change.as_ref(), frame);
*change = Some(update.change);
update.state
} else {
*change = None;
ProjectionState::new_frame(input, frame)
};
let lines = state.plan().clone();
scope.store_projection(slot, state);
lines
}
fn render_node_body(
scope: &RenderScope<'_>,
context: LayoutContext,
@ -7075,6 +7118,7 @@ fn render_node_body(
&& *vertical == VerticalAlign::Top;
let mut windowed_child_start = None;
let mut windowed_child_height = None;
let mut child_change = None;
let mut child_rendered = if let Some(child) = child {
let child_scope = scope.child(LocalStep::BoxChild, child);
let child_context = LayoutContext {
@ -7109,14 +7153,22 @@ fn render_node_body(
.expect("exact height checked above")?,
)
} else {
Some(child_scope.render(
let evaluated = child_scope.render_with_change(
child_context,
intrinsic || intrinsic_child,
None,
)?)
)?;
child_change = evaluated.change;
Some(evaluated.rendered)
}
} else {
Some(child_scope.render(child_context, intrinsic || intrinsic_child, None)?)
let evaluated = child_scope.render_with_change(
child_context,
intrinsic || intrinsic_child,
None,
)?;
child_change = evaluated.change;
Some(evaluated.rendered)
}
} else {
None
@ -7209,18 +7261,25 @@ fn render_node_body(
&& border_top_style.is_none()
&& border_bottom_style.is_none()
&& child_rendered.as_ref().is_some_and(|rendered| {
rendered
.lines
.iter()
.all(|line| !line.atoms.is_empty() && line.width == content_width)
rendered.lines.all_nonempty() && rendered.lines.uniform_width(content_width)
});
if transparent_preformatted {
let mut rendered = child_rendered.take().expect("validated child");
rendered.lines = rendered.lines.map_lines(|index, mut line| {
line.own_content(*region_id, index as i64);
line.apply_property_template(*surface_template_id);
line
});
let mut ops = vec![LineOp::OwnContent {
region: *region_id,
start: 0,
}];
ops.extend(surface_template_id.map(LineOp::Template));
rendered.lines = project_box_lines(
scope,
BoxProjectionSlot::TransparentContent,
rendered.lines,
&mut child_change,
ops,
);
if let Some(change) = child_change {
scope.publish_change(change);
}
return Ok(rendered);
}
@ -7235,30 +7294,35 @@ fn render_node_body(
}
}
LinePlan::from_lines(content_lines)
.map_lines(|_, line| line.padded(content_width, *text_align))
} else {
let rendered = child_rendered.expect("validated child");
let uniform_width = rendered
.lines
.iter()
.all(|line| line.width == content_width);
let preserve_exact_width = *content_width_exact && *wrap_mode == WrapMode::None;
let uniform_width = rendered.lines.uniform_width(content_width);
let rendered = if *wrap_mode != WrapMode::None && !uniform_width {
child_change = None;
wrap_rendered(rendered, content_width, *wrap_mode)
} else {
rendered
};
if preserve_exact_width {
rendered.lines
} else {
rendered
.lines
.map_lines(|_, line| line.padded(content_width, *text_align))
}
rendered.lines
};
// Ordinary Box formatting historically rebuilds default breaks;
// the transparent preformatted branch above preserves child breaks.
formatted = formatted.clear_breaks();
let mut change = child_change;
let mut pad_ops = Vec::new();
if content.is_some() || !(*content_width_exact && *wrap_mode == WrapMode::None) {
pad_ops.push(LineOp::PadTo {
width: content_width,
align: *text_align,
});
}
pad_ops.push(LineOp::ClearBreaks);
formatted = project_box_lines(
scope,
BoxProjectionSlot::ContentPad,
formatted,
&mut change,
pad_ops,
);
let text_height = windowed_child_height.unwrap_or(formatted.len() as i64);
let minimum_height = resolve_height(
min_height,
@ -7315,6 +7379,9 @@ fn render_node_body(
if let Some(start) = windowed_child_start {
content_index_start = start;
} else if formatted.len() > content_height as usize {
// Clipping changes operation origins. Until the exact old/new window
// anchors are available, the next stage takes its counted fallback.
change = None;
match overflow {
Overflow::Scroll => {
let max_offset = formatted.len() - content_height as usize;
@ -7333,113 +7400,206 @@ fn render_node_body(
}
}
}
formatted = formatted.map_lines(|index, mut line| {
line.own_content(*region_id, content_index_start + index as i64);
line
});
let lines =
vertical_align(formatted, content_height as usize, *vertical, content_width)
.map_lines(|_, line| {
if simple_scroll_rendered {
line
} else {
line.collapse_whitespace_content(content_width, *region_id)
}
});
formatted = project_box_lines(
scope,
BoxProjectionSlot::ContentOwn,
formatted,
&mut change,
vec![LineOp::OwnContent {
region: *region_id,
start: content_index_start,
}],
);
if formatted.len() > content_height as usize {
change = None;
formatted = formatted.slice(0..content_height as usize);
}
let remaining = content_height as usize - formatted.len();
let top = match vertical {
VerticalAlign::Top => 0,
VerticalAlign::Bottom => remaining,
VerticalAlign::Center => remaining / 2,
};
let lines = frame_box_lines(
scope,
BoxProjectionSlot::VerticalFrame,
formatted,
&mut change,
FrameSpec {
prefix: top,
suffix: remaining - top,
width: content_width,
prefix_properties: Arc::new(AtomProperties::default()),
suffix_properties: Arc::new(AtomProperties::default()),
},
);
let collapse_ops = if simple_scroll_rendered {
Vec::new()
} else {
vec![LineOp::CollapseWhitespace {
width: content_width,
region: *region_id,
}]
};
let lines = project_box_lines(
scope,
BoxProjectionSlot::ContentCollapse,
lines,
&mut change,
collapse_ops,
);
let mut padded = LinePlan::from_lines((0..*padding_top).map(|_| {
Line::blank_with_properties(
content_width,
region_properties(RegionRole::PaddingTop, *region_id, None),
let mut padded = frame_box_lines(
scope,
BoxProjectionSlot::PaddingFrame,
lines,
&mut change,
FrameSpec {
prefix: *padding_top as usize,
suffix: *padding_bottom as usize,
width: content_width,
prefix_properties: region_properties(RegionRole::PaddingTop, *region_id, None)
.into(),
suffix_properties: region_properties(
RegionRole::PaddingBottom,
*region_id,
None,
)
.into(),
},
);
let mut edge_ops = vec![LineOp::EdgeSpaces {
left: *padding_left,
left_properties: region_properties(RegionRole::PaddingLeft, *region_id, None)
.into(),
right: *padding_right,
right_properties: region_properties(RegionRole::PaddingRight, *region_id, None)
.into(),
}];
edge_ops.extend(
[*typography_style, *foreground_style, *background_style]
.into_iter()
.flatten()
.map(LineOp::Style),
);
edge_ops.push(LineOp::EdgeSpaces {
left: *border_left,
left_properties: region_properties(
RegionRole::BorderLeft,
*region_id,
*border_left_style,
)
.into(),
right: *border_right,
right_properties: region_properties(
RegionRole::BorderRight,
*region_id,
*border_right_style,
)
.into(),
});
padded = project_box_lines(
scope,
BoxProjectionSlot::PaddingEdges,
padded,
&mut change,
edge_ops,
);
let mut boundary_ops = Vec::new();
boundary_ops.extend(border_top_style.map(|style| LineOp::FirstStyleRole {
style,
region: *region_id,
}));
padded = padded.concat(&lines);
padded = padded.concat(&LinePlan::from_lines((0..*padding_bottom).map(|_| {
Line::blank_with_properties(
content_width,
region_properties(RegionRole::PaddingBottom, *region_id, None),
)
})));
padded = padded.map_lines(|_, mut line| {
line.prepend_space_with_properties(
*padding_left,
region_properties(RegionRole::PaddingLeft, *region_id, None),
);
line.push_space_with_properties(
*padding_right,
region_properties(RegionRole::PaddingRight, *region_id, None),
);
line.apply_style(*typography_style);
line.apply_style(*foreground_style);
line.apply_style(*background_style);
line.prepend_space_with_properties(
*border_left,
region_properties(RegionRole::BorderLeft, *region_id, *border_left_style),
);
line.push_space_with_properties(
*border_right,
region_properties(RegionRole::BorderRight, *region_id, *border_right_style),
);
line
});
if border_top_style.is_some() && !padded.is_empty() {
padded = padded.update_line(0, |mut line| {
line.apply_style(*border_top_style);
line.apply_role(RegionRole::BorderTop, *region_id);
line
});
}
if border_bottom_style.is_some() && !padded.is_empty() {
padded = padded.update_line(padded.len() - 1, |mut line| {
line.apply_style(*border_bottom_style);
line.apply_role(RegionRole::BorderBottom, *region_id);
line
});
}
padded = padded.map_lines(|_, mut line| {
line.apply_property_template(*surface_template_id);
line
});
padded = padded.map_lines(|_, mut line| {
line.prepend_space_with_properties(
*margin_left,
region_properties(RegionRole::MarginLeft, *region_id, None),
);
line.push_space_with_properties(
*margin_right,
region_properties(RegionRole::MarginRight, *region_id, None),
);
line
});
boundary_ops.extend(border_bottom_style.map(|style| LineOp::LastStyleRole {
style,
region: *region_id,
}));
padded = project_box_lines(
scope,
BoxProjectionSlot::BoundaryBorders,
padded,
&mut change,
boundary_ops,
);
padded = project_box_lines(
scope,
BoxProjectionSlot::SurfaceTemplate,
padded,
&mut change,
surface_template_id
.map(LineOp::Template)
.into_iter()
.collect(),
);
padded = project_box_lines(
scope,
BoxProjectionSlot::MarginEdges,
padded,
&mut change,
vec![LineOp::EdgeSpaces {
left: *margin_left,
left_properties: region_properties(RegionRole::MarginLeft, *region_id, None)
.into(),
right: *margin_right,
right_properties: region_properties(RegionRole::MarginRight, *region_id, None)
.into(),
}],
);
let total_width = content_width + side_width;
let mut output = LinePlan::from_lines((0..*margin_top).map(|_| {
Line::blank_with_properties(
total_width,
region_properties(RegionRole::MarginTop, *region_id, None),
)
}));
output = output.concat(&padded);
output = output.concat(&LinePlan::from_lines((0..*margin_bottom).map(|_| {
Line::blank_with_properties(
total_width,
region_properties(RegionRole::MarginBottom, *region_id, None),
)
})));
let output = frame_box_lines(
scope,
BoxProjectionSlot::MarginFrame,
padded,
&mut change,
FrameSpec {
prefix: *margin_top as usize,
suffix: *margin_bottom as usize,
width: total_width,
prefix_properties: region_properties(RegionRole::MarginTop, *region_id, None)
.into(),
suffix_properties: region_properties(
RegionRole::MarginBottom,
*region_id,
None,
)
.into(),
},
);
let mut rendered = Rendered::from_line_plan(output);
if !overflow_lines.is_empty() {
let left_space = margin_left + border_left + padding_left;
let right_space = padding_right + border_right + margin_right;
overflow_lines = overflow_lines.map_lines(|_, mut line| {
line.apply_style(*foreground_style);
line.prepend_space(left_space);
line.push_space(right_space);
line
let mut ops = foreground_style
.map(LineOp::Style)
.into_iter()
.collect::<Vec<_>>();
ops.push(LineOp::EdgeSpaces {
left: left_space,
left_properties: Arc::new(AtomProperties::default()),
right: right_space,
right_properties: Arc::new(AtomProperties::default()),
});
overflow_lines = project_box_lines(
scope,
BoxProjectionSlot::OverflowEdges,
overflow_lines,
&mut None,
ops,
);
rendered = stack_vertical(vec![rendered, Rendered::from_line_plan(overflow_lines)]);
change = None;
}
if *overflow == Overflow::Scroll && text_height > content_height {
rendered.apply_scroll_window(*region_id);
rendered.lines = project_box_lines(
scope,
BoxProjectionSlot::ScrollWindow,
rendered.lines,
&mut change,
vec![LineOp::ScrollWindow(*region_id)],
);
}
if let Some(change) = change {
scope.publish_change(change);
}
Ok(rendered)
}
@ -7488,6 +7648,9 @@ fn render_node_body(
Ok(concat_horizontal_sized(parts, 0))
}
LayoutNode::Column { children, .. } => {
if scope.is_retained() {
return composition::render_column(scope, children, context, intrinsic);
}
let rendered = column_leaves(scope, children)
.map(|child| child.render(context, intrinsic, None))
.collect::<Result<Vec<_>, _>>()?;
@ -7576,6 +7739,8 @@ fn column_leaves<'a>(
mod tests {
use super::*;
include!("composition_tests.rs");
include!("source_topology_tests.rs");
#[test]
@ -8085,14 +8250,14 @@ mod tests {
}),
)
.expect("typed column render");
assert_eq!(rendered.lines.get(0).unwrap().width, 100);
assert_eq!(rendered.lines.get(0).unwrap().width(), 100);
assert!(
matches!(
rendered.lines.get(0).unwrap().atoms.first(),
rendered.lines.get(0).unwrap().materialize().atoms.first(),
Some(Atom::Space { width: 45, .. })
),
"{:?}",
rendered.lines.get(0).unwrap().atoms
rendered.lines.get(0).unwrap().materialize().atoms
);
}
@ -8595,10 +8760,16 @@ mod tests {
let mut stacked = stack_vertical(vec![first, second]);
assert_eq!(stacked.lines.len() - 1, 2);
assert_eq!(stacked.lines.break_after(0).unwrap().owner, Some(11));
assert_eq!(
stacked.lines.break_after(1),
Some(&AtomProperties::default())
stacked.lines.break_after(0).unwrap().materialize().owner,
Some(11)
);
assert_eq!(
stacked
.lines
.break_after(1)
.map(|view| view.materialize().into_owned()),
Some(AtomProperties::default())
);
stacked.apply_scroll_window(9);
@ -8606,9 +8777,10 @@ mod tests {
.lines
.iter_with_breaks()
.filter_map(|(_, properties)| properties)
.all(|properties| { properties.scroll_window == Some(9) }));
.all(|properties| { properties.materialize().scroll_window == Some(9) }));
assert!(stacked.lines.iter().all(|line| {
line.atoms
line.materialize()
.atoms
.to_vec()
.iter()
.all(|atom| atom.properties().scroll_window == Some(9))

295
native/src/line_metrics.rs Normal file
View File

@ -0,0 +1,295 @@
//! Persistent numeric summaries. No node retains a line, document, or old projection.
use std::ops::Range;
use std::sync::Arc;
use super::{record, Line, LinePlanWork};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct LineMetric {
pub width: i64,
pub chars: u64,
pub min_content: i64,
pub nonempty: bool,
pub whitespace: bool,
pub noncontent: bool,
}
impl LineMetric {
pub fn line(line: &Line) -> Self {
Self {
width: line.width,
chars: line.atoms.char_count(),
min_content: line.atoms.min_content_width(),
nonempty: !line.atoms.is_empty(),
whitespace: line.whitespace_only(),
noncontent: line.has_noncontent_properties(),
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(super) struct Summary {
pub count: usize,
pub chars: u64,
pub width_sum: i64,
pub first_width: i64,
pub min_width: i64,
pub max_width: i64,
pub min_content: i64,
pub all_nonempty: bool,
}
impl Summary {
fn line(line: LineMetric) -> Self {
Self {
count: 1,
chars: line.chars,
width_sum: line.width,
first_width: line.width,
min_width: line.width,
max_width: line.width,
min_content: line.min_content,
all_nonempty: line.nonempty,
}
}
fn combine(self, other: Self) -> Self {
if self.count == 0 {
return other;
}
if other.count == 0 {
return self;
}
Self {
count: self
.count
.checked_add(other.count)
.expect("validated line count"),
chars: self
.chars
.checked_add(other.chars)
.expect("validated character count"),
width_sum: self
.width_sum
.checked_add(other.width_sum)
.expect("validated line widths"),
first_width: self.first_width,
min_width: self.min_width.min(other.min_width),
max_width: self.max_width.max(other.max_width),
min_content: self.min_content.max(other.min_content),
all_nonempty: self.all_nonempty && other.all_nonempty,
}
}
}
#[derive(Clone, Debug, Default)]
pub(super) struct Metrics(Option<Arc<Node>>);
#[derive(Debug)]
struct Node {
summary: Summary,
height: u32,
kind: Kind,
}
#[derive(Debug)]
enum Kind {
Leaf(Box<[LineMetric]>),
Branch(Metrics, Metrics),
}
impl Metrics {
pub fn from_iter(lines: impl IntoIterator<Item = LineMetric>) -> Self {
let mut lines = lines.into_iter();
let mut blocks = Vec::new();
loop {
let block = lines.by_ref().take(16).collect::<Vec<_>>();
if block.is_empty() {
break;
}
blocks.push(Self::leaf(block.into_boxed_slice()));
}
fn build(lines: &[Metrics]) -> Metrics {
match lines {
[] => Metrics::default(),
[line] => line.clone(),
_ => Metrics::branch(
build(&lines[..lines.len() / 2]),
build(&lines[lines.len() / 2..]),
),
}
}
build(&blocks)
}
fn line(line: LineMetric) -> Self {
Self::leaf(Box::new([line]))
}
fn leaf(lines: Box<[LineMetric]>) -> Self {
record(LinePlanWork {
metric_nodes_created: 1,
metric_entries_written: lines.len() as u64,
..LinePlanWork::default()
});
let summary = lines
.iter()
.copied()
.fold(Summary::default(), |summary, line| {
summary.combine(Summary::line(line))
});
Self(Some(Arc::new(Node {
summary,
height: 1,
kind: Kind::Leaf(lines),
})))
}
fn height(&self) -> u32 {
self.0.as_ref().map_or(0, |node| node.height)
}
pub fn summary(&self) -> Summary {
self.0
.as_ref()
.map_or(Summary::default(), |node| node.summary)
}
fn branch(left: Self, right: Self) -> Self {
if left.0.is_none() {
return right;
}
if right.0.is_none() {
return left;
}
record(LinePlanWork {
metric_nodes_created: 1,
..LinePlanWork::default()
});
Self(Some(Arc::new(Node {
summary: left.summary().combine(right.summary()),
height: left.height().max(right.height()) + 1,
kind: Kind::Branch(left, right),
})))
}
fn children(&self) -> (&Self, &Self) {
let Kind::Branch(left, right) = &self.0.as_ref().expect("nonempty metrics").kind else {
unreachable!("metric balance branch")
};
(left, right)
}
fn balanced(left: Self, right: Self) -> Self {
if left.height() > right.height() + 1 {
let (ll, lr) = left.children();
if ll.height() >= lr.height() {
Self::branch(ll.clone(), Self::branch(lr.clone(), right))
} else {
let (lrl, lrr) = lr.children();
Self::branch(
Self::branch(ll.clone(), lrl.clone()),
Self::branch(lrr.clone(), right),
)
}
} else if right.height() > left.height() + 1 {
let (rl, rr) = right.children();
if rr.height() >= rl.height() {
Self::branch(Self::branch(left, rl.clone()), rr.clone())
} else {
let (rll, rlr) = rl.children();
Self::branch(
Self::branch(left, rll.clone()),
Self::branch(rlr.clone(), rr.clone()),
)
}
} else {
Self::branch(left, right)
}
}
pub fn concat(&self, other: &Self) -> Self {
if self.height() > other.height() + 1 {
let (left, right) = self.children();
Self::balanced(left.clone(), right.concat(other))
} else if other.height() > self.height() + 1 {
let (left, right) = other.children();
Self::balanced(self.concat(left), right.clone())
} else {
Self::branch(self.clone(), other.clone())
}
}
#[cfg(test)]
pub fn get(&self, index: usize) -> Option<LineMetric> {
let node = self.0.as_ref()?;
record(LinePlanWork {
metric_nodes_visited: 1,
..LinePlanWork::default()
});
match &node.kind {
Kind::Leaf(lines) => lines.get(index).copied(),
Kind::Branch(left, right) => {
if index < left.summary().count {
left.get(index)
} else {
right.get(index - left.summary().count)
}
}
}
}
pub fn slice(&self, range: Range<usize>) -> Self {
assert!(range.start <= range.end && range.end <= self.summary().count);
if range.is_empty() {
return Self::default();
}
if range.start == 0 && range.end == self.summary().count {
return self.clone();
}
record(LinePlanWork {
metric_nodes_visited: 1,
..LinePlanWork::default()
});
if let Kind::Leaf(lines) = &self.0.as_ref().expect("nonempty metrics").kind {
return Self::leaf(lines[range].into());
}
let (left, right) = self.children();
let middle = left.summary().count;
if range.end <= middle {
left.slice(range)
} else if range.start >= middle {
right.slice(range.start - middle..range.end - middle)
} else {
left.slice(range.start..middle)
.concat(&right.slice(0..range.end - middle))
}
}
pub fn replace(&self, index: usize, line: LineMetric) -> Self {
self.slice(0..index)
.concat(&Self::line(line))
.concat(&self.slice(index + 1..self.summary().count))
}
pub fn iter(&self) -> impl Iterator<Item = LineMetric> + '_ {
let mut stack = self.0.as_deref().into_iter().collect::<Vec<_>>();
let mut leaf = [].iter();
std::iter::from_fn(move || loop {
if let Some(line) = leaf.next() {
return Some(*line);
}
let node = stack.pop()?;
record(LinePlanWork {
metric_nodes_visited: 1,
..LinePlanWork::default()
});
match &node.kind {
Kind::Leaf(lines) => leaf = lines.iter(),
Kind::Branch(left, right) => {
stack.push(right.0.as_deref().expect("nonempty metric branch"));
stack.push(left.0.as_deref().expect("nonempty metric branch"));
}
}
})
}
}

169
native/src/line_ops.rs Normal file
View File

@ -0,0 +1,169 @@
use super::super::{AtomProperties, HorizontalAlign, Line, RegionRole};
use super::line_metrics::LineMetric;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(in crate::layout) enum BoxProjectionSlot {
ContentPad,
ContentOwn,
ContentCollapse,
VerticalFrame,
PaddingFrame,
MarginFrame,
PaddingEdges,
BoundaryBorders,
SurfaceTemplate,
MarginEdges,
OverflowEdges,
ScrollWindow,
TransparentContent,
}
/// Ordered, cardinality-preserving operations. Each index is in this stage's input.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(in crate::layout) enum LineOp {
PadTo {
width: i64,
align: HorizontalAlign,
},
AppendSpace(i64),
OwnContent {
region: i64,
start: i64,
},
CollapseWhitespace {
width: i64,
region: i64,
},
EdgeSpaces {
left: i64,
left_properties: Arc<AtomProperties>,
right: i64,
right_properties: Arc<AtomProperties>,
},
Style(u32),
Template(u32),
ClearBreaks,
ScrollWindow(i64),
FirstStyleRole {
style: u32,
region: i64,
},
LastStyleRole {
style: u32,
region: i64,
},
}
impl LineOp {
pub fn apply(&self, mut line: Line, index: usize, length: usize) -> Line {
match self {
Self::PadTo { width, align } => line = line.padded(*width, *align),
Self::AppendSpace(width) => line.push_space(*width),
Self::OwnContent { region, start } => line.own_content(*region, *start + index as i64),
Self::CollapseWhitespace { width, region } => {
line = line.collapse_whitespace_content(*width, *region)
}
Self::EdgeSpaces {
left,
left_properties,
right,
right_properties,
} => {
line.prepend_space_with_properties(*left, left_properties.as_ref().clone());
line.push_space_with_properties(*right, right_properties.as_ref().clone());
}
Self::Style(style) => line.apply_style(Some(*style)),
Self::Template(template) => line.apply_property_template(Some(*template)),
Self::ScrollWindow(region) => line.atoms = line.atoms.apply_scroll_window(*region),
Self::FirstStyleRole { style, region } if index == 0 => {
line.apply_style(Some(*style));
line.apply_role(RegionRole::BorderTop, *region);
}
Self::LastStyleRole { style, region } if index + 1 == length => {
line.apply_style(Some(*style));
line.apply_role(RegionRole::BorderBottom, *region);
}
Self::FirstStyleRole { .. } | Self::LastStyleRole { .. } | Self::ClearBreaks => {}
}
line
}
pub(super) fn metric(&self, mut line: LineMetric, index: usize, length: usize) -> LineMetric {
fn space(line: &mut LineMetric, width: i64, properties: Option<&AtomProperties>) {
if width <= 0 {
return;
}
line.width += width;
line.chars += 1;
if !line.nonempty {
line.whitespace = true;
}
line.nonempty = true;
line.noncontent |= properties.is_some_and(|p| {
!p.style_ids.is_empty()
|| !p.roles.is_empty()
|| !p.property_template_ids.is_empty()
});
}
match self {
Self::PadTo { width, align } => {
let remaining = (*width - line.width).max(0);
let left = match align {
HorizontalAlign::Left => 0,
HorizontalAlign::Right => remaining,
HorizontalAlign::Center => remaining / 2,
};
space(&mut line, left, None);
space(&mut line, remaining - left, None);
}
Self::AppendSpace(width) => space(&mut line, *width, None),
Self::EdgeSpaces {
left,
left_properties,
right,
right_properties,
} => {
space(&mut line, *left, Some(left_properties));
space(&mut line, *right, Some(right_properties));
}
Self::CollapseWhitespace { width, .. } if line.whitespace && !line.noncontent => {
line = LineMetric {
width: (*width).max(0),
chars: u64::from(*width > 0),
min_content: 0,
nonempty: *width > 0,
whitespace: *width > 0,
noncontent: false,
};
}
Self::Style(_) | Self::Template(_) => line.noncontent |= line.nonempty,
Self::FirstStyleRole { .. } if index == 0 => line.noncontent |= line.nonempty,
Self::LastStyleRole { .. } if index + 1 == length => line.noncontent |= line.nonempty,
_ => {}
}
line
}
pub fn apply_break(&self, properties: &mut AtomProperties) {
match self {
Self::ClearBreaks => *properties = AtomProperties::default(),
Self::ScrollWindow(region) => properties.scroll_window = Some(*region),
_ => {}
}
}
pub fn boundary_sensitive(&self) -> bool {
matches!(
self,
Self::FirstStyleRole { .. } | Self::LastStyleRole { .. }
)
}
pub fn preserves_metrics(&self) -> bool {
matches!(
self,
Self::OwnContent { .. } | Self::ClearBreaks | Self::ScrollWindow(_)
)
}
}

File diff suppressed because it is too large Load Diff