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::*;
/// Additional methods on content.
@ -33,31 +34,31 @@ pub trait ContentExt {
impl ContentExt for Content {
fn strong(self) -> Self {
text::StrongNode(self).pack()
crate::text::StrongNode(self).pack()
}
fn emph(self) -> Self {
text::EmphNode(self).pack()
crate::text::EmphNode(self).pack()
}
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 {
layout::BoxNode { sizing, child: self }.pack()
crate::layout::BoxNode { sizing, child: self }.pack()
}
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 {
layout::PadNode { padding, child: self }.pack()
crate::layout::PadNode { padding, child: self }.pack()
}
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 {
@ -73,16 +74,16 @@ impl ContentExt for Content {
pub trait StyleMapExt {
/// Set a font family composed of a preferred family and existing families
/// 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 {
fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) {
fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain) {
self.set(
text::TextNode::FAMILY,
text::FallbackList(
crate::text::TextNode::FAMILY,
crate::text::FallbackList(
std::iter::once(preferred)
.chain(existing.get(text::TextNode::FAMILY).0.iter().cloned())
.chain(existing.get(crate::text::TextNode::FAMILY).0.iter().cloned())
.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,
}
#[node]
#[node(Behave)]
impl ColbreakNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let weak = args.named("weak")?.unwrap_or(false);
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 {
/// The spacing between the previous and this block.
#[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.
#[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> {
Ok(Self(args.eat()?.unwrap_or_default()).pack())
}
fn set(...) {
let spacing = args.named("spacing")?.map(VNode::weak);
styles.set_opt(Self::ABOVE, args.named("above")?.map(VNode::strong).or(spacing));
styles.set_opt(Self::BELOW, args.named("below")?.map(VNode::strong).or(spacing));
let spacing = args.named("spacing")?.map(VNode::block_spacing);
styles.set_opt(
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, PlaceNode, Spacing, VNode};
use crate::layout::BlockNode;
use super::{AlignNode, ColbreakNode, PlaceNode, Spacing, VNode};
use crate::prelude::*;
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
/// the contents of boxes.
#[derive(Hash)]
pub struct FlowNode(pub StyleVec<FlowChild>);
/// 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,
}
pub struct FlowNode(pub StyleVec<Content>);
#[node(LayoutBlock)]
impl FlowNode {}
@ -33,20 +19,18 @@ impl LayoutBlock for FlowNode {
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
let mut layouter = FlowLayouter::new(regions, styles);
let mut layouter = FlowLayouter::new(regions);
for (child, map) in self.0.iter() {
let styles = map.chain(&styles);
match child {
FlowChild::Spacing(node) => {
layouter.layout_spacing(node, styles);
}
FlowChild::Block(block) => {
layouter.layout_block(world, block, styles)?;
}
FlowChild::Colbreak => {
if let Some(&node) = child.downcast::<VNode>() {
layouter.layout_spacing(node.amount, styles);
} else if child.has::<dyn LayoutBlock>() {
layouter.layout_block(world, child, styles)?;
} else if child.is::<ColbreakNode>() {
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.
struct FlowLayouter<'a> {
struct FlowLayouter {
/// The regions to layout children into.
regions: Regions,
/// The shared styles.
shared: StyleChain<'a>,
/// Whether the flow should expand to fill the region.
expand: Axes<bool>,
/// The full size of `regions.size` that was available before we started
@ -95,8 +58,6 @@ struct FlowLayouter<'a> {
used: Size,
/// The sum of fractions in the current region.
fr: Fr,
/// The spacing below the last block.
below: Option<VNode>,
/// Spacing and layouted blocks.
items: Vec<FlowItem>,
/// Finished frames for previous regions.
@ -115,9 +76,9 @@ enum FlowItem {
Placed(Frame),
}
impl<'a> FlowLayouter<'a> {
impl FlowLayouter {
/// Create a new flow layouter.
fn new(regions: &Regions, shared: StyleChain<'a>) -> Self {
fn new(regions: &Regions) -> Self {
let expand = regions.expand;
let full = regions.first;
@ -127,20 +88,18 @@ impl<'a> FlowLayouter<'a> {
Self {
regions,
shared,
expand,
full,
used: Size::zero(),
fr: Fr::zero(),
below: None,
items: vec![],
finished: vec![],
}
}
/// Layout spacing.
fn layout_spacing(&mut self, node: &VNode, styles: StyleChain) {
match node.amount {
/// Actually layout the spacing.
fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) {
match spacing {
Spacing::Relative(v) => {
// Resolve the spacing and limit it to the remaining space.
let resolved = v.resolve(styles).relative_to(self.full.y);
@ -154,10 +113,6 @@ impl<'a> FlowLayouter<'a> {
self.fr += v;
}
}
if node.weak || node.amount.is_fractional() {
self.below = None;
}
}
/// Layout a block.
@ -172,19 +127,9 @@ impl<'a> FlowLayouter<'a> {
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
// aligned later.
let mut is_placed = false;
if let Some(placed) = block.downcast::<PlaceNode>() {
is_placed = true;
if placed.out_of_flow() {
let frame = block.layout_block(world, &self.regions, styles)?.remove(0);
self.items.push(FlowItem::Placed(frame));
@ -205,6 +150,7 @@ impl<'a> FlowLayouter<'a> {
.unwrap_or(Align::Top),
);
// Layout the block itself.
let frames = block.layout_block(world, &self.regions, styles)?;
let len = frames.len();
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(())
}
@ -272,7 +214,6 @@ impl<'a> FlowLayouter<'a> {
self.full = self.regions.first;
self.used = Size::zero();
self.fr = Fr::zero();
self.below = None;
self.finished.push(output);
}

View File

@ -32,16 +32,18 @@ use typst::diag::SourceResult;
use typst::frame::Frame;
use typst::geom::*;
use typst::model::{
capability, Content, Node, SequenceNode, Show, StyleChain, StyleEntry, StyleVec,
capability, Content, Node, SequenceNode, Show, StyleChain, StyleEntry,
StyleVecBuilder, StyledNode, Target,
};
use typst::World;
use crate::core::BehavedBuilder;
use crate::prelude::*;
use crate::structure::{
DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST,
};
use crate::text::{
LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode,
LinebreakNode, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode,
};
/// Root-level layout.
@ -468,41 +470,17 @@ impl Default for DocBuilder<'_> {
/// Accepts flow content.
#[derive(Default)]
struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>, bool);
struct FlowBuilder<'a>(BehavedBuilder<'a>, bool);
impl<'a> FlowBuilder<'a> {
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;
self.1 = false;
if content.is::<ParbreakNode>() {
self.1 = true;
} else if let Some(colbreak) = content.downcast::<ColbreakNode>() {
if colbreak.weak {
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.is::<VNode>() || content.is::<ColbreakNode>() {
self.0.push(content.clone(), styles);
} else if content.has::<dyn LayoutBlock>() {
if !last_was_parbreak {
let tight = if let Some(node) = content.downcast::<ListNode>() {
@ -517,17 +495,16 @@ impl<'a> FlowBuilder<'a> {
if tight {
let leading = styles.get(ParNode::LEADING);
let spacing = VNode::weak(leading.into());
self.0.weak(FlowChild::Spacing(spacing), styles, 1);
let spacing = VNode::list_attach(leading.into());
self.0.push(spacing.pack(), styles);
}
}
let child = FlowChild::Block(content.clone());
if content.is::<PlaceNode>() {
self.0.ignorant(child, styles);
} else {
self.0.supportive(child, styles);
}
let above = styles.get(BlockNode::ABOVE);
let below = styles.get(BlockNode::BELOW);
self.0.push(above.pack(), styles);
self.0.push(content.clone(), styles);
self.0.push(below.pack(), styles);
} else {
return false;
}
@ -549,43 +526,22 @@ impl<'a> FlowBuilder<'a> {
/// Accepts paragraph content.
#[derive(Default)]
struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>);
struct ParBuilder<'a>(BehavedBuilder<'a>);
impl<'a> ParBuilder<'a> {
fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool {
// Weak par elements:
// Weakness | Element
// 0 | weak fractional spacing
// 1 | weak spacing
// 2 | space
if content.is::<SpaceNode>() {
self.0.weak(ParChild::Text(' '.into()), styles, 2);
} else if let Some(linebreak) = content.downcast::<LinebreakNode>() {
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;
if content.is::<SpaceNode>()
|| content.is::<LinebreakNode>()
|| content.is::<HNode>()
|| content.is::<SmartQuoteNode>()
|| content.is::<TextNode>()
|| content.has::<dyn LayoutInline>()
{
self.0.push(content.clone(), styles);
return true;
}
true
false
}
fn finish(self, parent: &mut Builder<'a>) {
@ -600,10 +556,14 @@ impl<'a> ParBuilder<'a> {
if !indent.is_zero()
&& children
.items()
.find_map(|child| match child {
ParChild::Spacing(_) => None,
ParChild::Text(_) | ParChild::Quote { .. } => Some(true),
ParChild::Inline(_) => Some(false),
.find_map(|child| {
if child.is::<TextNode>() || child.is::<SmartQuoteNode>() {
Some(true)
} else if child.has::<dyn LayoutInline>() {
Some(false)
} else {
None
}
})
.unwrap_or_default()
&& parent
@ -611,14 +571,10 @@ impl<'a> ParBuilder<'a> {
.0
.items()
.rev()
.find_map(|child| match child {
FlowChild::Spacing(_) => None,
FlowChild::Block(content) => Some(content.is::<ParNode>()),
FlowChild::Colbreak => Some(false),
})
.unwrap_or_default()
.find(|child| child.has::<dyn LayoutBlock>())
.map_or(false, |child| child.is::<ParNode>())
{
children.push_front(ParChild::Spacing(indent.into()));
children.push_front(HNode::strong(indent.into()).pack());
}
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)]
pub struct PlaceNode(pub Content);
#[node(LayoutBlock)]
#[node(LayoutBlock, Behave)]
impl PlaceNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
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())
}
}
impl Behave for PlaceNode {
fn behaviour(&self) -> Behaviour {
Behaviour::Ignorant
}
}

View File

@ -5,11 +5,13 @@ use crate::prelude::*;
/// Horizontal spacing.
#[derive(Debug, Copy, Clone, Hash)]
pub struct HNode {
/// The amount of horizontal spacing.
pub amount: Spacing,
/// Whether the node is weak, see also [`Behaviour`].
pub weak: bool,
}
#[node]
#[node(Behave)]
impl HNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let amount = args.expect("spacing")?;
@ -18,31 +20,98 @@ impl HNode {
}
}
/// Vertical spacing.
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd)]
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.
impl HNode {
/// Normal strong spacing.
pub fn strong(amount: Spacing) -> Self {
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 {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let amount = args.expect("spacing")?;
let weak = args.named("weak")?.unwrap_or(false);
Ok(Self { amount, weak }.pack())
let node = if args.named("weak")?.unwrap_or(false) {
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.
pub mod base;
pub mod core;
pub mod graphics;
pub mod layout;
pub mod math;
@ -8,8 +9,6 @@ pub mod prelude;
pub mod structure;
pub mod text;
mod ext;
use typst::geom::{Align, Color, Dir, GenAlign};
use typst::model::{LangItems, Node, Scope, StyleMap};

View File

@ -1,20 +1,32 @@
//! Helpful imports for creating library functionality.
#[doc(no_inline)]
pub use std::fmt::{self, Debug, Formatter};
#[doc(no_inline)]
pub use std::num::NonZeroUsize;
#[doc(no_inline)]
pub use comemo::Tracked;
#[doc(no_inline)]
pub use typst::diag::{bail, error, with_alternative, At, SourceResult, StrResult};
#[doc(no_inline)]
pub use typst::frame::*;
#[doc(no_inline)]
pub use typst::geom::*;
#[doc(no_inline)]
pub use typst::model::{
array, capability, castable, dict, dynamic, format_str, node, Args, Array, Cast,
Content, Dict, Finalize, Fold, Func, Key, Node, RecipeId, Resolve, Scope, Show,
Smart, Str, StyleChain, StyleMap, StyleVec, Value, Vm,
Content, Dict, Finalize, Fold, Func, Node, RecipeId, Resolve, Show, Smart, Str,
StyleChain, StyleMap, StyleVec, Value, Vm,
};
#[doc(no_inline)]
pub use typst::syntax::{Span, Spanned};
#[doc(no_inline)]
pub use typst::util::{format_eco, EcoString};
#[doc(no_inline)]
pub use typst::World;
pub use super::ext::{ContentExt, StyleMapExt};
pub use super::layout::{LayoutBlock, LayoutInline, Regions};
#[doc(no_inline)]
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,
realized: Content,
) -> SourceResult<Content> {
let size = Em::new(match self.level.get() {
let scale = match self.level.get() {
1 => 1.4,
2 => 1.2,
_ => 1.0,
});
};
let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 });
let below = Em::new(0.66);
let size = Em::new(scale);
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();
map.set(TextNode::SIZE, TextSize(size.into()));
map.set(TextNode::WEIGHT, FontWeight::BOLD);
map.set(BlockNode::ABOVE, VNode::strong(above.into()));
map.set(BlockNode::BELOW, VNode::strong(below.into()));
map.set(BlockNode::ABOVE, VNode::block_around(above.into()));
map.set(BlockNode::BELOW, VNode::block_around(below.into()));
Ok(realized.styled_with_map(map))
}

View File

@ -410,20 +410,26 @@ impl Fold for FontFeatures {
#[derive(Debug, Clone, Hash)]
pub struct SpaceNode;
#[node]
#[node(Behave)]
impl SpaceNode {
fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> {
Ok(Self.pack())
}
}
impl Behave for SpaceNode {
fn behaviour(&self) -> Behaviour {
Behaviour::Weak(2)
}
}
/// A line break.
#[derive(Debug, Clone, Hash)]
pub struct LinebreakNode {
pub justify: bool,
}
#[node]
#[node(Behave)]
impl LinebreakNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
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.
#[derive(Debug, Clone, Hash)]
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_script::{Script, UnicodeScript};
use xi_unicode::LineBreakIterator;
use super::{shape, Lang, Quoter, Quotes, ShapedText, TextNode};
use crate::layout::Spacing;
use typst::model::Key;
use super::{
shape, Lang, LinebreakNode, Quoter, Quotes, ShapedText, SmartQuoteNode, SpaceNode,
TextNode,
};
use crate::layout::{HNode, Spacing};
use crate::prelude::*;
/// Arrange text, spacing and inline-level nodes into a paragraph.
#[derive(Hash)]
pub struct ParNode(pub StyleVec<ParChild>);
/// 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),
}
pub struct ParNode(pub StyleVec<Content>);
#[node(LayoutBlock)]
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.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct HorizontalAlign(pub GenAlign);
@ -426,43 +395,52 @@ fn collect<'a>(
while let Some((child, map)) = iter.next() {
let styles = map.chain(styles);
let segment = match child {
ParChild::Text(text) => {
let segment = if child.is::<SpaceNode>() {
full.push(' ');
Segment::Text(1)
} else if let Some(node) = child.downcast::<TextNode>() {
let prev = full.len();
if let Some(case) = styles.get(TextNode::CASE) {
full.push_str(&case.apply(text));
full.push_str(&case.apply(&node.0));
} else {
full.push_str(text);
full.push_str(&node.0);
}
Segment::Text(full.len() - prev)
}
&ParChild::Quote { double } => {
} else if let Some(node) = child.downcast::<LinebreakNode>() {
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();
if styles.get(TextNode::SMART_QUOTES) {
let lang = styles.get(TextNode::LANG);
let region = styles.get(TextNode::REGION);
let quotes = Quotes::from_lang(lang, region);
let peeked = iter.peek().and_then(|(child, _)| match child {
ParChild::Text(text) => text.chars().next(),
ParChild::Quote { .. } => Some('"'),
ParChild::Spacing(_) => Some(SPACING_REPLACE),
ParChild::Inline(_) => Some(NODE_REPLACE),
let peeked = iter.peek().and_then(|(child, _)| {
if let Some(node) = child.downcast::<TextNode>() {
node.0.chars().next()
} else if child.is::<SmartQuoteNode>() {
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 {
full.push(if double { '"' } else { '\'' });
full.push(if node.double { '"' } else { '\'' });
}
Segment::Text(full.len() - prev)
}
&ParChild::Spacing(spacing) => {
} else if let Some(&node) = child.downcast::<HNode>() {
full.push(SPACING_REPLACE);
Segment::Spacing(spacing)
}
ParChild::Inline(inline) => {
Segment::Spacing(node.amount)
} else if child.has::<dyn LayoutInline>() {
full.push(NODE_REPLACE);
Segment::Inline(inline)
}
Segment::Inline(child)
} else {
panic!("unexpected par child: {child:?}");
};
if let Some(last) = full.chars().last() {
@ -608,7 +586,7 @@ fn is_compatible(a: Script, b: Script) -> bool {
/// paragraph.
fn shared_get<'a, K: Key<'a>>(
styles: StyleChain<'a>,
children: &StyleVec<ParChild>,
children: &StyleVec<Content>,
key: K,
) -> Option<K::Output> {
children

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB