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 4e5ebb13e..e0749e128 100644 Binary files a/tests/ref/code/repr.png and b/tests/ref/code/repr.png differ diff --git a/tests/ref/text/par.png b/tests/ref/text/par.png index 03117e673..bb705a193 100644 Binary files a/tests/ref/text/par.png and b/tests/ref/text/par.png differ 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} ---