607 lines
20 KiB
Rust
607 lines
20 KiB
Rust
//! 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)
|
|
}
|