From e74ae6ce70d4c6ca006613eadf07f920951789e3 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 7 Jan 2022 21:24:36 +0100 Subject: [PATCH] Make all nodes into classes --- macros/src/lib.rs | 83 ++++++--- src/eval/mod.rs | 10 +- src/eval/node.rs | 77 +++++--- src/eval/value.rs | 8 +- src/geom/transform.rs | 2 +- src/layout/mod.rs | 17 +- src/library/align.rs | 18 +- src/library/columns.rs | 46 +++-- src/library/deco.rs | 79 +++++++++ src/library/flow.rs | 4 +- src/library/grid.rs | 36 ++-- src/library/heading.rs | 6 +- src/library/image.rs | 81 ++++----- src/library/link.rs | 33 ++-- src/library/list.rs | 28 ++- src/library/mod.rs | 59 ++++--- src/library/pad.rs | 40 +++-- src/library/page.rs | 27 ++- src/library/par.rs | 35 ++-- src/library/placed.rs | 46 ++--- src/library/shape.rs | 236 ++++++++++++------------- src/library/sized.rs | 29 +-- src/library/spacing.rs | 28 +-- src/library/stack.rs | 22 +-- src/library/text.rs | 81 ++------- src/library/transform.rs | 122 ++++++++----- src/library/utility.rs | 33 ++-- tests/ref/code/repr.png | Bin 9874 -> 9933 bytes tests/ref/text/par.png | Bin 8169 -> 9364 bytes tests/typ/layout/columns.typ | 10 +- tests/typ/layout/shape-circle.typ | 2 +- tests/typ/layout/shape-fill-stroke.typ | 2 +- tests/typ/text/par.typ | 17 +- 33 files changed, 726 insertions(+), 591 deletions(-) create mode 100644 src/library/deco.rs diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 16fb78858..b667d2e26 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -7,48 +7,82 @@ use syn::parse_quote; use syn::spanned::Spanned; use syn::{Error, Result}; -/// Generate node properties. +/// Turn a node into a class. #[proc_macro_attribute] -pub fn properties(_: TokenStream, item: TokenStream) -> TokenStream { +pub fn class(_: TokenStream, item: TokenStream) -> TokenStream { let impl_block = syn::parse_macro_input!(item as syn::ItemImpl); expand(impl_block).unwrap_or_else(|err| err.to_compile_error()).into() } -/// Expand a property impl block for a node. +/// Expand an impl block for a node. fn expand(mut impl_block: syn::ItemImpl) -> Result { // Split the node type into name and generic type arguments. + let params = &impl_block.generics.params; let self_ty = &*impl_block.self_ty; let (self_name, self_args) = parse_self(self_ty)?; - // Rewrite the const items from values to keys. - let mut modules = vec![]; - for item in &mut impl_block.items { - if let syn::ImplItem::Const(item) = item { - let module = process_const( - item, - &impl_block.generics, - self_ty, - &self_name, - &self_args, - )?; - modules.push(module); + let module = quote::format_ident!("{}_types", self_name); + + let mut key_modules = vec![]; + let mut construct = None; + let mut set = None; + + for item in std::mem::take(&mut impl_block.items) { + match item { + syn::ImplItem::Const(mut item) => { + key_modules.push(process_const( + &mut item, params, self_ty, &self_name, &self_args, + )?); + impl_block.items.push(syn::ImplItem::Const(item)); + } + syn::ImplItem::Method(method) => { + match method.sig.ident.to_string().as_str() { + "construct" => construct = Some(method), + "set" => set = Some(method), + _ => return Err(Error::new(method.span(), "unexpected method")), + } + } + _ => return Err(Error::new(item.span(), "unexpected item")), } } + let construct = + construct.ok_or_else(|| Error::new(impl_block.span(), "missing constructor"))?; + + let set = if impl_block.items.is_empty() { + set.unwrap_or_else(|| { + parse_quote! { + fn set(_: &mut Args, _: &mut StyleMap) -> TypResult<()> { + Ok(()) + } + } + }) + } else { + set.ok_or_else(|| Error::new(impl_block.span(), "missing set method"))? + }; + // Put everything into a module with a hopefully unique type to isolate // it from the outside. - let module = quote::format_ident!("{}_types", self_name); Ok(quote! { #[allow(non_snake_case)] mod #module { use std::any::TypeId; use std::marker::PhantomData; use once_cell::sync::Lazy; - use crate::eval::{Nonfolding, Property}; + use crate::eval::{Construct, Nonfolding, Property, Set}; use super::*; #impl_block - #(#modules)* + + impl<#params> Construct for #self_ty { + #construct + } + + impl<#params> Set for #self_ty { + #set + } + + #(#key_modules)* } }) } @@ -82,7 +116,7 @@ fn parse_self(self_ty: &syn::Type) -> Result<(String, Vec<&syn::Type>)> { /// Process a single const item. fn process_const( item: &mut syn::ImplItemConst, - impl_generics: &syn::Generics, + params: &syn::punctuated::Punctuated, self_ty: &syn::Type, self_name: &str, self_args: &[&syn::Type], @@ -95,7 +129,6 @@ fn process_const( let value_ty = &item.ty; // ... but the real type of the const becomes Key<#key_args>. - let key_params = &impl_generics.params; let key_args = quote! { #value_ty #(, #self_args)* }; // The display name, e.g. `TextNode::STRONG`. @@ -107,7 +140,7 @@ fn process_const( let mut folder = None; let mut nonfolding = Some(quote! { - impl<#key_params> Nonfolding for Key<#key_args> {} + impl<#params> Nonfolding for Key<#key_args> {} }); // Look for a folding function like `#[fold(u64::add)]`. @@ -132,16 +165,16 @@ fn process_const( mod #module_name { use super::*; - pub struct Key(pub PhantomData<(T, #key_args)>); + pub struct Key(pub PhantomData<(VALUE, #key_args)>); - impl<#key_params> Copy for Key<#key_args> {} - impl<#key_params> Clone for Key<#key_args> { + impl<#params> Copy for Key<#key_args> {} + impl<#params> Clone for Key<#key_args> { fn clone(&self) -> Self { *self } } - impl<#key_params> Property for Key<#key_args> { + impl<#params> Property for Key<#key_args> { type Value = #value_ty; const NAME: &'static str = #name; diff --git a/src/eval/mod.rs b/src/eval/mod.rs index e8c8fcd29..c16c22083 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -248,7 +248,7 @@ impl Eval for ListNode { fn eval(&self, ctx: &mut EvalContext) -> TypResult { Ok(Node::block(library::ListNode { child: self.body().eval(ctx)?.into_block(), - labelling: library::Unordered, + kind: library::Unordered, })) } } @@ -259,7 +259,7 @@ impl Eval for EnumNode { fn eval(&self, ctx: &mut EvalContext) -> TypResult { Ok(Node::block(library::ListNode { child: self.body().eval(ctx)?.into_block(), - labelling: library::Ordered(self.number()), + kind: library::Ordered(self.number()), })) } } @@ -450,6 +450,7 @@ impl Eval for CallExpr { type Output = Value; fn eval(&self, ctx: &mut EvalContext) -> TypResult { + let span = self.callee().span(); let callee = self.callee().eval(ctx)?; let mut args = self.args().eval(ctx)?; @@ -470,13 +471,14 @@ impl Eval for CallExpr { } Value::Class(class) => { - let node = class.construct(ctx, &mut args)?; + let point = || Tracepoint::Call(Some(class.name().to_string())); + let node = class.construct(ctx, &mut args).trace(point, self.span())?; args.finish()?; Ok(Value::Node(node)) } v => bail!( - self.callee().span(), + span, "expected callable or collection, found {}", v.type_name(), ), diff --git a/src/eval/node.rs b/src/eval/node.rs index ecbee8eea..d909fc7d6 100644 --- a/src/eval/node.rs +++ b/src/eval/node.rs @@ -10,7 +10,7 @@ use crate::diag::StrResult; use crate::geom::SpecAxis; use crate::layout::{Layout, PackedNode, RootNode}; use crate::library::{ - FlowChild, FlowNode, PageNode, ParChild, ParNode, PlacedNode, SpacingKind, TextNode, + FlowChild, FlowNode, PageNode, ParChild, ParNode, PlaceNode, SpacingKind, TextNode, }; use crate::util::EcoString; @@ -98,6 +98,10 @@ impl Node { /// Style this node with a full style map. pub fn styled_with_map(mut self, styles: StyleMap) -> Self { + if styles.is_empty() { + return self; + } + if let Self::Sequence(vec) = &mut self { if let [styled] = vec.as_mut_slice() { styled.map.apply(&styles); @@ -193,7 +197,7 @@ impl Packer { /// Finish up and return the resulting flow. fn into_block(mut self) -> PackedNode { - self.parbreak(None); + self.parbreak(None, false); FlowNode(self.flow.children).pack() } @@ -209,7 +213,7 @@ impl Packer { Node::Space => { // A text space is "soft", meaning that it can be eaten up by // adjacent line breaks or explicit spacings. - self.par.last.soft(Styled::new(ParChild::text(' '), styles)); + self.par.last.soft(Styled::new(ParChild::text(' '), styles), false); } Node::Linebreak => { // A line break eats up surrounding text spaces. @@ -222,12 +226,12 @@ impl Packer { // styles (`Some(_)`) whereas paragraph breaks forced by // incompatibility take their styles from the preceding // paragraph. - self.parbreak(Some(styles)); + self.parbreak(Some(styles), true); } Node::Colbreak => { // Explicit column breaks end the current paragraph and then // discards the paragraph break. - self.parbreak(None); + self.parbreak(None, false); self.make_flow_compatible(&styles); self.flow.children.push(Styled::new(FlowChild::Skip, styles)); self.flow.last.hard(); @@ -252,7 +256,7 @@ impl Packer { Node::Spacing(SpecAxis::Vertical, kind) => { // Explicit vertical spacing ends the current paragraph and then // discards the paragraph break. - self.parbreak(None); + self.parbreak(None, false); self.make_flow_compatible(&styles); self.flow.children.push(Styled::new(FlowChild::Spacing(kind), styles)); self.flow.last.hard(); @@ -284,14 +288,15 @@ impl Packer { /// Insert an inline-level element into the current paragraph. fn push_inline(&mut self, child: Styled) { - if let Some(styled) = self.par.last.any() { - self.push_coalescing(styled); - } - // The node must be both compatible with the current page and the // current paragraph. self.make_flow_compatible(&child.map); self.make_par_compatible(&child.map); + + if let Some(styled) = self.par.last.any() { + self.push_coalescing(styled); + } + self.push_coalescing(child); self.par.last.any(); } @@ -314,13 +319,13 @@ impl Packer { /// Insert a block-level element into the current flow. fn push_block(&mut self, node: Styled) { - let placed = node.item.is::(); + let placed = node.item.is::(); - self.parbreak(None); + self.parbreak(Some(node.map.clone()), false); self.make_flow_compatible(&node.map); self.flow.children.extend(self.flow.last.any()); self.flow.children.push(node.map(FlowChild::Node)); - self.parbreak(None); + self.parbreak(None, false); // Prevent paragraph spacing between the placed node and the paragraph // below it. @@ -330,18 +335,13 @@ impl Packer { } /// Advance to the next paragraph. - fn parbreak(&mut self, break_styles: Option) { + fn parbreak(&mut self, break_styles: Option, important: bool) { // Erase any styles that will be inherited anyway. let Builder { mut children, styles, .. } = mem::take(&mut self.par); for Styled { map, .. } in &mut children { map.erase(&styles); } - // For explicit paragraph breaks, `break_styles` is already `Some(_)`. - // For page breaks due to incompatibility, we fall back to the styles - // of the preceding paragraph. - let break_styles = break_styles.unwrap_or_else(|| styles.clone()); - // We don't want empty paragraphs. if !children.is_empty() { // The paragraph's children are all compatible with the page, so the @@ -352,14 +352,30 @@ impl Packer { self.flow.children.push(Styled::new(FlowChild::Node(par), styles)); } + // Actually styled breaks have precedence over whatever was before. + if break_styles.is_some() { + if let Last::Soft(_, false) = self.flow.last { + self.flow.last = Last::Any; + } + } + + // For explicit paragraph breaks, `break_styles` is already `Some(_)`. + // For page breaks due to incompatibility, we fall back to the styles + // of the preceding thing. + let break_styles = break_styles + .or_else(|| self.flow.children.last().map(|styled| styled.map.clone())) + .unwrap_or_default(); + // Insert paragraph spacing. - self.flow.last.soft(Styled::new(FlowChild::Break, break_styles)); + self.flow + .last + .soft(Styled::new(FlowChild::Break, break_styles), important); } /// Advance to the next page. fn pagebreak(&mut self) { if self.top { - self.parbreak(None); + self.parbreak(None, false); // Take the flow and erase any styles that will be inherited anyway. let Builder { mut children, styles, .. } = mem::take(&mut self.flow); @@ -381,7 +397,7 @@ impl Packer { } if !self.par.styles.compatible::(styles) { - self.parbreak(None); + self.parbreak(Some(styles.clone()), false); self.par.styles = styles.clone(); return; } @@ -441,8 +457,10 @@ enum Last { /// Hard nodes: Linebreaks and explicit spacing. Hard, /// Soft nodes: Word spaces and paragraph breaks. These are saved here - /// temporarily and then applied once an `Any` node appears. - Soft(N), + /// temporarily and then applied once an `Any` node appears. The boolean + /// says whether this soft node is "important" and preferrable to other soft + /// nodes (that is the case for explicit paragraph breaks). + Soft(N, bool), } impl Last { @@ -450,16 +468,19 @@ impl Last { /// now if currently in `Soft` state. fn any(&mut self) -> Option { match mem::replace(self, Self::Any) { - Self::Soft(soft) => Some(soft), + Self::Soft(soft, _) => Some(soft), _ => None, } } /// Transition into the `Soft` state, but only if in `Any`. Otherwise, the /// soft node is discarded. - fn soft(&mut self, soft: N) { - if let Self::Any = self { - *self = Self::Soft(soft); + fn soft(&mut self, soft: N, important: bool) { + if matches!( + (&self, important), + (Self::Any, _) | (Self::Soft(_, false), true) + ) { + *self = Self::Soft(soft, important); } } diff --git a/src/eval/value.rs b/src/eval/value.rs index 0995ab756..3b1ef3f7a 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -397,7 +397,13 @@ primitive! { EcoString: "string", Str } primitive! { Array: "array", Array } primitive! { Dict: "dictionary", Dict } primitive! { Node: "template", Node } -primitive! { Function: "function", Func } +primitive! { Function: "function", + Func, + Class(v) => Function::new( + Some(v.name().clone()), + move |ctx, args| v.construct(ctx, args).map(Value::Node) + ) +} primitive! { Class: "class", Class } impl Cast for Value { diff --git a/src/geom/transform.rs b/src/geom/transform.rs index ca44667b7..76615e755 100644 --- a/src/geom/transform.rs +++ b/src/geom/transform.rs @@ -30,7 +30,7 @@ impl Transform { } /// A scaling transform. - pub const fn scaling(sx: Relative, sy: Relative) -> Self { + pub const fn scale(sx: Relative, sy: Relative) -> Self { Self { sx, sy, ..Self::identity() } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 6935afc23..e272d1028 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -18,9 +18,9 @@ use std::rc::Rc; use crate::eval::{StyleChain, Styled}; use crate::font::FontStore; use crate::frame::Frame; -use crate::geom::{Align, Linear, Point, Sides, Size, Spec, Transform}; +use crate::geom::{Align, Linear, Point, Sides, Size, Spec}; use crate::image::ImageStore; -use crate::library::{AlignNode, PadNode, PageNode, SizedNode, TransformNode}; +use crate::library::{AlignNode, Move, PadNode, PageNode, SizedNode, TransformNode}; use crate::Context; /// The root layout node, a document consisting of top-level page runs. @@ -177,13 +177,12 @@ impl PackedNode { /// Transform this node's contents without affecting layout. pub fn moved(self, offset: Point) -> Self { - self.transformed(Transform::translation(offset.x, offset.y), Align::LEFT_TOP) - } - - /// Transform this node's contents without affecting layout. - pub fn transformed(self, transform: Transform, origin: Spec) -> Self { - if !transform.is_identity() { - TransformNode { transform, origin, child: self }.pack() + if !offset.is_zero() { + TransformNode { + kind: Move(offset.x, offset.y), + child: self, + } + .pack() } else { self } diff --git a/src/library/align.rs b/src/library/align.rs index e8dfabb1b..8eee116ec 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -3,14 +3,7 @@ use super::prelude::*; use super::ParNode; -/// `align`: Configure the alignment along the layouting axes. -pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult { - let aligns: Spec<_> = args.find().unwrap_or_default(); - let body: PackedNode = args.expect("body")?; - Ok(Value::block(body.aligned(aligns))) -} - -/// A node that aligns its child. +/// Align a node along the layouting axes. #[derive(Debug, Hash)] pub struct AlignNode { /// How to align the node horizontally and vertically. @@ -19,6 +12,15 @@ pub struct AlignNode { pub child: PackedNode, } +#[class] +impl AlignNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + let aligns: Spec<_> = args.find().unwrap_or_default(); + let body: PackedNode = args.expect("body")?; + Ok(Node::block(body.aligned(aligns))) + } +} + impl Layout for AlignNode { fn layout( &self, diff --git a/src/library/columns.rs b/src/library/columns.rs index ce02b5084..d2dc350f7 100644 --- a/src/library/columns.rs +++ b/src/library/columns.rs @@ -3,32 +3,34 @@ use super::prelude::*; use super::ParNode; -/// `columns`: Set content into multiple columns. -pub fn columns(_: &mut EvalContext, args: &mut Args) -> TypResult { - Ok(Value::block(ColumnsNode { - columns: args.expect("column count")?, - gutter: args.named("gutter")?.unwrap_or(Relative::new(0.04).into()), - child: args.expect("body")?, - })) -} - -/// `colbreak`: Start a new column. -pub fn colbreak(_: &mut EvalContext, _: &mut Args) -> TypResult { - Ok(Value::Node(Node::Colbreak)) -} - /// A node that separates a region into multiple equally sized columns. #[derive(Debug, Hash)] pub struct ColumnsNode { /// How many columns there should be. pub columns: NonZeroUsize, - /// The size of the gutter space between each column. - pub gutter: Linear, /// The child to be layouted into the columns. Most likely, this should be a /// flow or stack node. pub child: PackedNode, } +#[class] +impl ColumnsNode { + /// The size of the gutter space between each column. + pub const GUTTER: Linear = Relative::new(0.04).into(); + + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + Ok(Node::block(Self { + columns: args.expect("column count")?, + child: args.expect("body")?, + })) + } + + fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { + styles.set_opt(Self::GUTTER, args.named("gutter")?); + Ok(()) + } +} + impl Layout for ColumnsNode { fn layout( &self, @@ -57,7 +59,7 @@ impl Layout for ColumnsNode { .iter() .take(1 + regions.backlog.len() + regions.last.iter().len()) { - let gutter = self.gutter.resolve(base.x); + let gutter = styles.get(Self::GUTTER).resolve(base.x); let width = (current.x - gutter * (columns - 1) as f64) / columns as f64; let size = Size::new(width, current.y); gutters.push(gutter); @@ -131,3 +133,13 @@ impl Layout for ColumnsNode { finished } } + +/// A column break. +pub struct ColbreakNode; + +#[class] +impl ColbreakNode { + fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult { + Ok(Node::Colbreak) + } +} diff --git a/src/library/deco.rs b/src/library/deco.rs new file mode 100644 index 000000000..3e91d1de1 --- /dev/null +++ b/src/library/deco.rs @@ -0,0 +1,79 @@ +//! Text decorations. + +use super::prelude::*; +use super::TextNode; + +/// Typeset underline, striken-through or overlined text. +pub struct DecoNode(pub L); + +#[class] +impl DecoNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + let deco = Decoration { + line: L::LINE, + stroke: args.named("stroke")?.or_else(|| args.find()), + thickness: args.named::("thickness")?.or_else(|| args.find()), + offset: args.named("offset")?, + extent: args.named("extent")?.unwrap_or_default(), + }; + Ok(args.expect::("body")?.styled(TextNode::LINES, vec![deco])) + } +} + +/// Defines a line that is positioned over, under or on top of text. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Decoration { + /// Which line to draw. + pub line: DecoLine, + /// Stroke color of the line, defaults to the text color if `None`. + pub stroke: Option, + /// Thickness of the line's strokes (dependent on scaled font size), read + /// from the font tables if `None`. + pub thickness: Option, + /// Position of the line relative to the baseline (dependent on scaled font + /// size), read from the font tables if `None`. + pub offset: Option, + /// Amount that the line will be longer or shorter than its associated text + /// (dependent on scaled font size). + pub extent: Linear, +} + +/// The kind of decorative line. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum DecoLine { + /// A line under text. + Underline, + /// A line through text. + Strikethrough, + /// A line over text. + Overline, +} + +/// Differents kinds of decorative lines for text. +pub trait LineKind { + const LINE: DecoLine; +} + +/// A line under text. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Underline; + +impl LineKind for Underline { + const LINE: DecoLine = DecoLine::Underline; +} + +/// A line through text. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Strikethrough; + +impl LineKind for Strikethrough { + const LINE: DecoLine = DecoLine::Strikethrough; +} + +/// A line over text. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Overline; + +impl LineKind for Overline { + const LINE: DecoLine = DecoLine::Overline; +} diff --git a/src/library/flow.rs b/src/library/flow.rs index f274c9b6d..cfcd65619 100644 --- a/src/library/flow.rs +++ b/src/library/flow.rs @@ -3,7 +3,7 @@ use std::fmt::{self, Debug, Formatter}; use super::prelude::*; -use super::{AlignNode, ParNode, PlacedNode, SpacingKind, TextNode}; +use super::{AlignNode, ParNode, PlaceNode, SpacingKind, TextNode}; /// A vertical flow of content consisting of paragraphs and other layout nodes. /// @@ -172,7 +172,7 @@ impl<'a> FlowLayouter<'a> { ) { // Placed nodes that are out of flow produce placed items which aren't // aligned later. - if let Some(placed) = node.downcast::() { + if let Some(placed) = node.downcast::() { if placed.out_of_flow() { let frame = node.layout(ctx, &self.regions, styles).remove(0); self.items.push(FlowItem::Placed(frame.item)); diff --git a/src/library/grid.rs b/src/library/grid.rs index ffadf9c24..ee9aafe17 100644 --- a/src/library/grid.rs +++ b/src/library/grid.rs @@ -2,23 +2,6 @@ use super::prelude::*; -/// `grid`: Arrange children into a grid. -pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult { - let columns = args.named("columns")?.unwrap_or_default(); - let rows = args.named("rows")?.unwrap_or_default(); - let base_gutter: Vec = args.named("gutter")?.unwrap_or_default(); - let column_gutter = args.named("column-gutter")?; - let row_gutter = args.named("row-gutter")?; - Ok(Value::block(GridNode { - tracks: Spec::new(columns, rows), - gutter: Spec::new( - column_gutter.unwrap_or_else(|| base_gutter.clone()), - row_gutter.unwrap_or(base_gutter), - ), - children: args.all().collect(), - })) -} - /// A node that arranges its children in a grid. #[derive(Debug, Hash)] pub struct GridNode { @@ -30,6 +13,25 @@ pub struct GridNode { pub children: Vec, } +#[class] +impl GridNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + let columns = args.named("columns")?.unwrap_or_default(); + let rows = args.named("rows")?.unwrap_or_default(); + let base_gutter: Vec = args.named("gutter")?.unwrap_or_default(); + let column_gutter = args.named("column-gutter")?; + let row_gutter = args.named("row-gutter")?; + Ok(Node::block(Self { + tracks: Spec::new(columns, rows), + gutter: Spec::new( + column_gutter.unwrap_or_else(|| base_gutter.clone()), + row_gutter.unwrap_or(base_gutter), + ), + children: args.all().collect(), + })) + } +} + impl Layout for GridNode { fn layout( &self, diff --git a/src/library/heading.rs b/src/library/heading.rs index 3591ea0c0..d3beb4eef 100644 --- a/src/library/heading.rs +++ b/src/library/heading.rs @@ -13,25 +13,21 @@ pub struct HeadingNode { pub child: PackedNode, } -#[properties] +#[class] impl HeadingNode { /// The heading's font family. pub const FAMILY: Smart = Smart::Auto; /// The fill color of text in the heading. Just the surrounding text color /// if `auto`. pub const FILL: Smart = Smart::Auto; -} -impl Construct for HeadingNode { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(Node::block(Self { child: args.expect("body")?, level: args.named("level")?.unwrap_or(1), })) } -} -impl Set for HeadingNode { fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { styles.set_opt(Self::FAMILY, args.named("family")?); styles.set_opt(Self::FILL, args.named("fill")?); diff --git a/src/library/image.rs b/src/library/image.rs index c5cb9aebf..a5423ccb3 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -1,45 +1,41 @@ //! Raster and vector graphics. -use std::io; - use super::prelude::*; +use super::TextNode; use crate::diag::Error; use crate::image::ImageId; -/// `image`: An image. -pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult { - // Load the image. - let path = args.expect::>("path to image file")?; - let full = ctx.make_path(&path.v); - let id = ctx.images.load(&full).map_err(|err| { - Error::boxed(path.span, match err.kind() { - io::ErrorKind::NotFound => "file not found".into(), - _ => format!("failed to load image ({})", err), - }) - })?; - - let width = args.named("width")?; - let height = args.named("height")?; - let fit = args.named("fit")?.unwrap_or_default(); - - Ok(Value::inline( - ImageNode { id, fit }.pack().sized(Spec::new(width, height)), - )) -} - /// An image node. #[derive(Debug, Hash)] -pub struct ImageNode { - /// The id of the image file. - pub id: ImageId, - /// How the image should adjust itself to a given area. - pub fit: ImageFit, -} +pub struct ImageNode(pub ImageId); -#[properties] +#[class] impl ImageNode { - /// An URL the image should link to. - pub const LINK: Option = None; + /// How the image should adjust itself to a given area. + pub const FIT: ImageFit = ImageFit::Cover; + + fn construct(ctx: &mut EvalContext, args: &mut Args) -> TypResult { + let path = args.expect::>("path to image file")?; + let full = ctx.make_path(&path.v); + let id = ctx.images.load(&full).map_err(|err| { + Error::boxed(path.span, match err.kind() { + std::io::ErrorKind::NotFound => "file not found".into(), + _ => format!("failed to load image ({})", err), + }) + })?; + + let width = args.named("width")?; + let height = args.named("height")?; + + Ok(Node::inline( + ImageNode(id).pack().sized(Spec::new(width, height)), + )) + } + + fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { + styles.set_opt(Self::FIT, args.named("fit")?); + Ok(()) + } } impl Layout for ImageNode { @@ -49,7 +45,7 @@ impl Layout for ImageNode { regions: &Regions, styles: StyleChain, ) -> Vec>> { - let img = ctx.images.get(self.id); + let img = ctx.images.get(self.0); let pxw = img.width() as f64; let pxh = img.height() as f64; let px_ratio = pxw / pxh; @@ -70,10 +66,11 @@ impl Layout for ImageNode { Size::new(Length::pt(pxw), Length::pt(pxh)) }; - // The actual size of the fitted image. - let fitted = match self.fit { + // Compute the actual size of the fitted image. + let fit = styles.get(Self::FIT); + let fitted = match fit { ImageFit::Cover | ImageFit::Contain => { - if wide == (self.fit == ImageFit::Contain) { + if wide == (fit == ImageFit::Contain) { Size::new(target.x, target.x / px_ratio) } else { Size::new(target.y * px_ratio, target.y) @@ -86,16 +83,16 @@ impl Layout for ImageNode { // the frame to the target size, center aligning the image in the // process. let mut frame = Frame::new(fitted); - frame.push(Point::zero(), Element::Image(self.id, fitted)); + frame.push(Point::zero(), Element::Image(self.0, fitted)); frame.resize(target, Align::CENTER_HORIZON); // Create a clipping group if only part of the image should be visible. - if self.fit == ImageFit::Cover && !target.fits(fitted) { + if fit == ImageFit::Cover && !target.fits(fitted) { frame.clip(); } // Apply link if it exists. - if let Some(url) = styles.get_ref(Self::LINK) { + if let Some(url) = styles.get_ref(TextNode::LINK) { frame.link(url); } @@ -114,12 +111,6 @@ pub enum ImageFit { Stretch, } -impl Default for ImageFit { - fn default() -> Self { - Self::Cover - } -} - castable! { ImageFit, Expected: "string", diff --git a/src/library/link.rs b/src/library/link.rs index b7e3d587f..dc523ffd6 100644 --- a/src/library/link.rs +++ b/src/library/link.rs @@ -1,23 +1,24 @@ //! Hyperlinking. use super::prelude::*; -use super::{ImageNode, ShapeNode, TextNode}; +use super::TextNode; use crate::util::EcoString; -/// `link`: Link text and other elements to an URL. -pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult { - let url: String = args.expect::("url")?.into(); - let body = args.find().unwrap_or_else(|| { - let mut text = url.as_str(); - for prefix in ["mailto:", "tel:"] { - text = text.trim_start_matches(prefix); - } - Node::Text(text.into()) - }); +/// Link text and other elements to an URL. +pub struct LinkNode; - let mut map = StyleMap::new(); - map.set(TextNode::LINK, Some(url.clone())); - map.set(ImageNode::LINK, Some(url.clone())); - map.set(ShapeNode::LINK, Some(url)); - Ok(Value::Node(body.styled_with_map(map))) +#[class] +impl LinkNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + let url: String = args.expect::("url")?.into(); + let body = args.find().unwrap_or_else(|| { + let mut text = url.as_str(); + for prefix in ["mailto:", "tel:"] { + text = text.trim_start_matches(prefix); + } + Node::Text(text.into()) + }); + + Ok(body.styled(TextNode::LINK, Some(url))) + } } diff --git a/src/library/list.rs b/src/library/list.rs index bbdc400ae..9f742a32b 100644 --- a/src/library/list.rs +++ b/src/library/list.rs @@ -1,37 +1,31 @@ //! Unordered (bulleted) and ordered (numbered) lists. -use std::hash::Hash; - use super::prelude::*; use super::{GridNode, TextNode, TrackSizing}; /// An unordered or ordered list. #[derive(Debug, Hash)] -pub struct ListNode { +pub struct ListNode { + /// The list labelling style -- unordered or ordered. + pub kind: L, /// The node that produces the item's body. pub child: PackedNode, - /// The list labelling style -- unordered or ordered. - pub labelling: L, } -#[properties] -impl ListNode { +#[class] +impl ListNode { /// The indentation of each item's label. pub const LABEL_INDENT: Linear = Relative::new(0.0).into(); /// The space between the label and the body of each item. pub const BODY_INDENT: Linear = Relative::new(0.5).into(); -} -impl Construct for ListNode { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(args .all() - .map(|child: PackedNode| Node::block(Self { child, labelling: L::default() })) + .map(|child: PackedNode| Node::block(Self { kind: L::default(), child })) .sum()) } -} -impl Set for ListNode { fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { styles.set_opt(Self::LABEL_INDENT, args.named("label-indent")?); styles.set_opt(Self::BODY_INDENT, args.named("body-indent")?); @@ -39,7 +33,7 @@ impl Set for ListNode { } } -impl Layout for ListNode { +impl Layout for ListNode { fn layout( &self, ctx: &mut LayoutContext, @@ -60,7 +54,7 @@ impl Layout for ListNode { gutter: Spec::default(), children: vec![ PackedNode::default(), - Node::Text(self.labelling.label()).into_block(), + Node::Text(self.kind.label()).into_block(), PackedNode::default(), self.child.clone(), ], @@ -71,7 +65,7 @@ impl Layout for ListNode { } /// How to label a list. -pub trait Labelling: Debug + Default + Hash + 'static { +pub trait ListKind: Debug + Default + Hash + 'static { /// Return the item's label. fn label(&self) -> EcoString; } @@ -80,7 +74,7 @@ pub trait Labelling: Debug + Default + Hash + 'static { #[derive(Debug, Default, Hash)] pub struct Unordered; -impl Labelling for Unordered { +impl ListKind for Unordered { fn label(&self) -> EcoString { '•'.into() } @@ -90,7 +84,7 @@ impl Labelling for Unordered { #[derive(Debug, Default, Hash)] pub struct Ordered(pub Option); -impl Labelling for Ordered { +impl ListKind for Ordered { fn label(&self) -> EcoString { format_eco!("{}.", self.0.unwrap_or(1)) } diff --git a/src/library/mod.rs b/src/library/mod.rs index 461716a17..cb1177023 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -5,6 +5,7 @@ pub mod align; pub mod columns; +pub mod deco; pub mod flow; pub mod grid; pub mod heading; @@ -26,6 +27,7 @@ pub mod utility; pub use self::image::*; pub use align::*; pub use columns::*; +pub use deco::*; pub use flow::*; pub use grid::*; pub use heading::*; @@ -56,8 +58,9 @@ prelude! { pub use std::fmt::{self, Debug, Formatter}; pub use std::num::NonZeroUsize; pub use std::rc::Rc; + pub use std::hash::Hash; - pub use typst_macros::properties; + pub use typst_macros::class; pub use crate::diag::{At, TypResult}; pub use crate::eval::{ @@ -81,41 +84,39 @@ pub fn new() -> Scope { // Structure and semantics. std.def_class::("page"); + std.def_class::("pagebreak"); std.def_class::("par"); + std.def_class::("parbreak"); + std.def_class::("linebreak"); std.def_class::("text"); - std.def_func("underline", underline); - std.def_func("strike", strike); - std.def_func("overline", overline); - std.def_func("link", link); + std.def_class::>("underline"); + std.def_class::>("strike"); + std.def_class::>("overline"); + std.def_class::("link"); std.def_class::("heading"); std.def_class::>("list"); std.def_class::>("enum"); - std.def_func("image", image); - std.def_func("rect", rect); - std.def_func("square", square); - std.def_func("ellipse", ellipse); - std.def_func("circle", circle); + std.def_class::("image"); + std.def_class::>("rect"); + std.def_class::>("square"); + std.def_class::>("ellipse"); + std.def_class::>("circle"); // Layout. - std.def_func("h", h); - std.def_func("v", v); - std.def_func("box", box_); - std.def_func("block", block); - std.def_func("align", align); - std.def_func("pad", pad); - std.def_func("place", place); - std.def_func("move", move_); - std.def_func("scale", scale); - std.def_func("rotate", rotate); - std.def_func("stack", stack); - std.def_func("grid", grid); - std.def_func("columns", columns); - - // Breaks. - std.def_func("pagebreak", pagebreak); - std.def_func("colbreak", colbreak); - std.def_func("parbreak", parbreak); - std.def_func("linebreak", linebreak); + std.def_class::("h"); + std.def_class::("v"); + std.def_class::("box"); + std.def_class::("block"); + std.def_class::("align"); + std.def_class::("pad"); + std.def_class::("place"); + std.def_class::>("move"); + std.def_class::>("scale"); + std.def_class::>("rotate"); + std.def_class::("stack"); + std.def_class::("grid"); + std.def_class::("columns"); + std.def_class::("colbreak"); // Utility functions. std.def_func("assert", assert); diff --git a/src/library/pad.rs b/src/library/pad.rs index e2969fd64..394d3c179 100644 --- a/src/library/pad.rs +++ b/src/library/pad.rs @@ -2,25 +2,7 @@ use super::prelude::*; -/// `pad`: Pad content at the sides. -pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult { - let all = args.find(); - let left = args.named("left")?; - let top = args.named("top")?; - let right = args.named("right")?; - let bottom = args.named("bottom")?; - let body: PackedNode = args.expect("body")?; - let padding = Sides::new( - left.or(all).unwrap_or_default(), - top.or(all).unwrap_or_default(), - right.or(all).unwrap_or_default(), - bottom.or(all).unwrap_or_default(), - ); - - Ok(Value::block(body.padded(padding))) -} - -/// A node that adds padding to its child. +/// Pad content at the sides. #[derive(Debug, Hash)] pub struct PadNode { /// The amount of padding. @@ -29,6 +11,26 @@ pub struct PadNode { pub child: PackedNode, } +#[class] +impl PadNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + let all = args.find(); + let left = args.named("left")?; + let top = args.named("top")?; + let right = args.named("right")?; + let bottom = args.named("bottom")?; + let body: PackedNode = args.expect("body")?; + let padding = Sides::new( + left.or(all).unwrap_or_default(), + top.or(all).unwrap_or_default(), + right.or(all).unwrap_or_default(), + bottom.or(all).unwrap_or_default(), + ); + + Ok(Node::block(body.padded(padding))) + } +} + impl Layout for PadNode { fn layout( &self, diff --git a/src/library/page.rs b/src/library/page.rs index 522fd3ac0..e2c27a360 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -10,7 +10,7 @@ use super::ColumnsNode; #[derive(Clone, PartialEq, Hash)] pub struct PageNode(pub PackedNode); -#[properties] +#[class] impl PageNode { /// The unflipped width of the page. pub const WIDTH: Smart = Smart::Custom(Paper::default().width()); @@ -32,17 +32,11 @@ impl PageNode { pub const FILL: Option = None; /// How many columns the page has. pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); - /// How much space is between the page's columns. - pub const COLUMN_GUTTER: Linear = Relative::new(0.04).into(); -} -impl Construct for PageNode { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(Node::Page(Self(args.expect("body")?))) } -} -impl Set for PageNode { fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { if let Some(paper) = args.named::("paper")?.or_else(|| args.find()) { styles.set(Self::CLASS, paper.class()); @@ -69,7 +63,6 @@ impl Set for PageNode { styles.set_opt(Self::FLIPPED, args.named("flipped")?); styles.set_opt(Self::FILL, args.named("fill")?); styles.set_opt(Self::COLUMNS, args.named("columns")?); - styles.set_opt(Self::COLUMN_GUTTER, args.named("column-gutter")?); Ok(()) } @@ -102,12 +95,7 @@ impl PageNode { // Realize columns with columns node. let columns = styles.get(Self::COLUMNS); if columns.get() > 1 { - child = ColumnsNode { - columns, - gutter: styles.get(Self::COLUMN_GUTTER), - child: self.0.clone(), - } - .pack(); + child = ColumnsNode { columns, child: self.0.clone() }.pack(); } // Realize margins with padding node. @@ -142,9 +130,14 @@ impl Debug for PageNode { } } -/// `pagebreak`: Start a new page. -pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult { - Ok(Value::Node(Node::Pagebreak)) +/// A page break. +pub struct PagebreakNode; + +#[class] +impl PagebreakNode { + fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult { + Ok(Node::Pagebreak) + } } /// Specification of a paper. diff --git a/src/library/par.rs b/src/library/par.rs index 38d150979..4f711e76c 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -15,7 +15,7 @@ use crate::util::{EcoString, RangeExt, RcExt, SliceExt}; #[derive(Hash)] pub struct ParNode(pub Vec>); -#[properties] +#[class] impl ParNode { /// The direction for text and inline objects. pub const DIR: Dir = Dir::LTR; @@ -25,9 +25,7 @@ impl ParNode { pub const LEADING: Linear = Relative::new(0.65).into(); /// The spacing between paragraphs (dependent on scaled font size). pub const SPACING: Linear = Relative::new(1.2).into(); -} -impl Construct for ParNode { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { // The paragraph constructor is special: It doesn't create a paragraph // since that happens automatically through markup. Instead, it just @@ -35,13 +33,8 @@ impl Construct for ParNode { // adjacent stuff and it styles the contained paragraphs. Ok(Node::Block(args.expect("body")?)) } -} -impl Set for ParNode { fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { - let spacing = args.named("spacing")?; - let leading = args.named("leading")?; - let mut dir = args.named("lang")? .map(|iso: EcoString| match iso.to_lowercase().as_str() { @@ -69,8 +62,8 @@ impl Set for ParNode { styles.set_opt(Self::DIR, dir); styles.set_opt(Self::ALIGN, align); - styles.set_opt(Self::LEADING, leading); - styles.set_opt(Self::SPACING, spacing); + styles.set_opt(Self::LEADING, args.named("leading")?); + styles.set_opt(Self::SPACING, args.named("spacing")?); Ok(()) } @@ -166,14 +159,24 @@ impl Debug for ParChild { } } -/// `parbreak`: Start a new paragraph. -pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult { - Ok(Value::Node(Node::Parbreak)) +/// A paragraph break. +pub struct ParbreakNode; + +#[class] +impl ParbreakNode { + fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult { + Ok(Node::Parbreak) + } } -/// `linebreak`: Start a new line. -pub fn linebreak(_: &mut EvalContext, _: &mut Args) -> TypResult { - Ok(Value::Node(Node::Linebreak)) +/// A line break. +pub struct LinebreakNode; + +#[class] +impl LinebreakNode { + fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult { + Ok(Node::Linebreak) + } } /// A paragraph representation in which children are already layouted and text diff --git a/src/library/placed.rs b/src/library/placed.rs index e7b173251..cee687fa3 100644 --- a/src/library/placed.rs +++ b/src/library/placed.rs @@ -3,33 +3,24 @@ use super::prelude::*; use super::AlignNode; -/// `place`: Place content at an absolute position. -pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult { - let aligns = args.find().unwrap_or(Spec::with_x(Some(Align::Left))); - let tx = args.named("dx")?.unwrap_or_default(); - let ty = args.named("dy")?.unwrap_or_default(); - let body: PackedNode = args.expect("body")?; - Ok(Value::block(PlacedNode( - body.moved(Point::new(tx, ty)).aligned(aligns), - ))) -} - -/// A node that places its child absolutely. +/// Place content at an absolute position. #[derive(Debug, Hash)] -pub struct PlacedNode(pub PackedNode); +pub struct PlaceNode(pub PackedNode); -impl PlacedNode { - /// Whether this node wants to be placed relative to its its parent's base - /// origin. instead of relative to the parent's current flow/cursor - /// position. - pub fn out_of_flow(&self) -> bool { - self.0 - .downcast::() - .map_or(false, |node| node.aligns.y.is_some()) +#[class] +impl PlaceNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + let aligns = args.find().unwrap_or(Spec::with_x(Some(Align::Left))); + let tx = args.named("dx")?.unwrap_or_default(); + let ty = args.named("dy")?.unwrap_or_default(); + let body: PackedNode = args.expect("body")?; + Ok(Node::block(Self( + body.moved(Point::new(tx, ty)).aligned(aligns), + ))) } } -impl Layout for PlacedNode { +impl Layout for PlaceNode { fn layout( &self, ctx: &mut LayoutContext, @@ -63,3 +54,14 @@ impl Layout for PlacedNode { frames } } + +impl PlaceNode { + /// Whether this node wants to be placed relative to its its parent's base + /// origin. instead of relative to the parent's current flow/cursor + /// position. + pub fn out_of_flow(&self) -> bool { + self.0 + .downcast::() + .map_or(false, |node| node.aligns.y.is_some()) + } +} diff --git a/src/library/shape.rs b/src/library/shape.rs index c47885d20..32e39b6ab 100644 --- a/src/library/shape.rs +++ b/src/library/shape.rs @@ -3,110 +3,64 @@ use std::f64::consts::SQRT_2; use super::prelude::*; - -/// `rect`: A rectangle with optional content. -pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult { - let width = args.named("width")?; - let height = args.named("height")?; - shape_impl(args, ShapeKind::Rect, width, height) -} - -/// `square`: A square with optional content. -pub fn square(_: &mut EvalContext, args: &mut Args) -> TypResult { - let size = args.named::("size")?.map(Linear::from); - let width = match size { - None => args.named("width")?, - size => size, - }; - let height = match size { - None => args.named("height")?, - size => size, - }; - shape_impl(args, ShapeKind::Square, width, height) -} - -/// `ellipse`: An ellipse with optional content. -pub fn ellipse(_: &mut EvalContext, args: &mut Args) -> TypResult { - let width = args.named("width")?; - let height = args.named("height")?; - shape_impl(args, ShapeKind::Ellipse, width, height) -} - -/// `circle`: A circle with optional content. -pub fn circle(_: &mut EvalContext, args: &mut Args) -> TypResult { - let diameter = args.named("radius")?.map(|r: Length| 2.0 * Linear::from(r)); - let width = match diameter { - None => args.named("width")?, - diameter => diameter, - }; - let height = match diameter { - None => args.named("height")?, - diameter => diameter, - }; - shape_impl(args, ShapeKind::Circle, width, height) -} - -fn shape_impl( - args: &mut Args, - kind: ShapeKind, - width: Option, - height: Option, -) -> TypResult { - // The default appearance of a shape. - let default = Stroke { - paint: RgbaColor::BLACK.into(), - thickness: Length::pt(1.0), - }; - - // Parse fill & stroke. - let fill = args.named("fill")?.unwrap_or(None); - let stroke = match (args.named("stroke")?, args.named("thickness")?) { - (None, None) => fill.is_none().then(|| default), - (color, thickness) => color.unwrap_or(Some(default.paint)).map(|paint| Stroke { - paint, - thickness: thickness.unwrap_or(default.thickness), - }), - }; - - // Shorthand for padding. - let mut padding = args.named::("padding")?.unwrap_or_default(); - - // Padding with this ratio ensures that a rectangular child fits - // perfectly into a circle / an ellipse. - if kind.is_round() { - padding.rel += Relative::new(0.5 - SQRT_2 / 4.0); - } - - // The shape's contents. - let child = args.find().map(|body: PackedNode| body.padded(Sides::splat(padding))); - - Ok(Value::inline( - ShapeNode { kind, fill, stroke, child } - .pack() - .sized(Spec::new(width, height)), - )) -} +use super::TextNode; /// Places its child into a sizable and fillable shape. #[derive(Debug, Hash)] -pub struct ShapeNode { +pub struct ShapeNode { /// Which shape to place the child into. - pub kind: ShapeKind, - /// How to fill the shape. - pub fill: Option, - /// How the stroke the shape. - pub stroke: Option, + pub kind: S, /// The child node to place into the shape, if any. pub child: Option, } -#[properties] -impl ShapeNode { - /// An URL the shape should link to. - pub const LINK: Option = None; +#[class] +impl ShapeNode { + /// How to fill the shape. + pub const FILL: Option = None; + /// How the stroke the shape. + pub const STROKE: Smart> = Smart::Auto; + /// The stroke's thickness. + pub const THICKNESS: Length = Length::pt(1.0); + /// The How much to pad the shape's content. + pub const PADDING: Linear = Linear::zero(); + + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + let size = if !S::ROUND && S::QUADRATIC { + args.named::("size")?.map(Linear::from) + } else if S::ROUND && S::QUADRATIC { + args.named("radius")?.map(|r: Length| 2.0 * Linear::from(r)) + } else { + None + }; + + let width = match size { + None => args.named("width")?, + size => size, + }; + + let height = match size { + None => args.named("height")?, + size => size, + }; + + Ok(Node::inline( + ShapeNode { kind: S::default(), child: args.find() } + .pack() + .sized(Spec::new(width, height)), + )) + } + + fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { + styles.set_opt(Self::FILL, args.named("fill")?); + styles.set_opt(Self::STROKE, args.named("stroke")?); + styles.set_opt(Self::THICKNESS, args.named("thickness")?); + styles.set_opt(Self::PADDING, args.named("padding")?); + Ok(()) + } } -impl Layout for ShapeNode { +impl Layout for ShapeNode { fn layout( &self, ctx: &mut LayoutContext, @@ -115,12 +69,20 @@ impl Layout for ShapeNode { ) -> Vec>> { let mut frames; if let Some(child) = &self.child { + let mut padding = styles.get(Self::PADDING); + if S::ROUND { + padding.rel += Relative::new(0.5 - SQRT_2 / 4.0); + } + + // Pad the child. + let child = child.clone().padded(Sides::splat(padding)); + let mut pod = Regions::one(regions.current, regions.base, regions.expand); frames = child.layout(ctx, &pod, styles); // Relayout with full expansion into square region to make sure // the result is really a square or circle. - if self.kind.is_quadratic() { + if S::QUADRATIC { let length = if regions.expand.x || regions.expand.y { let target = regions.expand.select(regions.current, Size::zero()); target.x.max(target.y) @@ -141,7 +103,7 @@ impl Layout for ShapeNode { let mut size = Size::new(Length::pt(45.0), Length::pt(30.0)).min(regions.current); - if self.kind.is_quadratic() { + if S::QUADRATIC { let length = if regions.expand.x || regions.expand.y { let target = regions.expand.select(regions.current, Size::zero()); target.x.max(target.y) @@ -159,23 +121,26 @@ impl Layout for ShapeNode { let frame = Rc::make_mut(&mut frames[0].item); // Add fill and/or stroke. - if self.fill.is_some() || self.stroke.is_some() { - let geometry = match self.kind { - ShapeKind::Square | ShapeKind::Rect => Geometry::Rect(frame.size), - ShapeKind::Circle | ShapeKind::Ellipse => Geometry::Ellipse(frame.size), - }; - - let shape = Shape { - geometry, - fill: self.fill, - stroke: self.stroke, + let fill = styles.get(Self::FILL); + let thickness = styles.get(Self::THICKNESS); + let stroke = styles + .get(Self::STROKE) + .unwrap_or(fill.is_none().then(|| RgbaColor::BLACK.into())) + .map(|paint| Stroke { paint, thickness }); + + if fill.is_some() || stroke.is_some() { + let geometry = if S::ROUND { + Geometry::Ellipse(frame.size) + } else { + Geometry::Rect(frame.size) }; + let shape = Shape { geometry, fill, stroke }; frame.prepend(Point::zero(), Element::Shape(shape)); } // Apply link if it exists. - if let Some(url) = styles.get_ref(Self::LINK) { + if let Some(url) = styles.get_ref(TextNode::LINK) { frame.link(url); } @@ -183,27 +148,44 @@ impl Layout for ShapeNode { } } -/// The type of a shape. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum ShapeKind { - /// A rectangle with equal side lengths. - Square, - /// A quadrilateral with four right angles. - Rect, - /// An ellipse with coinciding foci. - Circle, - /// A curve around two focal points. - Ellipse, +/// Categorizes shapes. +pub trait ShapeKind: Debug + Default + Hash + 'static { + const ROUND: bool; + const QUADRATIC: bool; } -impl ShapeKind { - /// Whether the shape is curved. - pub fn is_round(self) -> bool { - matches!(self, Self::Circle | Self::Ellipse) - } +/// A rectangle with equal side lengths. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Square; - /// Whether the shape has a fixed 1-1 aspect ratio. - pub fn is_quadratic(self) -> bool { - matches!(self, Self::Square | Self::Circle) - } +impl ShapeKind for Square { + const ROUND: bool = false; + const QUADRATIC: bool = true; +} + +/// A quadrilateral with four right angles. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Rect; + +impl ShapeKind for Rect { + const ROUND: bool = false; + const QUADRATIC: bool = false; +} + +/// An ellipse with coinciding foci. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Circle; + +impl ShapeKind for Circle { + const ROUND: bool = true; + const QUADRATIC: bool = true; +} + +/// A curve around two focal points. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Ellipse; + +impl ShapeKind for Ellipse { + const ROUND: bool = true; + const QUADRATIC: bool = false; } diff --git a/src/library/sized.rs b/src/library/sized.rs index 2400971ab..575787173 100644 --- a/src/library/sized.rs +++ b/src/library/sized.rs @@ -2,18 +2,27 @@ use super::prelude::*; -/// `box`: Size content and place it into a paragraph. -pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult { - let width = args.named("width")?; - let height = args.named("height")?; - let body: PackedNode = args.find().unwrap_or_default(); - Ok(Value::inline(body.sized(Spec::new(width, height)))) +/// Size content and place it into a paragraph. +pub struct BoxNode; + +#[class] +impl BoxNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + let width = args.named("width")?; + let height = args.named("height")?; + let body: PackedNode = args.find().unwrap_or_default(); + Ok(Node::inline(body.sized(Spec::new(width, height)))) + } } -/// `block`: Place content into the flow. -pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult { - let body: PackedNode = args.find().unwrap_or_default(); - Ok(Value::block(body)) +/// Place content into a separate flow. +pub struct BlockNode; + +#[class] +impl BlockNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + Ok(Node::Block(args.find().unwrap_or_default())) + } } /// A node that sizes its child. diff --git a/src/library/spacing.rs b/src/library/spacing.rs index 1b1403e93..7c0c377c2 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -2,20 +2,24 @@ use super::prelude::*; -/// `h`: Horizontal spacing. -pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult { - Ok(Value::Node(Node::Spacing( - SpecAxis::Horizontal, - args.expect("spacing")?, - ))) +/// Horizontal spacing. +pub struct HNode; + +#[class] +impl HNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + Ok(Node::Spacing(SpecAxis::Horizontal, args.expect("spacing")?)) + } } -/// `v`: Vertical spacing. -pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult { - Ok(Value::Node(Node::Spacing( - SpecAxis::Vertical, - args.expect("spacing")?, - ))) +/// Vertical spacing. +pub struct VNode; + +#[class] +impl VNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + Ok(Node::Spacing(SpecAxis::Vertical, args.expect("spacing")?)) + } } /// Kinds of spacing. diff --git a/src/library/stack.rs b/src/library/stack.rs index f4f7a3cf6..8c8a9f603 100644 --- a/src/library/stack.rs +++ b/src/library/stack.rs @@ -3,16 +3,7 @@ use super::prelude::*; use super::{AlignNode, SpacingKind}; -/// `stack`: Stack children along an axis. -pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult { - Ok(Value::block(StackNode { - dir: args.named("dir")?.unwrap_or(Dir::TTB), - spacing: args.named("spacing")?, - children: args.all().collect(), - })) -} - -/// A node that stacks its children. +/// Stack children along an axis. #[derive(Debug, Hash)] pub struct StackNode { /// The stacking direction. @@ -23,6 +14,17 @@ pub struct StackNode { pub children: Vec, } +#[class] +impl StackNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + Ok(Node::block(Self { + dir: args.named("dir")?.unwrap_or(Dir::TTB), + spacing: args.named("spacing")?, + children: args.all().collect(), + })) + } +} + impl Layout for StackNode { fn layout( &self, diff --git a/src/library/text.rs b/src/library/text.rs index d5c87949b..78de55fd4 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -9,6 +9,7 @@ use rustybuzz::{Feature, UnicodeBuffer}; use ttf_parser::Tag; use super::prelude::*; +use super::{DecoLine, Decoration}; use crate::font::{ Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight, VerticalFontMetric, @@ -20,7 +21,7 @@ use crate::util::{EcoString, SliceExt}; #[derive(Hash)] pub struct TextNode(pub EcoString); -#[properties] +#[class] impl TextNode { /// A prioritized sequence of font families. pub const FAMILY_LIST: Vec = vec![FontFamily::SansSerif]; @@ -52,7 +53,7 @@ impl TextNode { pub const FILL: Paint = RgbaColor::BLACK.into(); /// Decorative lines. #[fold(|a, b| a.into_iter().chain(b).collect())] - pub const LINES: Vec = vec![]; + pub const LINES: Vec = vec![]; /// An URL the text should link to. pub const LINK: Option = None; @@ -92,18 +93,14 @@ impl TextNode { pub const FRACTIONS: bool = false; /// Raw OpenType features to apply. pub const FEATURES: Vec<(Tag, u32)> = vec![]; -} -impl Construct for TextNode { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { // The text constructor is special: It doesn't create a text node. // Instead, it leaves the passed argument structurally unchanged, but // styles all text in it. args.expect("body") } -} -impl Set for TextNode { fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { let list = args.named("family")?.or_else(|| { let families: Vec<_> = args.all().collect(); @@ -382,60 +379,6 @@ castable! { .collect(), } -/// `strike`: Typeset striken-through text. -pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult { - line_impl(args, LineKind::Strikethrough) -} - -/// `underline`: Typeset underlined text. -pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult { - line_impl(args, LineKind::Underline) -} - -/// `overline`: Typeset text with an overline. -pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult { - line_impl(args, LineKind::Overline) -} - -fn line_impl(args: &mut Args, kind: LineKind) -> TypResult { - let stroke = args.named("stroke")?.or_else(|| args.find()); - let thickness = args.named::("thickness")?.or_else(|| args.find()); - let offset = args.named("offset")?; - let extent = args.named("extent")?.unwrap_or_default(); - let body: Node = args.expect("body")?; - let deco = LineDecoration { kind, stroke, thickness, offset, extent }; - Ok(Value::Node(body.styled(TextNode::LINES, vec![deco]))) -} - -/// Defines a line that is positioned over, under or on top of text. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct LineDecoration { - /// The kind of line. - pub kind: LineKind, - /// Stroke color of the line, defaults to the text color if `None`. - pub stroke: Option, - /// Thickness of the line's strokes (dependent on scaled font size), read - /// from the font tables if `None`. - pub thickness: Option, - /// Position of the line relative to the baseline (dependent on scaled font - /// size), read from the font tables if `None`. - pub offset: Option, - /// Amount that the line will be longer or shorter than its associated text - /// (dependent on scaled font size). - pub extent: Linear, -} - -/// The kind of line decoration. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum LineKind { - /// A line under text. - Underline, - /// A line through text. - Strikethrough, - /// A line over text. - Overline, -} - /// Shape text into [`ShapedText`]. pub fn shape<'a>( fonts: &mut FontStore, @@ -848,23 +791,23 @@ impl<'a> ShapedText<'a> { frame.push(pos, Element::Text(text)); // Apply line decorations. - for line in self.styles.get_cloned(TextNode::LINES) { + for deco in self.styles.get_cloned(TextNode::LINES) { let face = fonts.get(face_id); - let metrics = match line.kind { - LineKind::Underline => face.underline, - LineKind::Strikethrough => face.strikethrough, - LineKind::Overline => face.overline, + let metrics = match deco.line { + DecoLine::Underline => face.underline, + DecoLine::Strikethrough => face.strikethrough, + DecoLine::Overline => face.overline, }; - let extent = line.extent.resolve(size); - let offset = line + let extent = deco.extent.resolve(size); + let offset = deco .offset .map(|s| s.resolve(size)) .unwrap_or(-metrics.position.resolve(size)); let stroke = Stroke { - paint: line.stroke.unwrap_or(fill), - thickness: line + paint: deco.stroke.unwrap_or(fill), + thickness: deco .thickness .map(|s| s.resolve(size)) .unwrap_or(metrics.thickness.resolve(size)), diff --git a/src/library/transform.rs b/src/library/transform.rs index 7392b89f9..aceb4197a 100644 --- a/src/library/transform.rs +++ b/src/library/transform.rs @@ -3,64 +3,49 @@ use super::prelude::*; use crate::geom::Transform; -/// `move`: Move content without affecting layout. -pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult { - let tx = args.named("x")?.unwrap_or_default(); - let ty = args.named("y")?.unwrap_or_default(); - let transform = Transform::translation(tx, ty); - transform_impl(args, transform) -} - -/// `scale`: Scale content without affecting layout. -pub fn scale(_: &mut EvalContext, args: &mut Args) -> TypResult { - let all = args.find(); - let sx = args.named("x")?.or(all).unwrap_or(Relative::one()); - let sy = args.named("y")?.or(all).unwrap_or(Relative::one()); - let transform = Transform::scaling(sx, sy); - transform_impl(args, transform) -} - -/// `rotate`: Rotate content without affecting layout. -pub fn rotate(_: &mut EvalContext, args: &mut Args) -> TypResult { - let angle = args.named("angle")?.or_else(|| args.find()).unwrap_or_default(); - let transform = Transform::rotation(angle); - transform_impl(args, transform) -} - -fn transform_impl(args: &mut Args, transform: Transform) -> TypResult { - let body: PackedNode = args.expect("body")?; - let origin = args - .named("origin")? - .unwrap_or(Spec::splat(None)) - .unwrap_or(Align::CENTER_HORIZON); - - Ok(Value::inline(body.transformed(transform, origin))) -} - /// A node that transforms its child without affecting layout. #[derive(Debug, Hash)] -pub struct TransformNode { +pub struct TransformNode { /// Transformation to apply to the contents. - pub transform: Transform, - /// The origin of the transformation. - pub origin: Spec, + pub kind: T, /// The node whose contents should be transformed. pub child: PackedNode, } -impl Layout for TransformNode { +#[class] +impl TransformNode { + /// The origin of the transformation. + pub const ORIGIN: Spec> = Spec::default(); + + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult { + Ok(Node::inline(Self { + kind: T::construct(args)?, + child: args.expect("body")?, + })) + } + + fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { + styles.set_opt(Self::ORIGIN, args.named("origin")?); + Ok(()) + } +} + +impl Layout for TransformNode { fn layout( &self, ctx: &mut LayoutContext, regions: &Regions, styles: StyleChain, ) -> Vec>> { + let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); + let matrix = self.kind.matrix(); + let mut frames = self.child.layout(ctx, regions, styles); for Constrained { item: frame, .. } in &mut frames { - let Spec { x, y } = self.origin.zip(frame.size).map(|(o, s)| o.resolve(s)); + let Spec { x, y } = origin.zip(frame.size).map(|(o, s)| o.resolve(s)); let transform = Transform::translation(x, y) - .pre_concat(self.transform) + .pre_concat(matrix) .pre_concat(Transform::translation(-x, -y)); Rc::make_mut(frame).transform(transform); @@ -69,3 +54,58 @@ impl Layout for TransformNode { frames } } + +/// Kinds of transformations. +pub trait TransformKind: Debug + Hash + Sized + 'static { + fn construct(args: &mut Args) -> TypResult; + fn matrix(&self) -> Transform; +} + +/// A translation on the X and Y axes. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Move(pub Length, pub Length); + +impl TransformKind for Move { + fn construct(args: &mut Args) -> TypResult { + let tx = args.named("x")?.unwrap_or_default(); + let ty = args.named("y")?.unwrap_or_default(); + Ok(Self(tx, ty)) + } + + fn matrix(&self) -> Transform { + Transform::translation(self.0, self.1) + } +} + +/// A rotational transformation. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Rotate(pub Angle); + +impl TransformKind for Rotate { + fn construct(args: &mut Args) -> TypResult { + Ok(Self( + args.named("angle")?.or_else(|| args.find()).unwrap_or_default(), + )) + } + + fn matrix(&self) -> Transform { + Transform::rotation(self.0) + } +} + +/// A scale transformation. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Scale(pub Relative, pub Relative); + +impl TransformKind for Scale { + fn construct(args: &mut Args) -> TypResult { + let all = args.find(); + let sx = args.named("x")?.or(all).unwrap_or(Relative::one()); + let sy = args.named("y")?.or(all).unwrap_or(Relative::one()); + Ok(Self(sx, sy)) + } + + fn matrix(&self) -> Transform { + Transform::scale(self.0, self.1) + } +} diff --git a/src/library/utility.rs b/src/library/utility.rs index 4e4632c45..6cc174490 100644 --- a/src/library/utility.rs +++ b/src/library/utility.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use super::prelude::*; use crate::eval::Array; -/// `assert`: Ensure that a condition is fulfilled. +/// Ensure that a condition is fulfilled. pub fn assert(_: &mut EvalContext, args: &mut Args) -> TypResult { let Spanned { v, span } = args.expect::>("condition")?; if !v { @@ -15,18 +15,17 @@ pub fn assert(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(Value::None) } -/// `type`: The name of a value's type. +/// The name of a value's type. pub fn type_(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(args.expect::("value")?.type_name().into()) } -/// `repr`: The string representation of a value. +/// The string representation of a value. pub fn repr(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(args.expect::("value")?.repr().into()) } -/// `join`: Join a sequence of values, optionally interspersing it with another -/// value. +/// Join a sequence of values, optionally interspersing it with another value. pub fn join(_: &mut EvalContext, args: &mut Args) -> TypResult { let span = args.span; let sep = args.named::("sep")?.unwrap_or(Value::None); @@ -46,7 +45,7 @@ pub fn join(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(result) } -/// `int`: Convert a value to a integer. +/// Convert a value to a integer. pub fn int(_: &mut EvalContext, args: &mut Args) -> TypResult { let Spanned { v, span } = args.expect("value")?; Ok(Value::Int(match v { @@ -61,7 +60,7 @@ pub fn int(_: &mut EvalContext, args: &mut Args) -> TypResult { })) } -/// `float`: Convert a value to a float. +/// Convert a value to a float. pub fn float(_: &mut EvalContext, args: &mut Args) -> TypResult { let Spanned { v, span } = args.expect("value")?; Ok(Value::Float(match v { @@ -75,7 +74,7 @@ pub fn float(_: &mut EvalContext, args: &mut Args) -> TypResult { })) } -/// `str`: Try to convert a value to a string. +/// Try to convert a value to a string. pub fn str(_: &mut EvalContext, args: &mut Args) -> TypResult { let Spanned { v, span } = args.expect("value")?; Ok(Value::Str(match v { @@ -86,7 +85,7 @@ pub fn str(_: &mut EvalContext, args: &mut Args) -> TypResult { })) } -/// `rgb`: Create an RGB(A) color. +/// Create an RGB(A) color. pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(Value::from( if let Some(string) = args.find::>() { @@ -111,7 +110,7 @@ pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult { )) } -/// `abs`: The absolute value of a numeric value. +/// The absolute value of a numeric value. pub fn abs(_: &mut EvalContext, args: &mut Args) -> TypResult { let Spanned { v, span } = args.expect("numeric value")?; Ok(match v { @@ -126,12 +125,12 @@ pub fn abs(_: &mut EvalContext, args: &mut Args) -> TypResult { }) } -/// `min`: The minimum of a sequence of values. +/// The minimum of a sequence of values. pub fn min(_: &mut EvalContext, args: &mut Args) -> TypResult { minmax(args, Ordering::Less) } -/// `max`: The maximum of a sequence of values. +/// The maximum of a sequence of values. pub fn max(_: &mut EvalContext, args: &mut Args) -> TypResult { minmax(args, Ordering::Greater) } @@ -157,7 +156,7 @@ fn minmax(args: &mut Args, goal: Ordering) -> TypResult { Ok(extremum) } -/// `range`: Create a sequence of numbers. +/// Create a sequence of numbers. pub fn range(_: &mut EvalContext, args: &mut Args) -> TypResult { let first = args.expect::("end")?; let (start, end) = match args.eat::()? { @@ -182,17 +181,17 @@ pub fn range(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(Value::Array(Array::from_vec(seq))) } -/// `lower`: Convert a string to lowercase. +/// Convert a string to lowercase. pub fn lower(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(args.expect::("string")?.to_lowercase().into()) } -/// `upper`: Convert a string to uppercase. +/// Convert a string to uppercase. pub fn upper(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(args.expect::("string")?.to_uppercase().into()) } -/// `len`: The length of a string, an array or a dictionary. +/// The length of a string, an array or a dictionary. pub fn len(_: &mut EvalContext, args: &mut Args) -> TypResult { let Spanned { v, span } = args.expect("collection")?; Ok(Value::Int(match v { @@ -207,7 +206,7 @@ pub fn len(_: &mut EvalContext, args: &mut Args) -> TypResult { })) } -/// `sorted`: The sorted version of an array. +/// The sorted version of an array. pub fn sorted(_: &mut EvalContext, args: &mut Args) -> TypResult { let Spanned { v, span } = args.expect::>("array")?; Ok(Value::Array(v.sorted().at(span)?)) diff --git a/tests/ref/code/repr.png b/tests/ref/code/repr.png index 4e5ebb13e8c61f777587d525c64b8b67180e83d1..e0749e1284cf9de1a50b7bd551b6bb922963d712 100644 GIT binary patch delta 9271 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 MnuZ#$a1Igw2VT0WSpWb4 diff --git a/tests/ref/text/par.png b/tests/ref/text/par.png index 03117e673c7e4d3592e9c8f051939bf00d1961ec..bb705a193da6a30938c88534d71e8798b0aaefe8 100644 GIT binary patch literal 9364 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 J%8?eK{{`hFI&%O3 diff --git a/tests/typ/layout/columns.typ b/tests/typ/layout/columns.typ index bf954d930..7868ac39a 100644 --- a/tests/typ/layout/columns.typ +++ b/tests/typ/layout/columns.typ @@ -2,7 +2,8 @@ --- // Test normal operation and RTL directions. -#set page(height: 3.25cm, width: 7.05cm, columns: 2, column-gutter: 30pt) +#set page(height: 3.25cm, width: 7.05cm, columns: 2) +#set columns(gutter: 30pt) #set text("Noto Sans Arabic", serif) #set par(lang: "ar") @@ -10,7 +11,7 @@ العديد من التفاعلات الكيميائية. (DNA) من أهم الأحماض النووية التي تُشكِّل إلى جانب كل من البروتينات والليبيدات والسكريات المتعددة #rect(fill: eastern, height: 8pt, width: 6pt) -الجزيئات الضخمة الأربعة الضرورية للحياة. +الجزيئات الضخمة الأربعة الضرورية للحياة. --- // Test the `columns` function. @@ -28,7 +29,7 @@ #set page(height: 5cm, width: 7.05cm, columns: 2) Lorem ipsum dolor sit amet is a common blind text -and I again am in need of filling up this page +and I again am in need of filling up this page #align(bottom, rect(fill: eastern, width: 100%, height: 12pt)) #colbreak() @@ -49,7 +50,8 @@ a page for a test but it does get the job done. --- // Test setting a column gutter and more than two columns. -#set page(height: 3.25cm, width: 7.05cm, columns: 3, column-gutter: 30pt) +#set page(height: 3.25cm, width: 7.05cm, columns: 3) +#set columns(gutter: 30pt) #rect(width: 100%, height: 2.5cm, fill: conifer) #rect(width: 100%, height: 2cm, fill: eastern) diff --git a/tests/typ/layout/shape-circle.typ b/tests/typ/layout/shape-circle.typ index 8b795830b..4b978e866 100644 --- a/tests/typ/layout/shape-circle.typ +++ b/tests/typ/layout/shape-circle.typ @@ -9,7 +9,7 @@ // Test auto sizing. Auto-sized circle. \ -#circle(fill: rgb("eb5278"), thickness: 2pt, +#circle(fill: rgb("eb5278"), stroke: black, thickness: 2pt, align(center + horizon)[But, soft!] ) diff --git a/tests/typ/layout/shape-fill-stroke.typ b/tests/typ/layout/shape-fill-stroke.typ index 3ae5f987e..935f3bc7e 100644 --- a/tests/typ/layout/shape-fill-stroke.typ +++ b/tests/typ/layout/shape-fill-stroke.typ @@ -13,7 +13,7 @@ rect(fill: eastern, stroke: none), rect(fill: forest, stroke: none, thickness: 2pt), rect(fill: forest, stroke: conifer), - rect(fill: forest, thickness: 2pt), + rect(fill: forest, stroke: black, thickness: 2pt), rect(fill: forest, stroke: conifer, thickness: 2pt), ) { (align(horizon)[{i + 1}.], rect, []) diff --git a/tests/typ/text/par.typ b/tests/typ/text/par.typ index 8bd43deb0..96a9eb6e7 100644 --- a/tests/typ/text/par.typ +++ b/tests/typ/text/par.typ @@ -25,11 +25,26 @@ World You +--- +// Test that paragraphs break due to incompatibility has correct spacing. +A #set par(spacing: 0pt); B #parbreak() C + +--- +// Test that paragraph breaks due to block nodes have the correct spacing. +- A + +#set par(spacing: 0pt) +- B +- C +#set par(spacing: 5pt) +- D +- E + --- // Test that paragraph break due to incompatibility respects // spacing defined by the two adjacent paragraphs. #let a = [#set par(spacing: 40pt);Hello] -#let b = [#set par(spacing: 60pt);World] +#let b = [#set par(spacing: 10pt);World] {a}{b} ---