diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-04-30 14:12:28 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-04-30 14:12:28 +0200 |
| commit | f9e115daf54c29358f890b137f50a33a781af680 (patch) | |
| tree | 496de52246629ea8039db6beea94eb779ed2851d /src/model | |
| parent | f7c67cde72e6a67f45180856b332bae9863243bd (diff) | |
New block spacing model
Diffstat (limited to 'src/model')
| -rw-r--r-- | src/model/collapse.rs | 110 | ||||
| -rw-r--r-- | src/model/content.rs | 180 | ||||
| -rw-r--r-- | src/model/layout.rs | 6 | ||||
| -rw-r--r-- | src/model/show.rs | 6 |
4 files changed, 221 insertions, 81 deletions
diff --git a/src/model/collapse.rs b/src/model/collapse.rs index 17933fe8..258f577e 100644 --- a/src/model/collapse.rs +++ b/src/model/collapse.rs @@ -35,26 +35,37 @@ impl<'a, T> CollapsingBuilder<'a, T> { } /// Can only exist when there is at least one supportive item to its left - /// and to its right, with no destructive items or weak items in between to - /// its left and no destructive items in between to its right. There may be + /// and to its right, with no destructive items in between. There may be /// ignorant items in between in both directions. - pub fn weak(&mut self, item: T, strength: u8, styles: StyleChain<'a>) { - if self.last != Last::Destructive { - if self.last == Last::Weak { - if let Some(i) = self - .staged - .iter() - .position(|(.., prev)| prev.map_or(false, |p| p < strength)) - { - self.staged.remove(i); - } else { - return; - } - } + /// + /// Between weak items, there may be at least one per layer and among the + /// candidates the strongest one (smallest `weakness`) wins. When tied, + /// the one that compares larger through `PartialOrd` wins. + pub fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8) + where + T: PartialOrd, + { + if self.last == Last::Destructive { + return; + } - self.staged.push((item, styles, Some(strength))); - self.last = Last::Weak; + if self.last == Last::Weak { + if let Some(i) = + self.staged.iter().position(|(prev_item, _, prev_weakness)| { + prev_weakness.map_or(false, |prev_weakness| { + weakness < prev_weakness + || (weakness == prev_weakness && item > *prev_item) + }) + }) + { + self.staged.remove(i); + } else { + return; + } } + + self.staged.push((item, styles, Some(weakness))); + self.last = Last::Weak; } /// Forces nearby weak items to collapse. @@ -90,8 +101,8 @@ impl<'a, T> CollapsingBuilder<'a, T> { /// Push the staged items, filtering out weak items if `supportive` is /// false. fn flush(&mut self, supportive: bool) { - for (item, styles, strength) in self.staged.drain(..) { - if supportive || strength.is_none() { + for (item, styles, meta) in self.staged.drain(..) { + if supportive || meta.is_none() { self.builder.push(item, styles); } } @@ -103,3 +114,64 @@ impl<'a, T> Default for CollapsingBuilder<'a, T> { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::library::layout::FlowChild; + use crate::library::prelude::*; + + #[track_caller] + fn test<T>(builder: CollapsingBuilder<T>, expected: &[T]) + where + T: Debug + PartialEq, + { + let result = builder.finish().0; + let items: Vec<_> = result.items().collect(); + let expected: Vec<_> = expected.iter().collect(); + assert_eq!(items, expected); + } + + fn node() -> FlowChild { + FlowChild::Node(Content::Text("Hi".into()).pack()) + } + + fn abs(pt: f64) -> FlowChild { + FlowChild::Spacing(Length::pt(pt).into()) + } + + #[test] + fn test_collapsing_weak() { + let mut builder = CollapsingBuilder::new(); + let styles = StyleChain::default(); + builder.weak(FlowChild::Colbreak, styles, 0); + builder.supportive(node(), styles); + builder.weak(abs(10.0), styles, 0); + builder.ignorant(FlowChild::Colbreak, styles); + builder.weak(abs(20.0), styles, 0); + builder.supportive(node(), styles); + builder.weak(abs(10.0), styles, 0); + builder.weak(abs(20.0), styles, 1); + builder.supportive(node(), styles); + test(builder, &[ + node(), + FlowChild::Colbreak, + abs(20.0), + node(), + abs(10.0), + node(), + ]); + } + + #[test] + fn test_collapsing_destructive() { + let mut builder = CollapsingBuilder::new(); + let styles = StyleChain::default(); + builder.supportive(node(), styles); + builder.weak(abs(10.0), styles, 0); + builder.destructive(FlowChild::Colbreak, styles); + builder.weak(abs(20.0), styles, 0); + builder.supportive(node(), styles); + test(builder, &[node(), FlowChild::Colbreak, node()]); + } +} diff --git a/src/model/content.rs b/src/model/content.rs index 6e1e2f1c..31255a29 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -40,29 +40,32 @@ use crate::util::EcoString; pub enum Content { /// A word space. Space, - /// A forced line break. If `true`, the preceding line can still be - /// justified, if `false` not. - Linebreak(bool), + /// A forced line break. + Linebreak { justified: bool }, /// Horizontal spacing. - Horizontal(Spacing), + Horizontal { amount: Spacing, weak: bool }, /// Plain text. Text(EcoString), - /// A smart quote, may be single (`false`) or double (`true`). - Quote(bool), + /// A smart quote. + Quote { double: bool }, /// An inline-level node. Inline(LayoutNode), /// A paragraph break. Parbreak, /// A column break. - Colbreak, + Colbreak { weak: bool }, /// Vertical spacing. - Vertical(Spacing), + Vertical { + amount: Spacing, + weak: bool, + generated: bool, + }, /// A block-level node. Block(LayoutNode), /// A list / enum item. Item(ListItem), /// A page break. - Pagebreak(bool), + Pagebreak { weak: bool }, /// A page node. Page(PageNode), /// A node that can be realized with styles. @@ -153,21 +156,28 @@ impl Content { Self::show(DecoNode::<UNDERLINE>(self)) } - /// Add vertical spacing above and below the node. - pub fn spaced(self, above: Length, below: Length) -> Self { - if above.is_zero() && below.is_zero() { + /// Add weak vertical spacing above and below the node. + pub fn spaced(self, above: Option<Length>, below: Option<Length>) -> Self { + if above.is_none() && below.is_none() { return self; } let mut seq = vec![]; - if !above.is_zero() { - seq.push(Content::Vertical(above.into())); + if let Some(above) = above { + seq.push(Content::Vertical { + amount: above.into(), + weak: true, + generated: true, + }); } seq.push(self); - - if !below.is_zero() { - seq.push(Content::Vertical(below.into())); + if let Some(below) = below { + seq.push(Content::Vertical { + amount: below.into(), + weak: true, + generated: true, + }); } Self::sequence(seq) @@ -219,17 +229,21 @@ impl Debug for Content { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Space => f.pad("Space"), - Self::Linebreak(justified) => write!(f, "Linebreak({justified})"), - Self::Horizontal(kind) => write!(f, "Horizontal({kind:?})"), + Self::Linebreak { justified } => write!(f, "Linebreak({justified})"), + Self::Horizontal { amount, weak } => { + write!(f, "Horizontal({amount:?}, {weak})") + } Self::Text(text) => write!(f, "Text({text:?})"), - Self::Quote(double) => write!(f, "Quote({double})"), + Self::Quote { double } => write!(f, "Quote({double})"), Self::Inline(node) => node.fmt(f), Self::Parbreak => f.pad("Parbreak"), - Self::Colbreak => f.pad("Colbreak"), - Self::Vertical(kind) => write!(f, "Vertical({kind:?})"), + Self::Colbreak { weak } => write!(f, "Colbreak({weak})"), + Self::Vertical { amount, weak, generated } => { + write!(f, "Vertical({amount:?}, {weak}, {generated})") + } Self::Block(node) => node.fmt(f), Self::Item(item) => item.fmt(f), - Self::Pagebreak(soft) => write!(f, "Pagebreak({soft})"), + Self::Pagebreak { weak } => write!(f, "Pagebreak({weak})"), Self::Page(page) => page.fmt(f), Self::Show(node) => node.fmt(f), Self::Styled(styled) => { @@ -360,7 +374,7 @@ impl<'a, 'ctx> Builder<'a, 'ctx> { return Ok(()); } - let keep = matches!(content, Content::Pagebreak(false)); + let keep = matches!(content, Content::Pagebreak { weak: false }); self.interrupt(Interruption::Page, styles, keep)?; if let Some(doc) = &mut self.doc { @@ -419,10 +433,8 @@ impl<'a, 'ctx> Builder<'a, 'ctx> { if intr >= Interruption::Par { if !self.par.is_empty() { - self.flow.0.weak(FlowChild::Leading, 0, styles); mem::take(&mut self.par).finish(self); } - self.flow.0.weak(FlowChild::Leading, 0, styles); } if intr >= Interruption::Page { @@ -456,8 +468,8 @@ struct DocBuilder<'a> { impl<'a> DocBuilder<'a> { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) { match content { - Content::Pagebreak(soft) => { - self.keep_next = !soft; + Content::Pagebreak { weak } => { + self.keep_next = !weak; } Content::Page(page) => { self.pages.push(page.clone(), styles); @@ -483,16 +495,31 @@ struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>); impl<'a> FlowBuilder<'a> { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + // Weak flow elements: + // Weakness | Element + // 0 | weak colbreak + // 1 | weak fractional spacing + // 2 | weak spacing + // 3 | generated weak spacing + // 4 | generated weak fractional spacing + // 5 | par spacing + match content { - Content::Parbreak => { - self.0.weak(FlowChild::Parbreak, 1, styles); - } - Content::Colbreak => { - self.0.destructive(FlowChild::Colbreak, styles); + Content::Parbreak => {} + Content::Colbreak { weak } => { + if *weak { + self.0.weak(FlowChild::Colbreak, styles, 0); + } else { + self.0.destructive(FlowChild::Colbreak, styles); + } } - Content::Vertical(kind) => { - let child = FlowChild::Spacing(*kind); - if kind.is_fractional() { + &Content::Vertical { amount, weak, generated } => { + let child = FlowChild::Spacing(amount); + let frac = amount.is_fractional(); + if weak { + let weakness = 1 + u8::from(frac) + 2 * u8::from(generated); + self.0.weak(child, styles, weakness); + } else if frac { self.0.destructive(child, styles); } else { self.0.ignorant(child, styles); @@ -512,6 +539,18 @@ impl<'a> FlowBuilder<'a> { true } + fn par(&mut self, par: ParNode, styles: StyleChain<'a>, indent: bool) { + let amount = if indent && !styles.get(ParNode::SPACING_AND_INDENT) { + styles.get(ParNode::LEADING).into() + } else { + styles.get(ParNode::SPACING).into() + }; + + self.0.weak(FlowChild::Spacing(amount), styles, 5); + self.0.supportive(FlowChild::Node(par.pack()), styles); + self.0.weak(FlowChild::Spacing(amount), styles, 5); + } + fn finish(self, doc: &mut DocBuilder<'a>, styles: StyleChain<'a>) { let (flow, shared) = self.0.finish(); let styles = if flow.is_empty() { styles } else { shared }; @@ -530,24 +569,34 @@ struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>); impl<'a> ParBuilder<'a> { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + // Weak par elements: + // Weakness | Element + // 0 | weak fractional spacing + // 1 | weak spacing + // 2 | space + match content { Content::Space => { - self.0.weak(ParChild::Text(' '.into()), 0, styles); + self.0.weak(ParChild::Text(' '.into()), styles, 2); } - Content::Linebreak(justified) => { - let c = if *justified { '\u{2028}' } else { '\n' }; + &Content::Linebreak { justified } => { + let c = if justified { '\u{2028}' } else { '\n' }; self.0.destructive(ParChild::Text(c.into()), styles); } - Content::Horizontal(kind) => { - let child = ParChild::Spacing(*kind); - if kind.is_fractional() { + &Content::Horizontal { amount, weak } => { + let child = ParChild::Spacing(amount); + let frac = amount.is_fractional(); + if weak { + let weakness = u8::from(!frac); + self.0.weak(child, styles, weakness); + } else if frac { self.0.destructive(child, styles); } else { self.0.ignorant(child, styles); } } - Content::Quote(double) => { - self.0.supportive(ParChild::Quote(*double), styles); + &Content::Quote { double } => { + self.0.supportive(ParChild::Quote { double }, styles); } Content::Text(text) => { self.0.supportive(ParChild::Text(text.clone()), styles); @@ -575,7 +624,7 @@ impl<'a> ParBuilder<'a> { .items() .find_map(|child| match child { ParChild::Spacing(_) => None, - ParChild::Text(_) | ParChild::Quote(_) => Some(true), + ParChild::Text(_) | ParChild::Quote { .. } => Some(true), ParChild::Node(_) => Some(false), }) .unwrap_or_default() @@ -585,10 +634,8 @@ impl<'a> ParBuilder<'a> { .items() .rev() .find_map(|child| match child { - FlowChild::Leading => None, - FlowChild::Parbreak => None, + FlowChild::Spacing(_) => None, FlowChild::Node(node) => Some(node.is::<ParNode>()), - FlowChild::Spacing(_) => Some(false), FlowChild::Colbreak => Some(false), }) .unwrap_or_default() @@ -596,8 +643,7 @@ impl<'a> ParBuilder<'a> { children.push_front(ParChild::Spacing(indent.into())); } - let node = ParNode(children).pack(); - parent.flow.0.supportive(FlowChild::Node(node), shared); + parent.flow.par(ParNode(children), shared, !indent.is_zero()); } fn is_empty(&self) -> bool { @@ -611,19 +657,24 @@ struct ListBuilder<'a> { items: StyleVecBuilder<'a, ListItem>, /// Whether the list contains no paragraph breaks. tight: bool, + /// Whether the list can be attached. + attachable: bool, /// Trailing content for which it is unclear whether it is part of the list. staged: Vec<(&'a Content, StyleChain<'a>)>, } impl<'a> ListBuilder<'a> { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - match content { - Content::Space if !self.items.is_empty() => { - self.staged.push((content, styles)); - } - Content::Parbreak if !self.items.is_empty() => { - self.staged.push((content, styles)); + if self.items.is_empty() { + match content { + Content::Space => {} + Content::Item(_) => {} + Content::Parbreak => self.attachable = false, + _ => self.attachable = true, } + } + + match content { Content::Item(item) if self .items @@ -634,6 +685,9 @@ impl<'a> ListBuilder<'a> { self.items.push(item.clone(), styles); self.tight &= self.staged.drain(..).all(|(t, _)| *t != Content::Parbreak); } + Content::Space | Content::Parbreak if !self.items.is_empty() => { + self.staged.push((content, styles)); + } _ => return false, } @@ -647,10 +701,17 @@ impl<'a> ListBuilder<'a> { None => return Ok(()), }; + let start = 1; let tight = self.tight; + let attached = tight && self.attachable; + let content = match kind { - UNORDERED => Content::show(ListNode::<UNORDERED> { start: 1, tight, items }), - ORDERED | _ => Content::show(ListNode::<ORDERED> { start: 1, tight, items }), + UNORDERED => { + Content::show(ListNode::<UNORDERED> { start, tight, attached, items }) + } + ORDERED | _ => { + Content::show(ListNode::<ORDERED> { start, tight, attached, items }) + } }; let stored = parent.scratch.templates.alloc(content); @@ -660,6 +721,8 @@ impl<'a> ListBuilder<'a> { parent.accept(content, styles)?; } + parent.list.attachable = true; + Ok(()) } @@ -673,6 +736,7 @@ impl Default for ListBuilder<'_> { Self { items: StyleVecBuilder::default(), tight: true, + attachable: true, staged: vec![], } } diff --git a/src/model/layout.rs b/src/model/layout.rs index 78bfedc7..51154286 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -1,7 +1,7 @@ //! Layouting infrastructure. use std::any::Any; -use std::fmt::{self, Debug, Formatter}; +use std::fmt::{self, Debug, Formatter, Write}; use std::hash::Hash; use std::sync::Arc; @@ -239,7 +239,9 @@ impl Default for LayoutNode { impl Debug for LayoutNode { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) + f.write_str("Layout(")?; + self.0.fmt(f)?; + f.write_char(')') } } diff --git a/src/model/show.rs b/src/model/show.rs index d1365eb2..16374deb 100644 --- a/src/model/show.rs +++ b/src/model/show.rs @@ -1,4 +1,4 @@ -use std::fmt::{self, Debug, Formatter}; +use std::fmt::{self, Debug, Formatter, Write}; use std::hash::Hash; use std::sync::Arc; @@ -87,7 +87,9 @@ impl Show for ShowNode { impl Debug for ShowNode { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) + f.write_str("Show(")?; + self.0.fmt(f)?; + f.write_char(')') } } |
