New interaction model

This commit is contained in:
Laurenz 2022-11-12 23:25:54 +01:00
parent d9ce194fe7
commit bf59c08a0a
15 changed files with 409 additions and 395 deletions

128
library/src/core/behave.rs Normal file
View File

@ -0,0 +1,128 @@
//! Node interaction.
use typst::model::{capability, Content, StyleChain, StyleVec, StyleVecBuilder};
/// How a node interacts with other nodes.
#[capability]
pub trait Behave: 'static + Send + Sync {
/// The node's interaction behaviour.
fn behaviour(&self) -> Behaviour;
/// Whether this weak node is larger than a previous one and thus picked as
/// the maximum when the levels are the same.
#[allow(unused_variables)]
fn larger(&self, prev: &Content) -> bool {
false
}
}
/// How a node interacts with other nodes in a stream.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Behaviour {
/// A weak node which only survives when a supportive node is before and
/// after it. Furthermore, per consecutive run of weak nodes, only one
/// survives: The one with the lowest weakness level (or the larger one if
/// there is a tie).
Weak(u8),
/// A node that enables adjacent weak nodes to exist. The default.
Supportive,
/// A node that destroys adjacent weak nodes.
Destructive,
/// A node that does not interact at all with other node, having the
/// same effect as if it didn't exist.
Ignorant,
}
/// A wrapper around a [`StyleVecBuilder`] that allows items to interact.
pub struct BehavedBuilder<'a> {
/// The internal builder.
builder: StyleVecBuilder<'a, Content>,
/// Staged weak and ignorant items that we can't yet commit to the builder.
/// The option is `Some(_)` for weak items and `None` for ignorant items.
staged: Vec<(Content, Behaviour, StyleChain<'a>)>,
/// What the last non-ignorant item was.
last: Behaviour,
}
impl<'a> BehavedBuilder<'a> {
/// Create a new style-vec builder.
pub fn new() -> Self {
Self {
builder: StyleVecBuilder::new(),
staged: vec![],
last: Behaviour::Destructive,
}
}
/// Whether the builder is empty.
pub fn is_empty(&self) -> bool {
self.builder.is_empty() && self.staged.is_empty()
}
/// Push an item into the sequence.
pub fn push(&mut self, item: Content, styles: StyleChain<'a>) {
let interaction = item
.to::<dyn Behave>()
.map_or(Behaviour::Supportive, Behave::behaviour);
match interaction {
Behaviour::Weak(level) => {
if matches!(self.last, Behaviour::Weak(_)) {
let item = item.to::<dyn Behave>().unwrap();
let i = self.staged.iter().position(|prev| {
let Behaviour::Weak(prev_level) = prev.1 else { return false };
level < prev_level
|| (level == prev_level && item.larger(&prev.0))
});
let Some(i) = i else { return };
self.staged.remove(i);
}
if self.last != Behaviour::Destructive {
self.staged.push((item, interaction, styles));
self.last = interaction;
}
}
Behaviour::Supportive => {
self.flush(true);
self.builder.push(item, styles);
self.last = interaction;
}
Behaviour::Destructive => {
self.flush(false);
self.builder.push(item, styles);
self.last = interaction;
}
Behaviour::Ignorant => {
self.staged.push((item, interaction, styles));
}
}
}
/// Iterate over the contained items.
pub fn items(&self) -> impl DoubleEndedIterator<Item = &Content> {
self.builder.items().chain(self.staged.iter().map(|(item, ..)| item))
}
/// Return the finish style vec and the common prefix chain.
pub fn finish(mut self) -> (StyleVec<Content>, StyleChain<'a>) {
self.flush(false);
self.builder.finish()
}
/// Push the staged items, filtering out weak items if `supportive` is
/// false.
fn flush(&mut self, supportive: bool) {
for (item, interaction, styles) in self.staged.drain(..) {
if supportive || interaction == Behaviour::Ignorant {
self.builder.push(item, styles);
}
}
}
}
impl<'a> Default for BehavedBuilder<'a> {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,4 +1,5 @@
use super::*; //! Extension traits.
use crate::prelude::*; use crate::prelude::*;
/// Additional methods on content. /// Additional methods on content.
@ -33,31 +34,31 @@ pub trait ContentExt {
impl ContentExt for Content { impl ContentExt for Content {
fn strong(self) -> Self { fn strong(self) -> Self {
text::StrongNode(self).pack() crate::text::StrongNode(self).pack()
} }
fn emph(self) -> Self { fn emph(self) -> Self {
text::EmphNode(self).pack() crate::text::EmphNode(self).pack()
} }
fn underlined(self) -> Self { fn underlined(self) -> Self {
text::DecoNode::<{ text::UNDERLINE }>(self).pack() crate::text::DecoNode::<{ crate::text::UNDERLINE }>(self).pack()
} }
fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self { fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self {
layout::BoxNode { sizing, child: self }.pack() crate::layout::BoxNode { sizing, child: self }.pack()
} }
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self { fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
layout::AlignNode { aligns, child: self }.pack() crate::layout::AlignNode { aligns, child: self }.pack()
} }
fn padded(self, padding: Sides<Rel<Length>>) -> Self { fn padded(self, padding: Sides<Rel<Length>>) -> Self {
layout::PadNode { padding, child: self }.pack() crate::layout::PadNode { padding, child: self }.pack()
} }
fn moved(self, delta: Axes<Rel<Length>>) -> Self { fn moved(self, delta: Axes<Rel<Length>>) -> Self {
layout::MoveNode { delta, child: self }.pack() crate::layout::MoveNode { delta, child: self }.pack()
} }
fn filled(self, fill: Paint) -> Self { fn filled(self, fill: Paint) -> Self {
@ -73,16 +74,16 @@ impl ContentExt for Content {
pub trait StyleMapExt { pub trait StyleMapExt {
/// Set a font family composed of a preferred family and existing families /// Set a font family composed of a preferred family and existing families
/// from a style chain. /// from a style chain.
fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain); fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain);
} }
impl StyleMapExt for StyleMap { impl StyleMapExt for StyleMap {
fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) { fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain) {
self.set( self.set(
text::TextNode::FAMILY, crate::text::TextNode::FAMILY,
text::FallbackList( crate::text::FallbackList(
std::iter::once(preferred) std::iter::once(preferred)
.chain(existing.get(text::TextNode::FAMILY).0.iter().cloned()) .chain(existing.get(crate::text::TextNode::FAMILY).0.iter().cloned())
.collect(), .collect(),
), ),
); );

7
library/src/core/mod.rs Normal file
View File

@ -0,0 +1,7 @@
//! Central definitions for the standard library.
mod behave;
mod ext;
pub use behave::*;
pub use ext::*;

View File

@ -104,10 +104,20 @@ pub struct ColbreakNode {
pub weak: bool, pub weak: bool,
} }
#[node] #[node(Behave)]
impl ColbreakNode { impl ColbreakNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let weak = args.named("weak")?.unwrap_or(false); let weak = args.named("weak")?.unwrap_or(false);
Ok(Self { weak }.pack()) Ok(Self { weak }.pack())
} }
} }
impl Behave for ColbreakNode {
fn behaviour(&self) -> Behaviour {
if self.weak {
Behaviour::Weak(1)
} else {
Behaviour::Destructive
}
}
}

View File

@ -66,19 +66,25 @@ pub struct BlockNode(pub Content);
impl BlockNode { impl BlockNode {
/// The spacing between the previous and this block. /// The spacing between the previous and this block.
#[property(skip)] #[property(skip)]
pub const ABOVE: VNode = VNode::weak(Em::new(1.2).into()); pub const ABOVE: VNode = VNode::block_spacing(Em::new(1.2).into());
/// The spacing between this and the following block. /// The spacing between this and the following block.
#[property(skip)] #[property(skip)]
pub const BELOW: VNode = VNode::weak(Em::new(1.2).into()); pub const BELOW: VNode = VNode::block_spacing(Em::new(1.2).into());
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.eat()?.unwrap_or_default()).pack()) Ok(Self(args.eat()?.unwrap_or_default()).pack())
} }
fn set(...) { fn set(...) {
let spacing = args.named("spacing")?.map(VNode::weak); let spacing = args.named("spacing")?.map(VNode::block_spacing);
styles.set_opt(Self::ABOVE, args.named("above")?.map(VNode::strong).or(spacing)); styles.set_opt(
styles.set_opt(Self::BELOW, args.named("below")?.map(VNode::strong).or(spacing)); Self::ABOVE,
args.named("above")?.map(VNode::block_around).or(spacing),
);
styles.set_opt(
Self::BELOW,
args.named("below")?.map(VNode::block_around).or(spacing),
);
} }
} }

View File

@ -1,7 +1,4 @@
use std::cmp::Ordering; use super::{AlignNode, ColbreakNode, PlaceNode, Spacing, VNode};
use super::{AlignNode, PlaceNode, Spacing, VNode};
use crate::layout::BlockNode;
use crate::prelude::*; use crate::prelude::*;
use crate::text::ParNode; use crate::text::ParNode;
@ -10,18 +7,7 @@ use crate::text::ParNode;
/// This node is reponsible for layouting both the top-level content flow and /// This node is reponsible for layouting both the top-level content flow and
/// the contents of boxes. /// the contents of boxes.
#[derive(Hash)] #[derive(Hash)]
pub struct FlowNode(pub StyleVec<FlowChild>); pub struct FlowNode(pub StyleVec<Content>);
/// A child of a flow node.
#[derive(Hash, PartialEq)]
pub enum FlowChild {
/// Vertical spacing between other children.
Spacing(VNode),
/// Arbitrary block-level content.
Block(Content),
/// A column / region break.
Colbreak,
}
#[node(LayoutBlock)] #[node(LayoutBlock)]
impl FlowNode {} impl FlowNode {}
@ -33,20 +19,18 @@ impl LayoutBlock for FlowNode {
regions: &Regions, regions: &Regions,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<Vec<Frame>> { ) -> SourceResult<Vec<Frame>> {
let mut layouter = FlowLayouter::new(regions, styles); let mut layouter = FlowLayouter::new(regions);
for (child, map) in self.0.iter() { for (child, map) in self.0.iter() {
let styles = map.chain(&styles); let styles = map.chain(&styles);
match child { if let Some(&node) = child.downcast::<VNode>() {
FlowChild::Spacing(node) => { layouter.layout_spacing(node.amount, styles);
layouter.layout_spacing(node, styles); } else if child.has::<dyn LayoutBlock>() {
} layouter.layout_block(world, child, styles)?;
FlowChild::Block(block) => { } else if child.is::<ColbreakNode>() {
layouter.layout_block(world, block, styles)?;
}
FlowChild::Colbreak => {
layouter.finish_region(); layouter.finish_region();
} } else {
panic!("unexpected flow child: {child:?}");
} }
} }
@ -61,31 +45,10 @@ impl Debug for FlowNode {
} }
} }
impl Debug for FlowChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Spacing(kind) => write!(f, "{:?}", kind),
Self::Block(block) => block.fmt(f),
Self::Colbreak => f.pad("Colbreak"),
}
}
}
impl PartialOrd for FlowChild {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b),
_ => None,
}
}
}
/// Performs flow layout. /// Performs flow layout.
struct FlowLayouter<'a> { struct FlowLayouter {
/// The regions to layout children into. /// The regions to layout children into.
regions: Regions, regions: Regions,
/// The shared styles.
shared: StyleChain<'a>,
/// Whether the flow should expand to fill the region. /// Whether the flow should expand to fill the region.
expand: Axes<bool>, expand: Axes<bool>,
/// The full size of `regions.size` that was available before we started /// The full size of `regions.size` that was available before we started
@ -95,8 +58,6 @@ struct FlowLayouter<'a> {
used: Size, used: Size,
/// The sum of fractions in the current region. /// The sum of fractions in the current region.
fr: Fr, fr: Fr,
/// The spacing below the last block.
below: Option<VNode>,
/// Spacing and layouted blocks. /// Spacing and layouted blocks.
items: Vec<FlowItem>, items: Vec<FlowItem>,
/// Finished frames for previous regions. /// Finished frames for previous regions.
@ -115,9 +76,9 @@ enum FlowItem {
Placed(Frame), Placed(Frame),
} }
impl<'a> FlowLayouter<'a> { impl FlowLayouter {
/// Create a new flow layouter. /// Create a new flow layouter.
fn new(regions: &Regions, shared: StyleChain<'a>) -> Self { fn new(regions: &Regions) -> Self {
let expand = regions.expand; let expand = regions.expand;
let full = regions.first; let full = regions.first;
@ -127,20 +88,18 @@ impl<'a> FlowLayouter<'a> {
Self { Self {
regions, regions,
shared,
expand, expand,
full, full,
used: Size::zero(), used: Size::zero(),
fr: Fr::zero(), fr: Fr::zero(),
below: None,
items: vec![], items: vec![],
finished: vec![], finished: vec![],
} }
} }
/// Layout spacing. /// Actually layout the spacing.
fn layout_spacing(&mut self, node: &VNode, styles: StyleChain) { fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) {
match node.amount { match spacing {
Spacing::Relative(v) => { Spacing::Relative(v) => {
// Resolve the spacing and limit it to the remaining space. // Resolve the spacing and limit it to the remaining space.
let resolved = v.resolve(styles).relative_to(self.full.y); let resolved = v.resolve(styles).relative_to(self.full.y);
@ -154,10 +113,6 @@ impl<'a> FlowLayouter<'a> {
self.fr += v; self.fr += v;
} }
} }
if node.weak || node.amount.is_fractional() {
self.below = None;
}
} }
/// Layout a block. /// Layout a block.
@ -172,19 +127,9 @@ impl<'a> FlowLayouter<'a> {
self.finish_region(); self.finish_region();
} }
// Add spacing between the last block and this one.
if let Some(below) = self.below.take() {
let above = styles.get(BlockNode::ABOVE);
let pick_below = (above.weak && !below.weak) || (below.amount > above.amount);
let spacing = if pick_below { below } else { above };
self.layout_spacing(&spacing, self.shared);
}
// Placed nodes that are out of flow produce placed items which aren't // Placed nodes that are out of flow produce placed items which aren't
// aligned later. // aligned later.
let mut is_placed = false;
if let Some(placed) = block.downcast::<PlaceNode>() { if let Some(placed) = block.downcast::<PlaceNode>() {
is_placed = true;
if placed.out_of_flow() { if placed.out_of_flow() {
let frame = block.layout_block(world, &self.regions, styles)?.remove(0); let frame = block.layout_block(world, &self.regions, styles)?.remove(0);
self.items.push(FlowItem::Placed(frame)); self.items.push(FlowItem::Placed(frame));
@ -205,6 +150,7 @@ impl<'a> FlowLayouter<'a> {
.unwrap_or(Align::Top), .unwrap_or(Align::Top),
); );
// Layout the block itself.
let frames = block.layout_block(world, &self.regions, styles)?; let frames = block.layout_block(world, &self.regions, styles)?;
let len = frames.len(); let len = frames.len();
for (i, frame) in frames.into_iter().enumerate() { for (i, frame) in frames.into_iter().enumerate() {
@ -220,10 +166,6 @@ impl<'a> FlowLayouter<'a> {
} }
} }
if !is_placed {
self.below = Some(styles.get(BlockNode::BELOW));
}
Ok(()) Ok(())
} }
@ -272,7 +214,6 @@ impl<'a> FlowLayouter<'a> {
self.full = self.regions.first; self.full = self.regions.first;
self.used = Size::zero(); self.used = Size::zero();
self.fr = Fr::zero(); self.fr = Fr::zero();
self.below = None;
self.finished.push(output); self.finished.push(output);
} }

View File

@ -32,16 +32,18 @@ use typst::diag::SourceResult;
use typst::frame::Frame; use typst::frame::Frame;
use typst::geom::*; use typst::geom::*;
use typst::model::{ use typst::model::{
capability, Content, Node, SequenceNode, Show, StyleChain, StyleEntry, StyleVec, capability, Content, Node, SequenceNode, Show, StyleChain, StyleEntry,
StyleVecBuilder, StyledNode, Target, StyleVecBuilder, StyledNode, Target,
}; };
use typst::World; use typst::World;
use crate::core::BehavedBuilder;
use crate::prelude::*;
use crate::structure::{ use crate::structure::{
DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST, DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST,
}; };
use crate::text::{ use crate::text::{
LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode, LinebreakNode, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode,
}; };
/// Root-level layout. /// Root-level layout.
@ -468,41 +470,17 @@ impl Default for DocBuilder<'_> {
/// Accepts flow content. /// Accepts flow content.
#[derive(Default)] #[derive(Default)]
struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>, bool); struct FlowBuilder<'a>(BehavedBuilder<'a>, bool);
impl<'a> FlowBuilder<'a> { impl<'a> FlowBuilder<'a> {
fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool { fn accept(&mut self, content: &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
let last_was_parbreak = self.1; let last_was_parbreak = self.1;
self.1 = false; self.1 = false;
if content.is::<ParbreakNode>() { if content.is::<ParbreakNode>() {
self.1 = true; self.1 = true;
} else if let Some(colbreak) = content.downcast::<ColbreakNode>() { } else if content.is::<VNode>() || content.is::<ColbreakNode>() {
if colbreak.weak { self.0.push(content.clone(), styles);
self.0.weak(FlowChild::Colbreak, styles, 0);
} else {
self.0.destructive(FlowChild::Colbreak, styles);
}
} else if let Some(vertical) = content.downcast::<VNode>() {
let child = FlowChild::Spacing(*vertical);
let frac = vertical.amount.is_fractional();
if vertical.weak {
let weakness = 1 + u8::from(frac);
self.0.weak(child, styles, weakness);
} else if frac {
self.0.destructive(child, styles);
} else {
self.0.ignorant(child, styles);
}
} else if content.has::<dyn LayoutBlock>() { } else if content.has::<dyn LayoutBlock>() {
if !last_was_parbreak { if !last_was_parbreak {
let tight = if let Some(node) = content.downcast::<ListNode>() { let tight = if let Some(node) = content.downcast::<ListNode>() {
@ -517,17 +495,16 @@ impl<'a> FlowBuilder<'a> {
if tight { if tight {
let leading = styles.get(ParNode::LEADING); let leading = styles.get(ParNode::LEADING);
let spacing = VNode::weak(leading.into()); let spacing = VNode::list_attach(leading.into());
self.0.weak(FlowChild::Spacing(spacing), styles, 1); self.0.push(spacing.pack(), styles);
} }
} }
let child = FlowChild::Block(content.clone()); let above = styles.get(BlockNode::ABOVE);
if content.is::<PlaceNode>() { let below = styles.get(BlockNode::BELOW);
self.0.ignorant(child, styles); self.0.push(above.pack(), styles);
} else { self.0.push(content.clone(), styles);
self.0.supportive(child, styles); self.0.push(below.pack(), styles);
}
} else { } else {
return false; return false;
} }
@ -549,43 +526,22 @@ impl<'a> FlowBuilder<'a> {
/// Accepts paragraph content. /// Accepts paragraph content.
#[derive(Default)] #[derive(Default)]
struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>); struct ParBuilder<'a>(BehavedBuilder<'a>);
impl<'a> ParBuilder<'a> { impl<'a> ParBuilder<'a> {
fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool { fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool {
// Weak par elements: if content.is::<SpaceNode>()
// Weakness | Element || content.is::<LinebreakNode>()
// 0 | weak fractional spacing || content.is::<HNode>()
// 1 | weak spacing || content.is::<SmartQuoteNode>()
// 2 | space || content.is::<TextNode>()
|| content.has::<dyn LayoutInline>()
if content.is::<SpaceNode>() { {
self.0.weak(ParChild::Text(' '.into()), styles, 2); self.0.push(content.clone(), styles);
} else if let Some(linebreak) = content.downcast::<LinebreakNode>() { return true;
let c = if linebreak.justify { '\u{2028}' } else { '\n' };
self.0.destructive(ParChild::Text(c.into()), styles);
} else if let Some(horizontal) = content.downcast::<HNode>() {
let child = ParChild::Spacing(horizontal.amount);
let frac = horizontal.amount.is_fractional();
if horizontal.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);
}
} else if let Some(quote) = content.downcast::<SmartQuoteNode>() {
self.0.supportive(ParChild::Quote { double: quote.double }, styles);
} else if let Some(text) = content.downcast::<TextNode>() {
self.0.supportive(ParChild::Text(text.0.clone()), styles);
} else if content.has::<dyn LayoutInline>() {
self.0.supportive(ParChild::Inline(content.clone()), styles);
} else {
return false;
} }
true false
} }
fn finish(self, parent: &mut Builder<'a>) { fn finish(self, parent: &mut Builder<'a>) {
@ -600,10 +556,14 @@ impl<'a> ParBuilder<'a> {
if !indent.is_zero() if !indent.is_zero()
&& children && children
.items() .items()
.find_map(|child| match child { .find_map(|child| {
ParChild::Spacing(_) => None, if child.is::<TextNode>() || child.is::<SmartQuoteNode>() {
ParChild::Text(_) | ParChild::Quote { .. } => Some(true), Some(true)
ParChild::Inline(_) => Some(false), } else if child.has::<dyn LayoutInline>() {
Some(false)
} else {
None
}
}) })
.unwrap_or_default() .unwrap_or_default()
&& parent && parent
@ -611,14 +571,10 @@ impl<'a> ParBuilder<'a> {
.0 .0
.items() .items()
.rev() .rev()
.find_map(|child| match child { .find(|child| child.has::<dyn LayoutBlock>())
FlowChild::Spacing(_) => None, .map_or(false, |child| child.is::<ParNode>())
FlowChild::Block(content) => Some(content.is::<ParNode>()),
FlowChild::Colbreak => Some(false),
})
.unwrap_or_default()
{ {
children.push_front(ParChild::Spacing(indent.into())); children.push_front(HNode::strong(indent.into()).pack());
} }
parent.flow.accept(&ParNode(children).pack(), shared); parent.flow.accept(&ParNode(children).pack(), shared);
@ -701,115 +657,3 @@ impl Default for ListBuilder<'_> {
} }
} }
} }
/// A wrapper around a [`StyleVecBuilder`] that allows to collapse items.
struct CollapsingBuilder<'a, T> {
/// The internal builder.
builder: StyleVecBuilder<'a, T>,
/// Staged weak and ignorant items that we can't yet commit to the builder.
/// The option is `Some(_)` for weak items and `None` for ignorant items.
staged: Vec<(T, StyleChain<'a>, Option<u8>)>,
/// What the last non-ignorant item was.
last: Last,
}
/// What the last non-ignorant item was.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum Last {
Weak,
Destructive,
Supportive,
}
impl<'a, T> CollapsingBuilder<'a, T> {
/// Create a new style-vec builder.
fn new() -> Self {
Self {
builder: StyleVecBuilder::new(),
staged: vec![],
last: Last::Destructive,
}
}
/// Whether the builder is empty.
fn is_empty(&self) -> bool {
self.builder.is_empty() && self.staged.is_empty()
}
/// Can only exist when there is at least one supportive item to its left
/// and to its right, with no destructive items in between. There may be
/// ignorant items in between in both directions.
///
/// 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.
fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8)
where
T: PartialOrd,
{
if self.last == Last::Destructive {
return;
}
if self.last == Last::Weak {
let weak = self.staged.iter().position(|(prev_item, _, prev_weakness)| {
prev_weakness.map_or(false, |prev_weakness| {
weakness < prev_weakness
|| (weakness == prev_weakness && item > *prev_item)
})
});
let Some(weak) = weak else { return };
self.staged.remove(weak);
}
self.staged.push((item, styles, Some(weakness)));
self.last = Last::Weak;
}
/// Forces nearby weak items to collapse.
fn destructive(&mut self, item: T, styles: StyleChain<'a>) {
self.flush(false);
self.builder.push(item, styles);
self.last = Last::Destructive;
}
/// Allows nearby weak items to exist.
fn supportive(&mut self, item: T, styles: StyleChain<'a>) {
self.flush(true);
self.builder.push(item, styles);
self.last = Last::Supportive;
}
/// Has no influence on other items.
fn ignorant(&mut self, item: T, styles: StyleChain<'a>) {
self.staged.push((item, styles, None));
}
/// Iterate over the contained items.
fn items(&self) -> impl DoubleEndedIterator<Item = &T> {
self.builder.items().chain(self.staged.iter().map(|(item, ..)| item))
}
/// Return the finish style vec and the common prefix chain.
fn finish(mut self) -> (StyleVec<T>, StyleChain<'a>) {
self.flush(false);
self.builder.finish()
}
/// Push the staged items, filtering out weak items if `supportive` is
/// false.
fn flush(&mut self, supportive: bool) {
for (item, styles, meta) in self.staged.drain(..) {
if supportive || meta.is_none() {
self.builder.push(item, styles);
}
}
}
}
impl<'a, T> Default for CollapsingBuilder<'a, T> {
fn default() -> Self {
Self::new()
}
}

View File

@ -5,7 +5,7 @@ use crate::prelude::*;
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct PlaceNode(pub Content); pub struct PlaceNode(pub Content);
#[node(LayoutBlock)] #[node(LayoutBlock, Behave)]
impl PlaceNode { impl PlaceNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start))); let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start)));
@ -54,3 +54,9 @@ impl PlaceNode {
.map_or(false, |node| node.aligns.y.is_some()) .map_or(false, |node| node.aligns.y.is_some())
} }
} }
impl Behave for PlaceNode {
fn behaviour(&self) -> Behaviour {
Behaviour::Ignorant
}
}

View File

@ -5,11 +5,13 @@ use crate::prelude::*;
/// Horizontal spacing. /// Horizontal spacing.
#[derive(Debug, Copy, Clone, Hash)] #[derive(Debug, Copy, Clone, Hash)]
pub struct HNode { pub struct HNode {
/// The amount of horizontal spacing.
pub amount: Spacing, pub amount: Spacing,
/// Whether the node is weak, see also [`Behaviour`].
pub weak: bool, pub weak: bool,
} }
#[node] #[node(Behave)]
impl HNode { impl HNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let amount = args.expect("spacing")?; let amount = args.expect("spacing")?;
@ -18,31 +20,98 @@ impl HNode {
} }
} }
/// Vertical spacing. impl HNode {
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd)] /// Normal strong spacing.
pub struct VNode {
pub amount: Spacing,
pub weak: bool,
}
impl VNode {
/// Create weak vertical spacing.
pub fn weak(amount: Spacing) -> Self {
Self { amount, weak: true }
}
/// Create strong vertical spacing.
pub fn strong(amount: Spacing) -> Self { pub fn strong(amount: Spacing) -> Self {
Self { amount, weak: false } Self { amount, weak: false }
} }
/// User-created weak spacing.
pub fn weak(amount: Spacing) -> Self {
Self { amount, weak: true }
}
} }
#[node] impl Behave for HNode {
fn behaviour(&self) -> Behaviour {
if self.amount.is_fractional() {
Behaviour::Destructive
} else if self.weak {
Behaviour::Weak(1)
} else {
Behaviour::Ignorant
}
}
fn larger(&self, prev: &Content) -> bool {
let Some(prev) = prev.downcast::<Self>() else { return false };
self.amount > prev.amount
}
}
/// Vertical spacing.
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd)]
pub struct VNode {
/// The amount of vertical spacing.
pub amount: Spacing,
/// The node's weakness level, see also [`Behaviour`].
pub weakness: u8,
}
#[node(Behave)]
impl VNode { impl VNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let amount = args.expect("spacing")?; let amount = args.expect("spacing")?;
let weak = args.named("weak")?.unwrap_or(false); let node = if args.named("weak")?.unwrap_or(false) {
Ok(Self { amount, weak }.pack()) Self::weak(amount)
} else {
Self::strong(amount)
};
Ok(node.pack())
}
}
impl VNode {
/// Normal strong spacing.
pub fn strong(amount: Spacing) -> Self {
Self { amount, weakness: 0 }
}
/// User-created weak spacing.
pub fn weak(amount: Spacing) -> Self {
Self { amount, weakness: 1 }
}
/// Weak spacing with list attach weakness.
pub fn list_attach(amount: Spacing) -> Self {
Self { amount, weakness: 2 }
}
/// Weak spacing with BlockNode::ABOVE/BELOW weakness.
pub fn block_around(amount: Spacing) -> Self {
Self { amount, weakness: 3 }
}
/// Weak spacing with BlockNode::SPACING weakness.
pub fn block_spacing(amount: Spacing) -> Self {
Self { amount, weakness: 4 }
}
}
impl Behave for VNode {
fn behaviour(&self) -> Behaviour {
if self.amount.is_fractional() {
Behaviour::Destructive
} else if self.weakness > 0 {
Behaviour::Weak(self.weakness)
} else {
Behaviour::Ignorant
}
}
fn larger(&self, prev: &Content) -> bool {
let Some(prev) = prev.downcast::<Self>() else { return false };
self.amount > prev.amount
} }
} }

View File

@ -1,6 +1,7 @@
//! Typst's standard library. //! Typst's standard library.
pub mod base; pub mod base;
pub mod core;
pub mod graphics; pub mod graphics;
pub mod layout; pub mod layout;
pub mod math; pub mod math;
@ -8,8 +9,6 @@ pub mod prelude;
pub mod structure; pub mod structure;
pub mod text; pub mod text;
mod ext;
use typst::geom::{Align, Color, Dir, GenAlign}; use typst::geom::{Align, Color, Dir, GenAlign};
use typst::model::{LangItems, Node, Scope, StyleMap}; use typst::model::{LangItems, Node, Scope, StyleMap};

View File

@ -1,20 +1,32 @@
//! Helpful imports for creating library functionality. //! Helpful imports for creating library functionality.
#[doc(no_inline)]
pub use std::fmt::{self, Debug, Formatter}; pub use std::fmt::{self, Debug, Formatter};
#[doc(no_inline)]
pub use std::num::NonZeroUsize; pub use std::num::NonZeroUsize;
#[doc(no_inline)]
pub use comemo::Tracked; pub use comemo::Tracked;
#[doc(no_inline)]
pub use typst::diag::{bail, error, with_alternative, At, SourceResult, StrResult}; pub use typst::diag::{bail, error, with_alternative, At, SourceResult, StrResult};
#[doc(no_inline)]
pub use typst::frame::*; pub use typst::frame::*;
#[doc(no_inline)]
pub use typst::geom::*; pub use typst::geom::*;
#[doc(no_inline)]
pub use typst::model::{ pub use typst::model::{
array, capability, castable, dict, dynamic, format_str, node, Args, Array, Cast, array, capability, castable, dict, dynamic, format_str, node, Args, Array, Cast,
Content, Dict, Finalize, Fold, Func, Key, Node, RecipeId, Resolve, Scope, Show, Content, Dict, Finalize, Fold, Func, Node, RecipeId, Resolve, Show, Smart, Str,
Smart, Str, StyleChain, StyleMap, StyleVec, Value, Vm, StyleChain, StyleMap, StyleVec, Value, Vm,
}; };
#[doc(no_inline)]
pub use typst::syntax::{Span, Spanned}; pub use typst::syntax::{Span, Spanned};
#[doc(no_inline)]
pub use typst::util::{format_eco, EcoString}; pub use typst::util::{format_eco, EcoString};
#[doc(no_inline)]
pub use typst::World; pub use typst::World;
pub use super::ext::{ContentExt, StyleMapExt}; #[doc(no_inline)]
pub use super::layout::{LayoutBlock, LayoutInline, Regions}; pub use crate::core::{Behave, Behaviour, ContentExt, StyleMapExt};
#[doc(no_inline)]
pub use crate::layout::{LayoutBlock, LayoutInline, Regions};

View File

@ -50,20 +50,21 @@ impl Finalize for HeadingNode {
_: StyleChain, _: StyleChain,
realized: Content, realized: Content,
) -> SourceResult<Content> { ) -> SourceResult<Content> {
let size = Em::new(match self.level.get() { let scale = match self.level.get() {
1 => 1.4, 1 => 1.4,
2 => 1.2, 2 => 1.2,
_ => 1.0, _ => 1.0,
}); };
let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 }); let size = Em::new(scale);
let below = Em::new(0.66); let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 }) / scale;
let below = Em::new(0.66) / scale;
let mut map = StyleMap::new(); let mut map = StyleMap::new();
map.set(TextNode::SIZE, TextSize(size.into())); map.set(TextNode::SIZE, TextSize(size.into()));
map.set(TextNode::WEIGHT, FontWeight::BOLD); map.set(TextNode::WEIGHT, FontWeight::BOLD);
map.set(BlockNode::ABOVE, VNode::strong(above.into())); map.set(BlockNode::ABOVE, VNode::block_around(above.into()));
map.set(BlockNode::BELOW, VNode::strong(below.into())); map.set(BlockNode::BELOW, VNode::block_around(below.into()));
Ok(realized.styled_with_map(map)) Ok(realized.styled_with_map(map))
} }

View File

@ -410,20 +410,26 @@ impl Fold for FontFeatures {
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct SpaceNode; pub struct SpaceNode;
#[node] #[node(Behave)]
impl SpaceNode { impl SpaceNode {
fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> {
Ok(Self.pack()) Ok(Self.pack())
} }
} }
impl Behave for SpaceNode {
fn behaviour(&self) -> Behaviour {
Behaviour::Weak(2)
}
}
/// A line break. /// A line break.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct LinebreakNode { pub struct LinebreakNode {
pub justify: bool, pub justify: bool,
} }
#[node] #[node(Behave)]
impl LinebreakNode { impl LinebreakNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let justify = args.named("justify")?.unwrap_or(false); let justify = args.named("justify")?.unwrap_or(false);
@ -431,6 +437,12 @@ impl LinebreakNode {
} }
} }
impl Behave for LinebreakNode {
fn behaviour(&self) -> Behaviour {
Behaviour::Destructive
}
}
/// A smart quote. /// A smart quote.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct SmartQuoteNode { pub struct SmartQuoteNode {

View File

@ -1,30 +1,19 @@
use std::cmp::Ordering;
use typst::util::EcoString;
use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_bidi::{BidiInfo, Level as BidiLevel};
use unicode_script::{Script, UnicodeScript}; use unicode_script::{Script, UnicodeScript};
use xi_unicode::LineBreakIterator; use xi_unicode::LineBreakIterator;
use super::{shape, Lang, Quoter, Quotes, ShapedText, TextNode}; use typst::model::Key;
use crate::layout::Spacing;
use super::{
shape, Lang, LinebreakNode, Quoter, Quotes, ShapedText, SmartQuoteNode, SpaceNode,
TextNode,
};
use crate::layout::{HNode, Spacing};
use crate::prelude::*; use crate::prelude::*;
/// Arrange text, spacing and inline-level nodes into a paragraph. /// Arrange text, spacing and inline-level nodes into a paragraph.
#[derive(Hash)] #[derive(Hash)]
pub struct ParNode(pub StyleVec<ParChild>); pub struct ParNode(pub StyleVec<Content>);
/// A uniformly styled atomic piece of a paragraph.
#[derive(Hash, PartialEq)]
pub enum ParChild {
/// A chunk of text.
Text(EcoString),
/// A single or double smart quote.
Quote { double: bool },
/// Horizontal spacing between other children.
Spacing(Spacing),
/// Arbitrary inline-level content.
Inline(Content),
}
#[node(LayoutBlock)] #[node(LayoutBlock)]
impl ParNode { impl ParNode {
@ -84,26 +73,6 @@ impl Debug for ParNode {
} }
} }
impl Debug for ParChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Text(text) => write!(f, "Text({:?})", text),
Self::Quote { double } => write!(f, "Quote({double})"),
Self::Spacing(kind) => write!(f, "{:?}", kind),
Self::Inline(inline) => inline.fmt(f),
}
}
}
impl PartialOrd for ParChild {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b),
_ => None,
}
}
}
/// A horizontal alignment. /// A horizontal alignment.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct HorizontalAlign(pub GenAlign); pub struct HorizontalAlign(pub GenAlign);
@ -426,43 +395,52 @@ fn collect<'a>(
while let Some((child, map)) = iter.next() { while let Some((child, map)) = iter.next() {
let styles = map.chain(styles); let styles = map.chain(styles);
let segment = match child { let segment = if child.is::<SpaceNode>() {
ParChild::Text(text) => { full.push(' ');
Segment::Text(1)
} else if let Some(node) = child.downcast::<TextNode>() {
let prev = full.len(); let prev = full.len();
if let Some(case) = styles.get(TextNode::CASE) { if let Some(case) = styles.get(TextNode::CASE) {
full.push_str(&case.apply(text)); full.push_str(&case.apply(&node.0));
} else { } else {
full.push_str(text); full.push_str(&node.0);
} }
Segment::Text(full.len() - prev) Segment::Text(full.len() - prev)
} } else if let Some(node) = child.downcast::<LinebreakNode>() {
&ParChild::Quote { double } => { let c = if node.justify { '\u{2028}' } else { '\n' };
full.push(c);
Segment::Text(c.len_utf8())
} else if let Some(node) = child.downcast::<SmartQuoteNode>() {
let prev = full.len(); let prev = full.len();
if styles.get(TextNode::SMART_QUOTES) { if styles.get(TextNode::SMART_QUOTES) {
let lang = styles.get(TextNode::LANG); let lang = styles.get(TextNode::LANG);
let region = styles.get(TextNode::REGION); let region = styles.get(TextNode::REGION);
let quotes = Quotes::from_lang(lang, region); let quotes = Quotes::from_lang(lang, region);
let peeked = iter.peek().and_then(|(child, _)| match child { let peeked = iter.peek().and_then(|(child, _)| {
ParChild::Text(text) => text.chars().next(), if let Some(node) = child.downcast::<TextNode>() {
ParChild::Quote { .. } => Some('"'), node.0.chars().next()
ParChild::Spacing(_) => Some(SPACING_REPLACE), } else if child.is::<SmartQuoteNode>() {
ParChild::Inline(_) => Some(NODE_REPLACE), Some('"')
} else if child.is::<SpaceNode>() || child.is::<HNode>() {
Some(SPACING_REPLACE)
} else {
Some(NODE_REPLACE)
}
}); });
full.push_str(quoter.quote(&quotes, double, peeked)); full.push_str(quoter.quote(&quotes, node.double, peeked));
} else { } else {
full.push(if double { '"' } else { '\'' }); full.push(if node.double { '"' } else { '\'' });
} }
Segment::Text(full.len() - prev) Segment::Text(full.len() - prev)
} } else if let Some(&node) = child.downcast::<HNode>() {
&ParChild::Spacing(spacing) => {
full.push(SPACING_REPLACE); full.push(SPACING_REPLACE);
Segment::Spacing(spacing) Segment::Spacing(node.amount)
} } else if child.has::<dyn LayoutInline>() {
ParChild::Inline(inline) => {
full.push(NODE_REPLACE); full.push(NODE_REPLACE);
Segment::Inline(inline) Segment::Inline(child)
} } else {
panic!("unexpected par child: {child:?}");
}; };
if let Some(last) = full.chars().last() { if let Some(last) = full.chars().last() {
@ -608,7 +586,7 @@ fn is_compatible(a: Script, b: Script) -> bool {
/// paragraph. /// paragraph.
fn shared_get<'a, K: Key<'a>>( fn shared_get<'a, K: Key<'a>>(
styles: StyleChain<'a>, styles: StyleChain<'a>,
children: &StyleVec<ParChild>, children: &StyleVec<Content>,
key: K, key: K,
) -> Option<K::Output> { ) -> Option<K::Output> {
children children

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB