perf: retain sparse column composition and exact box projections
This commit is contained in:
parent
14c1698c41
commit
39d079b2cc
606
native/src/composition.rs
Normal file
606
native/src/composition.rs
Normal 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)
|
||||
}
|
||||
206
native/src/composition_tests.rs
Normal file
206
native/src/composition_tests.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(¤t.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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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
295
native/src/line_metrics.rs
Normal 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
169
native/src/line_ops.rs
Normal 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
Loading…
Reference in New Issue
Block a user