Make all nodes into classes

This commit is contained in:
Laurenz 2022-01-07 21:24:36 +01:00
parent 0b62439090
commit e74ae6ce70
33 changed files with 726 additions and 591 deletions

View File

@ -7,48 +7,82 @@ use syn::parse_quote;
use syn::spanned::Spanned; use syn::spanned::Spanned;
use syn::{Error, Result}; use syn::{Error, Result};
/// Generate node properties. /// Turn a node into a class.
#[proc_macro_attribute] #[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); let impl_block = syn::parse_macro_input!(item as syn::ItemImpl);
expand(impl_block).unwrap_or_else(|err| err.to_compile_error()).into() 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<TokenStream2> { fn expand(mut impl_block: syn::ItemImpl) -> Result<TokenStream2> {
// Split the node type into name and generic type arguments. // 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_ty = &*impl_block.self_ty;
let (self_name, self_args) = parse_self(self_ty)?; let (self_name, self_args) = parse_self(self_ty)?;
// Rewrite the const items from values to keys. let module = quote::format_ident!("{}_types", self_name);
let mut modules = vec![];
for item in &mut impl_block.items { let mut key_modules = vec![];
if let syn::ImplItem::Const(item) = item { let mut construct = None;
let module = process_const( let mut set = None;
item,
&impl_block.generics, for item in std::mem::take(&mut impl_block.items) {
self_ty, match item {
&self_name, syn::ImplItem::Const(mut item) => {
&self_args, key_modules.push(process_const(
)?; &mut item, params, self_ty, &self_name, &self_args,
modules.push(module); )?);
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 // Put everything into a module with a hopefully unique type to isolate
// it from the outside. // it from the outside.
let module = quote::format_ident!("{}_types", self_name);
Ok(quote! { Ok(quote! {
#[allow(non_snake_case)] #[allow(non_snake_case)]
mod #module { mod #module {
use std::any::TypeId; use std::any::TypeId;
use std::marker::PhantomData; use std::marker::PhantomData;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use crate::eval::{Nonfolding, Property}; use crate::eval::{Construct, Nonfolding, Property, Set};
use super::*; use super::*;
#impl_block #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. /// Process a single const item.
fn process_const( fn process_const(
item: &mut syn::ImplItemConst, item: &mut syn::ImplItemConst,
impl_generics: &syn::Generics, params: &syn::punctuated::Punctuated<syn::GenericParam, syn::Token![,]>,
self_ty: &syn::Type, self_ty: &syn::Type,
self_name: &str, self_name: &str,
self_args: &[&syn::Type], self_args: &[&syn::Type],
@ -95,7 +129,6 @@ fn process_const(
let value_ty = &item.ty; let value_ty = &item.ty;
// ... but the real type of the const becomes Key<#key_args>. // ... 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)* }; let key_args = quote! { #value_ty #(, #self_args)* };
// The display name, e.g. `TextNode::STRONG`. // The display name, e.g. `TextNode::STRONG`.
@ -107,7 +140,7 @@ fn process_const(
let mut folder = None; let mut folder = None;
let mut nonfolding = Some(quote! { 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)]`. // Look for a folding function like `#[fold(u64::add)]`.
@ -132,16 +165,16 @@ fn process_const(
mod #module_name { mod #module_name {
use super::*; use super::*;
pub struct Key<T, #key_params>(pub PhantomData<(T, #key_args)>); pub struct Key<VALUE, #params>(pub PhantomData<(VALUE, #key_args)>);
impl<#key_params> Copy for Key<#key_args> {} impl<#params> Copy for Key<#key_args> {}
impl<#key_params> Clone for Key<#key_args> { impl<#params> Clone for Key<#key_args> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
*self *self
} }
} }
impl<#key_params> Property for Key<#key_args> { impl<#params> Property for Key<#key_args> {
type Value = #value_ty; type Value = #value_ty;
const NAME: &'static str = #name; const NAME: &'static str = #name;

View File

@ -248,7 +248,7 @@ impl Eval for ListNode {
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
Ok(Node::block(library::ListNode { Ok(Node::block(library::ListNode {
child: self.body().eval(ctx)?.into_block(), 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<Self::Output> { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
Ok(Node::block(library::ListNode { Ok(Node::block(library::ListNode {
child: self.body().eval(ctx)?.into_block(), 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; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let span = self.callee().span();
let callee = self.callee().eval(ctx)?; let callee = self.callee().eval(ctx)?;
let mut args = self.args().eval(ctx)?; let mut args = self.args().eval(ctx)?;
@ -470,13 +471,14 @@ impl Eval for CallExpr {
} }
Value::Class(class) => { 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()?; args.finish()?;
Ok(Value::Node(node)) Ok(Value::Node(node))
} }
v => bail!( v => bail!(
self.callee().span(), span,
"expected callable or collection, found {}", "expected callable or collection, found {}",
v.type_name(), v.type_name(),
), ),

View File

@ -10,7 +10,7 @@ use crate::diag::StrResult;
use crate::geom::SpecAxis; use crate::geom::SpecAxis;
use crate::layout::{Layout, PackedNode, RootNode}; use crate::layout::{Layout, PackedNode, RootNode};
use crate::library::{ use crate::library::{
FlowChild, FlowNode, PageNode, ParChild, ParNode, PlacedNode, SpacingKind, TextNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, PlaceNode, SpacingKind, TextNode,
}; };
use crate::util::EcoString; use crate::util::EcoString;
@ -98,6 +98,10 @@ impl Node {
/// Style this node with a full style map. /// Style this node with a full style map.
pub fn styled_with_map(mut self, styles: StyleMap) -> Self { 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 Self::Sequence(vec) = &mut self {
if let [styled] = vec.as_mut_slice() { if let [styled] = vec.as_mut_slice() {
styled.map.apply(&styles); styled.map.apply(&styles);
@ -193,7 +197,7 @@ impl Packer {
/// Finish up and return the resulting flow. /// Finish up and return the resulting flow.
fn into_block(mut self) -> PackedNode { fn into_block(mut self) -> PackedNode {
self.parbreak(None); self.parbreak(None, false);
FlowNode(self.flow.children).pack() FlowNode(self.flow.children).pack()
} }
@ -209,7 +213,7 @@ impl Packer {
Node::Space => { Node::Space => {
// A text space is "soft", meaning that it can be eaten up by // A text space is "soft", meaning that it can be eaten up by
// adjacent line breaks or explicit spacings. // 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 => { Node::Linebreak => {
// A line break eats up surrounding text spaces. // A line break eats up surrounding text spaces.
@ -222,12 +226,12 @@ impl Packer {
// styles (`Some(_)`) whereas paragraph breaks forced by // styles (`Some(_)`) whereas paragraph breaks forced by
// incompatibility take their styles from the preceding // incompatibility take their styles from the preceding
// paragraph. // paragraph.
self.parbreak(Some(styles)); self.parbreak(Some(styles), true);
} }
Node::Colbreak => { Node::Colbreak => {
// Explicit column breaks end the current paragraph and then // Explicit column breaks end the current paragraph and then
// discards the paragraph break. // discards the paragraph break.
self.parbreak(None); self.parbreak(None, false);
self.make_flow_compatible(&styles); self.make_flow_compatible(&styles);
self.flow.children.push(Styled::new(FlowChild::Skip, styles)); self.flow.children.push(Styled::new(FlowChild::Skip, styles));
self.flow.last.hard(); self.flow.last.hard();
@ -252,7 +256,7 @@ impl Packer {
Node::Spacing(SpecAxis::Vertical, kind) => { Node::Spacing(SpecAxis::Vertical, kind) => {
// Explicit vertical spacing ends the current paragraph and then // Explicit vertical spacing ends the current paragraph and then
// discards the paragraph break. // discards the paragraph break.
self.parbreak(None); self.parbreak(None, false);
self.make_flow_compatible(&styles); self.make_flow_compatible(&styles);
self.flow.children.push(Styled::new(FlowChild::Spacing(kind), styles)); self.flow.children.push(Styled::new(FlowChild::Spacing(kind), styles));
self.flow.last.hard(); self.flow.last.hard();
@ -284,14 +288,15 @@ impl Packer {
/// Insert an inline-level element into the current paragraph. /// Insert an inline-level element into the current paragraph.
fn push_inline(&mut self, child: Styled<ParChild>) { fn push_inline(&mut self, child: Styled<ParChild>) {
if let Some(styled) = self.par.last.any() {
self.push_coalescing(styled);
}
// The node must be both compatible with the current page and the // The node must be both compatible with the current page and the
// current paragraph. // current paragraph.
self.make_flow_compatible(&child.map); self.make_flow_compatible(&child.map);
self.make_par_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.push_coalescing(child);
self.par.last.any(); self.par.last.any();
} }
@ -314,13 +319,13 @@ impl Packer {
/// Insert a block-level element into the current flow. /// Insert a block-level element into the current flow.
fn push_block(&mut self, node: Styled<PackedNode>) { fn push_block(&mut self, node: Styled<PackedNode>) {
let placed = node.item.is::<PlacedNode>(); let placed = node.item.is::<PlaceNode>();
self.parbreak(None); self.parbreak(Some(node.map.clone()), false);
self.make_flow_compatible(&node.map); self.make_flow_compatible(&node.map);
self.flow.children.extend(self.flow.last.any()); self.flow.children.extend(self.flow.last.any());
self.flow.children.push(node.map(FlowChild::Node)); 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 // Prevent paragraph spacing between the placed node and the paragraph
// below it. // below it.
@ -330,18 +335,13 @@ impl Packer {
} }
/// Advance to the next paragraph. /// Advance to the next paragraph.
fn parbreak(&mut self, break_styles: Option<StyleMap>) { fn parbreak(&mut self, break_styles: Option<StyleMap>, important: bool) {
// Erase any styles that will be inherited anyway. // Erase any styles that will be inherited anyway.
let Builder { mut children, styles, .. } = mem::take(&mut self.par); let Builder { mut children, styles, .. } = mem::take(&mut self.par);
for Styled { map, .. } in &mut children { for Styled { map, .. } in &mut children {
map.erase(&styles); 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. // We don't want empty paragraphs.
if !children.is_empty() { if !children.is_empty() {
// The paragraph's children are all compatible with the page, so the // 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)); 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. // 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. /// Advance to the next page.
fn pagebreak(&mut self) { fn pagebreak(&mut self) {
if self.top { if self.top {
self.parbreak(None); self.parbreak(None, false);
// Take the flow and erase any styles that will be inherited anyway. // Take the flow and erase any styles that will be inherited anyway.
let Builder { mut children, styles, .. } = mem::take(&mut self.flow); let Builder { mut children, styles, .. } = mem::take(&mut self.flow);
@ -381,7 +397,7 @@ impl Packer {
} }
if !self.par.styles.compatible::<ParNode>(styles) { if !self.par.styles.compatible::<ParNode>(styles) {
self.parbreak(None); self.parbreak(Some(styles.clone()), false);
self.par.styles = styles.clone(); self.par.styles = styles.clone();
return; return;
} }
@ -441,8 +457,10 @@ enum Last<N> {
/// Hard nodes: Linebreaks and explicit spacing. /// Hard nodes: Linebreaks and explicit spacing.
Hard, Hard,
/// Soft nodes: Word spaces and paragraph breaks. These are saved here /// Soft nodes: Word spaces and paragraph breaks. These are saved here
/// temporarily and then applied once an `Any` node appears. /// temporarily and then applied once an `Any` node appears. The boolean
Soft(N), /// 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<N> Last<N> { impl<N> Last<N> {
@ -450,16 +468,19 @@ impl<N> Last<N> {
/// now if currently in `Soft` state. /// now if currently in `Soft` state.
fn any(&mut self) -> Option<N> { fn any(&mut self) -> Option<N> {
match mem::replace(self, Self::Any) { match mem::replace(self, Self::Any) {
Self::Soft(soft) => Some(soft), Self::Soft(soft, _) => Some(soft),
_ => None, _ => None,
} }
} }
/// Transition into the `Soft` state, but only if in `Any`. Otherwise, the /// Transition into the `Soft` state, but only if in `Any`. Otherwise, the
/// soft node is discarded. /// soft node is discarded.
fn soft(&mut self, soft: N) { fn soft(&mut self, soft: N, important: bool) {
if let Self::Any = self { if matches!(
*self = Self::Soft(soft); (&self, important),
(Self::Any, _) | (Self::Soft(_, false), true)
) {
*self = Self::Soft(soft, important);
} }
} }

View File

@ -397,7 +397,13 @@ primitive! { EcoString: "string", Str }
primitive! { Array: "array", Array } primitive! { Array: "array", Array }
primitive! { Dict: "dictionary", Dict } primitive! { Dict: "dictionary", Dict }
primitive! { Node: "template", Node } 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 } primitive! { Class: "class", Class }
impl Cast<Value> for Value { impl Cast<Value> for Value {

View File

@ -30,7 +30,7 @@ impl Transform {
} }
/// A scaling 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() } Self { sx, sy, ..Self::identity() }
} }

View File

@ -18,9 +18,9 @@ use std::rc::Rc;
use crate::eval::{StyleChain, Styled}; use crate::eval::{StyleChain, Styled};
use crate::font::FontStore; use crate::font::FontStore;
use crate::frame::Frame; 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::image::ImageStore;
use crate::library::{AlignNode, PadNode, PageNode, SizedNode, TransformNode}; use crate::library::{AlignNode, Move, PadNode, PageNode, SizedNode, TransformNode};
use crate::Context; use crate::Context;
/// The root layout node, a document consisting of top-level page runs. /// 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. /// Transform this node's contents without affecting layout.
pub fn moved(self, offset: Point) -> Self { pub fn moved(self, offset: Point) -> Self {
self.transformed(Transform::translation(offset.x, offset.y), Align::LEFT_TOP) if !offset.is_zero() {
} TransformNode {
kind: Move(offset.x, offset.y),
/// Transform this node's contents without affecting layout. child: self,
pub fn transformed(self, transform: Transform, origin: Spec<Align>) -> Self { }
if !transform.is_identity() { .pack()
TransformNode { transform, origin, child: self }.pack()
} else { } else {
self self
} }

View File

@ -3,14 +3,7 @@
use super::prelude::*; use super::prelude::*;
use super::ParNode; use super::ParNode;
/// `align`: Configure the alignment along the layouting axes. /// Align a node along the layouting axes.
pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
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.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct AlignNode { pub struct AlignNode {
/// How to align the node horizontally and vertically. /// How to align the node horizontally and vertically.
@ -19,6 +12,15 @@ pub struct AlignNode {
pub child: PackedNode, pub child: PackedNode,
} }
#[class]
impl AlignNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
let aligns: Spec<_> = args.find().unwrap_or_default();
let body: PackedNode = args.expect("body")?;
Ok(Node::block(body.aligned(aligns)))
}
}
impl Layout for AlignNode { impl Layout for AlignNode {
fn layout( fn layout(
&self, &self,

View File

@ -3,32 +3,34 @@
use super::prelude::*; use super::prelude::*;
use super::ParNode; use super::ParNode;
/// `columns`: Set content into multiple columns.
pub fn columns(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
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<Value> {
Ok(Value::Node(Node::Colbreak))
}
/// A node that separates a region into multiple equally sized columns. /// A node that separates a region into multiple equally sized columns.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct ColumnsNode { pub struct ColumnsNode {
/// How many columns there should be. /// How many columns there should be.
pub columns: NonZeroUsize, 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 /// The child to be layouted into the columns. Most likely, this should be a
/// flow or stack node. /// flow or stack node.
pub child: PackedNode, 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<Node> {
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 { impl Layout for ColumnsNode {
fn layout( fn layout(
&self, &self,
@ -57,7 +59,7 @@ impl Layout for ColumnsNode {
.iter() .iter()
.take(1 + regions.backlog.len() + regions.last.iter().len()) .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 width = (current.x - gutter * (columns - 1) as f64) / columns as f64;
let size = Size::new(width, current.y); let size = Size::new(width, current.y);
gutters.push(gutter); gutters.push(gutter);
@ -131,3 +133,13 @@ impl Layout for ColumnsNode {
finished finished
} }
} }
/// A column break.
pub struct ColbreakNode;
#[class]
impl ColbreakNode {
fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult<Node> {
Ok(Node::Colbreak)
}
}

79
src/library/deco.rs Normal file
View File

@ -0,0 +1,79 @@
//! Text decorations.
use super::prelude::*;
use super::TextNode;
/// Typeset underline, striken-through or overlined text.
pub struct DecoNode<L: LineKind>(pub L);
#[class]
impl<L: LineKind> DecoNode<L> {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
let deco = Decoration {
line: L::LINE,
stroke: args.named("stroke")?.or_else(|| args.find()),
thickness: args.named::<Linear>("thickness")?.or_else(|| args.find()),
offset: args.named("offset")?,
extent: args.named("extent")?.unwrap_or_default(),
};
Ok(args.expect::<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 Decoration {
/// Which line to draw.
pub line: DecoLine,
/// Stroke color of the line, defaults to the text color if `None`.
pub stroke: Option<Paint>,
/// Thickness of the line's strokes (dependent on scaled font size), read
/// from the font tables if `None`.
pub thickness: Option<Linear>,
/// Position of the line relative to the baseline (dependent on scaled font
/// size), read from the font tables if `None`.
pub offset: Option<Linear>,
/// 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;
}

View File

@ -3,7 +3,7 @@
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use super::prelude::*; 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. /// 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 // Placed nodes that are out of flow produce placed items which aren't
// aligned later. // aligned later.
if let Some(placed) = node.downcast::<PlacedNode>() { if let Some(placed) = node.downcast::<PlaceNode>() {
if placed.out_of_flow() { if placed.out_of_flow() {
let frame = node.layout(ctx, &self.regions, styles).remove(0); let frame = node.layout(ctx, &self.regions, styles).remove(0);
self.items.push(FlowItem::Placed(frame.item)); self.items.push(FlowItem::Placed(frame.item));

View File

@ -2,23 +2,6 @@
use super::prelude::*; use super::prelude::*;
/// `grid`: Arrange children into a grid.
pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let columns = args.named("columns")?.unwrap_or_default();
let rows = args.named("rows")?.unwrap_or_default();
let base_gutter: Vec<TrackSizing> = 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. /// A node that arranges its children in a grid.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct GridNode { pub struct GridNode {
@ -30,6 +13,25 @@ pub struct GridNode {
pub children: Vec<PackedNode>, pub children: Vec<PackedNode>,
} }
#[class]
impl GridNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
let columns = args.named("columns")?.unwrap_or_default();
let rows = args.named("rows")?.unwrap_or_default();
let base_gutter: Vec<TrackSizing> = 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 { impl Layout for GridNode {
fn layout( fn layout(
&self, &self,

View File

@ -13,25 +13,21 @@ pub struct HeadingNode {
pub child: PackedNode, pub child: PackedNode,
} }
#[properties] #[class]
impl HeadingNode { impl HeadingNode {
/// The heading's font family. /// The heading's font family.
pub const FAMILY: Smart<FontFamily> = Smart::Auto; pub const FAMILY: Smart<FontFamily> = Smart::Auto;
/// The fill color of text in the heading. Just the surrounding text color /// The fill color of text in the heading. Just the surrounding text color
/// if `auto`. /// if `auto`.
pub const FILL: Smart<Paint> = Smart::Auto; pub const FILL: Smart<Paint> = Smart::Auto;
}
impl Construct for HeadingNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(Node::block(Self { Ok(Node::block(Self {
child: args.expect("body")?, child: args.expect("body")?,
level: args.named("level")?.unwrap_or(1), level: args.named("level")?.unwrap_or(1),
})) }))
} }
}
impl Set for HeadingNode {
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
styles.set_opt(Self::FAMILY, args.named("family")?); styles.set_opt(Self::FAMILY, args.named("family")?);
styles.set_opt(Self::FILL, args.named("fill")?); styles.set_opt(Self::FILL, args.named("fill")?);

View File

@ -1,45 +1,41 @@
//! Raster and vector graphics. //! Raster and vector graphics.
use std::io;
use super::prelude::*; use super::prelude::*;
use super::TextNode;
use crate::diag::Error; use crate::diag::Error;
use crate::image::ImageId; use crate::image::ImageId;
/// `image`: An image.
pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
// Load the image.
let path = args.expect::<Spanned<EcoString>>("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. /// An image node.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct ImageNode { pub struct ImageNode(pub ImageId);
/// The id of the image file.
pub id: ImageId,
/// How the image should adjust itself to a given area.
pub fit: ImageFit,
}
#[properties] #[class]
impl ImageNode { impl ImageNode {
/// An URL the image should link to. /// How the image should adjust itself to a given area.
pub const LINK: Option<String> = None; pub const FIT: ImageFit = ImageFit::Cover;
fn construct(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
let path = args.expect::<Spanned<EcoString>>("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 { impl Layout for ImageNode {
@ -49,7 +45,7 @@ impl Layout for ImageNode {
regions: &Regions, regions: &Regions,
styles: StyleChain, styles: StyleChain,
) -> Vec<Constrained<Rc<Frame>>> { ) -> Vec<Constrained<Rc<Frame>>> {
let img = ctx.images.get(self.id); let img = ctx.images.get(self.0);
let pxw = img.width() as f64; let pxw = img.width() as f64;
let pxh = img.height() as f64; let pxh = img.height() as f64;
let px_ratio = pxw / pxh; let px_ratio = pxw / pxh;
@ -70,10 +66,11 @@ impl Layout for ImageNode {
Size::new(Length::pt(pxw), Length::pt(pxh)) Size::new(Length::pt(pxw), Length::pt(pxh))
}; };
// The actual size of the fitted image. // Compute the actual size of the fitted image.
let fitted = match self.fit { let fit = styles.get(Self::FIT);
let fitted = match fit {
ImageFit::Cover | ImageFit::Contain => { ImageFit::Cover | ImageFit::Contain => {
if wide == (self.fit == ImageFit::Contain) { if wide == (fit == ImageFit::Contain) {
Size::new(target.x, target.x / px_ratio) Size::new(target.x, target.x / px_ratio)
} else { } else {
Size::new(target.y * px_ratio, target.y) 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 // the frame to the target size, center aligning the image in the
// process. // process.
let mut frame = Frame::new(fitted); 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); frame.resize(target, Align::CENTER_HORIZON);
// Create a clipping group if only part of the image should be visible. // 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(); frame.clip();
} }
// Apply link if it exists. // 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); frame.link(url);
} }
@ -114,12 +111,6 @@ pub enum ImageFit {
Stretch, Stretch,
} }
impl Default for ImageFit {
fn default() -> Self {
Self::Cover
}
}
castable! { castable! {
ImageFit, ImageFit,
Expected: "string", Expected: "string",

View File

@ -1,23 +1,24 @@
//! Hyperlinking. //! Hyperlinking.
use super::prelude::*; use super::prelude::*;
use super::{ImageNode, ShapeNode, TextNode}; use super::TextNode;
use crate::util::EcoString; use crate::util::EcoString;
/// `link`: Link text and other elements to an URL. /// Link text and other elements to an URL.
pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub struct LinkNode;
let url: String = args.expect::<EcoString>("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())
});
let mut map = StyleMap::new(); #[class]
map.set(TextNode::LINK, Some(url.clone())); impl LinkNode {
map.set(ImageNode::LINK, Some(url.clone())); fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
map.set(ShapeNode::LINK, Some(url)); let url: String = args.expect::<EcoString>("url")?.into();
Ok(Value::Node(body.styled_with_map(map))) 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)))
}
} }

View File

@ -1,37 +1,31 @@
//! Unordered (bulleted) and ordered (numbered) lists. //! Unordered (bulleted) and ordered (numbered) lists.
use std::hash::Hash;
use super::prelude::*; use super::prelude::*;
use super::{GridNode, TextNode, TrackSizing}; use super::{GridNode, TextNode, TrackSizing};
/// An unordered or ordered list. /// An unordered or ordered list.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct ListNode<L> { pub struct ListNode<L: ListKind> {
/// The list labelling style -- unordered or ordered.
pub kind: L,
/// The node that produces the item's body. /// The node that produces the item's body.
pub child: PackedNode, pub child: PackedNode,
/// The list labelling style -- unordered or ordered.
pub labelling: L,
} }
#[properties] #[class]
impl<L: Labelling> ListNode<L> { impl<L: ListKind> ListNode<L> {
/// The indentation of each item's label. /// The indentation of each item's label.
pub const LABEL_INDENT: Linear = Relative::new(0.0).into(); pub const LABEL_INDENT: Linear = Relative::new(0.0).into();
/// The space between the label and the body of each item. /// The space between the label and the body of each item.
pub const BODY_INDENT: Linear = Relative::new(0.5).into(); pub const BODY_INDENT: Linear = Relative::new(0.5).into();
}
impl<L: Labelling> Construct for ListNode<L> {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(args Ok(args
.all() .all()
.map(|child: PackedNode| Node::block(Self { child, labelling: L::default() })) .map(|child: PackedNode| Node::block(Self { kind: L::default(), child }))
.sum()) .sum())
} }
}
impl<L: Labelling> Set for ListNode<L> {
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
styles.set_opt(Self::LABEL_INDENT, args.named("label-indent")?); styles.set_opt(Self::LABEL_INDENT, args.named("label-indent")?);
styles.set_opt(Self::BODY_INDENT, args.named("body-indent")?); styles.set_opt(Self::BODY_INDENT, args.named("body-indent")?);
@ -39,7 +33,7 @@ impl<L: Labelling> Set for ListNode<L> {
} }
} }
impl<L: Labelling> Layout for ListNode<L> { impl<L: ListKind> Layout for ListNode<L> {
fn layout( fn layout(
&self, &self,
ctx: &mut LayoutContext, ctx: &mut LayoutContext,
@ -60,7 +54,7 @@ impl<L: Labelling> Layout for ListNode<L> {
gutter: Spec::default(), gutter: Spec::default(),
children: vec![ children: vec![
PackedNode::default(), PackedNode::default(),
Node::Text(self.labelling.label()).into_block(), Node::Text(self.kind.label()).into_block(),
PackedNode::default(), PackedNode::default(),
self.child.clone(), self.child.clone(),
], ],
@ -71,7 +65,7 @@ impl<L: Labelling> Layout for ListNode<L> {
} }
/// How to label a list. /// How to label a list.
pub trait Labelling: Debug + Default + Hash + 'static { pub trait ListKind: Debug + Default + Hash + 'static {
/// Return the item's label. /// Return the item's label.
fn label(&self) -> EcoString; fn label(&self) -> EcoString;
} }
@ -80,7 +74,7 @@ pub trait Labelling: Debug + Default + Hash + 'static {
#[derive(Debug, Default, Hash)] #[derive(Debug, Default, Hash)]
pub struct Unordered; pub struct Unordered;
impl Labelling for Unordered { impl ListKind for Unordered {
fn label(&self) -> EcoString { fn label(&self) -> EcoString {
'•'.into() '•'.into()
} }
@ -90,7 +84,7 @@ impl Labelling for Unordered {
#[derive(Debug, Default, Hash)] #[derive(Debug, Default, Hash)]
pub struct Ordered(pub Option<usize>); pub struct Ordered(pub Option<usize>);
impl Labelling for Ordered { impl ListKind for Ordered {
fn label(&self) -> EcoString { fn label(&self) -> EcoString {
format_eco!("{}.", self.0.unwrap_or(1)) format_eco!("{}.", self.0.unwrap_or(1))
} }

View File

@ -5,6 +5,7 @@
pub mod align; pub mod align;
pub mod columns; pub mod columns;
pub mod deco;
pub mod flow; pub mod flow;
pub mod grid; pub mod grid;
pub mod heading; pub mod heading;
@ -26,6 +27,7 @@ pub mod utility;
pub use self::image::*; pub use self::image::*;
pub use align::*; pub use align::*;
pub use columns::*; pub use columns::*;
pub use deco::*;
pub use flow::*; pub use flow::*;
pub use grid::*; pub use grid::*;
pub use heading::*; pub use heading::*;
@ -56,8 +58,9 @@ prelude! {
pub use std::fmt::{self, Debug, Formatter}; pub use std::fmt::{self, Debug, Formatter};
pub use std::num::NonZeroUsize; pub use std::num::NonZeroUsize;
pub use std::rc::Rc; 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::diag::{At, TypResult};
pub use crate::eval::{ pub use crate::eval::{
@ -81,41 +84,39 @@ pub fn new() -> Scope {
// Structure and semantics. // Structure and semantics.
std.def_class::<PageNode>("page"); std.def_class::<PageNode>("page");
std.def_class::<PagebreakNode>("pagebreak");
std.def_class::<ParNode>("par"); std.def_class::<ParNode>("par");
std.def_class::<ParbreakNode>("parbreak");
std.def_class::<LinebreakNode>("linebreak");
std.def_class::<TextNode>("text"); std.def_class::<TextNode>("text");
std.def_func("underline", underline); std.def_class::<DecoNode<Underline>>("underline");
std.def_func("strike", strike); std.def_class::<DecoNode<Strikethrough>>("strike");
std.def_func("overline", overline); std.def_class::<DecoNode<Overline>>("overline");
std.def_func("link", link); std.def_class::<LinkNode>("link");
std.def_class::<HeadingNode>("heading"); std.def_class::<HeadingNode>("heading");
std.def_class::<ListNode<Unordered>>("list"); std.def_class::<ListNode<Unordered>>("list");
std.def_class::<ListNode<Ordered>>("enum"); std.def_class::<ListNode<Ordered>>("enum");
std.def_func("image", image); std.def_class::<ImageNode>("image");
std.def_func("rect", rect); std.def_class::<ShapeNode<Rect>>("rect");
std.def_func("square", square); std.def_class::<ShapeNode<Square>>("square");
std.def_func("ellipse", ellipse); std.def_class::<ShapeNode<Ellipse>>("ellipse");
std.def_func("circle", circle); std.def_class::<ShapeNode<Circle>>("circle");
// Layout. // Layout.
std.def_func("h", h); std.def_class::<HNode>("h");
std.def_func("v", v); std.def_class::<VNode>("v");
std.def_func("box", box_); std.def_class::<BoxNode>("box");
std.def_func("block", block); std.def_class::<BlockNode>("block");
std.def_func("align", align); std.def_class::<AlignNode>("align");
std.def_func("pad", pad); std.def_class::<PadNode>("pad");
std.def_func("place", place); std.def_class::<PlaceNode>("place");
std.def_func("move", move_); std.def_class::<TransformNode<Move>>("move");
std.def_func("scale", scale); std.def_class::<TransformNode<Scale>>("scale");
std.def_func("rotate", rotate); std.def_class::<TransformNode<Rotate>>("rotate");
std.def_func("stack", stack); std.def_class::<StackNode>("stack");
std.def_func("grid", grid); std.def_class::<GridNode>("grid");
std.def_func("columns", columns); std.def_class::<ColumnsNode>("columns");
std.def_class::<ColbreakNode>("colbreak");
// Breaks.
std.def_func("pagebreak", pagebreak);
std.def_func("colbreak", colbreak);
std.def_func("parbreak", parbreak);
std.def_func("linebreak", linebreak);
// Utility functions. // Utility functions.
std.def_func("assert", assert); std.def_func("assert", assert);

View File

@ -2,25 +2,7 @@
use super::prelude::*; use super::prelude::*;
/// `pad`: Pad content at the sides. /// Pad content at the sides.
pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
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.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct PadNode { pub struct PadNode {
/// The amount of padding. /// The amount of padding.
@ -29,6 +11,26 @@ pub struct PadNode {
pub child: PackedNode, pub child: PackedNode,
} }
#[class]
impl PadNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
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 { impl Layout for PadNode {
fn layout( fn layout(
&self, &self,

View File

@ -10,7 +10,7 @@ use super::ColumnsNode;
#[derive(Clone, PartialEq, Hash)] #[derive(Clone, PartialEq, Hash)]
pub struct PageNode(pub PackedNode); pub struct PageNode(pub PackedNode);
#[properties] #[class]
impl PageNode { impl PageNode {
/// The unflipped width of the page. /// The unflipped width of the page.
pub const WIDTH: Smart<Length> = Smart::Custom(Paper::default().width()); pub const WIDTH: Smart<Length> = Smart::Custom(Paper::default().width());
@ -32,17 +32,11 @@ impl PageNode {
pub const FILL: Option<Paint> = None; pub const FILL: Option<Paint> = None;
/// How many columns the page has. /// How many columns the page has.
pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); 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<Node> { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(Node::Page(Self(args.expect("body")?))) Ok(Node::Page(Self(args.expect("body")?)))
} }
}
impl Set for PageNode {
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) { if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) {
styles.set(Self::CLASS, paper.class()); 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::FLIPPED, args.named("flipped")?);
styles.set_opt(Self::FILL, args.named("fill")?); styles.set_opt(Self::FILL, args.named("fill")?);
styles.set_opt(Self::COLUMNS, args.named("columns")?); styles.set_opt(Self::COLUMNS, args.named("columns")?);
styles.set_opt(Self::COLUMN_GUTTER, args.named("column-gutter")?);
Ok(()) Ok(())
} }
@ -102,12 +95,7 @@ impl PageNode {
// Realize columns with columns node. // Realize columns with columns node.
let columns = styles.get(Self::COLUMNS); let columns = styles.get(Self::COLUMNS);
if columns.get() > 1 { if columns.get() > 1 {
child = ColumnsNode { child = ColumnsNode { columns, child: self.0.clone() }.pack();
columns,
gutter: styles.get(Self::COLUMN_GUTTER),
child: self.0.clone(),
}
.pack();
} }
// Realize margins with padding node. // Realize margins with padding node.
@ -142,9 +130,14 @@ impl Debug for PageNode {
} }
} }
/// `pagebreak`: Start a new page. /// A page break.
pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { pub struct PagebreakNode;
Ok(Value::Node(Node::Pagebreak))
#[class]
impl PagebreakNode {
fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult<Node> {
Ok(Node::Pagebreak)
}
} }
/// Specification of a paper. /// Specification of a paper.

View File

@ -15,7 +15,7 @@ use crate::util::{EcoString, RangeExt, RcExt, SliceExt};
#[derive(Hash)] #[derive(Hash)]
pub struct ParNode(pub Vec<Styled<ParChild>>); pub struct ParNode(pub Vec<Styled<ParChild>>);
#[properties] #[class]
impl ParNode { impl ParNode {
/// The direction for text and inline objects. /// The direction for text and inline objects.
pub const DIR: Dir = Dir::LTR; pub const DIR: Dir = Dir::LTR;
@ -25,9 +25,7 @@ impl ParNode {
pub const LEADING: Linear = Relative::new(0.65).into(); pub const LEADING: Linear = Relative::new(0.65).into();
/// The spacing between paragraphs (dependent on scaled font size). /// The spacing between paragraphs (dependent on scaled font size).
pub const SPACING: Linear = Relative::new(1.2).into(); pub const SPACING: Linear = Relative::new(1.2).into();
}
impl Construct for ParNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
// The paragraph constructor is special: It doesn't create a paragraph // The paragraph constructor is special: It doesn't create a paragraph
// since that happens automatically through markup. Instead, it just // 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. // adjacent stuff and it styles the contained paragraphs.
Ok(Node::Block(args.expect("body")?)) Ok(Node::Block(args.expect("body")?))
} }
}
impl Set for ParNode {
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
let spacing = args.named("spacing")?;
let leading = args.named("leading")?;
let mut dir = let mut dir =
args.named("lang")? args.named("lang")?
.map(|iso: EcoString| match iso.to_lowercase().as_str() { .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::DIR, dir);
styles.set_opt(Self::ALIGN, align); styles.set_opt(Self::ALIGN, align);
styles.set_opt(Self::LEADING, leading); styles.set_opt(Self::LEADING, args.named("leading")?);
styles.set_opt(Self::SPACING, spacing); styles.set_opt(Self::SPACING, args.named("spacing")?);
Ok(()) Ok(())
} }
@ -166,14 +159,24 @@ impl Debug for ParChild {
} }
} }
/// `parbreak`: Start a new paragraph. /// A paragraph break.
pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { pub struct ParbreakNode;
Ok(Value::Node(Node::Parbreak))
#[class]
impl ParbreakNode {
fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult<Node> {
Ok(Node::Parbreak)
}
} }
/// `linebreak`: Start a new line. /// A line break.
pub fn linebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { pub struct LinebreakNode;
Ok(Value::Node(Node::Linebreak))
#[class]
impl LinebreakNode {
fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult<Node> {
Ok(Node::Linebreak)
}
} }
/// A paragraph representation in which children are already layouted and text /// A paragraph representation in which children are already layouted and text

View File

@ -3,33 +3,24 @@
use super::prelude::*; use super::prelude::*;
use super::AlignNode; use super::AlignNode;
/// `place`: Place content at an absolute position. /// Place content at an absolute position.
pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
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.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct PlacedNode(pub PackedNode); pub struct PlaceNode(pub PackedNode);
impl PlacedNode { #[class]
/// Whether this node wants to be placed relative to its its parent's base impl PlaceNode {
/// origin. instead of relative to the parent's current flow/cursor fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
/// position. let aligns = args.find().unwrap_or(Spec::with_x(Some(Align::Left)));
pub fn out_of_flow(&self) -> bool { let tx = args.named("dx")?.unwrap_or_default();
self.0 let ty = args.named("dy")?.unwrap_or_default();
.downcast::<AlignNode>() let body: PackedNode = args.expect("body")?;
.map_or(false, |node| node.aligns.y.is_some()) Ok(Node::block(Self(
body.moved(Point::new(tx, ty)).aligned(aligns),
)))
} }
} }
impl Layout for PlacedNode { impl Layout for PlaceNode {
fn layout( fn layout(
&self, &self,
ctx: &mut LayoutContext, ctx: &mut LayoutContext,
@ -63,3 +54,14 @@ impl Layout for PlacedNode {
frames 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::<AlignNode>()
.map_or(false, |node| node.aligns.y.is_some())
}
}

View File

@ -3,110 +3,64 @@
use std::f64::consts::SQRT_2; use std::f64::consts::SQRT_2;
use super::prelude::*; use super::prelude::*;
use super::TextNode;
/// `rect`: A rectangle with optional content.
pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
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<Value> {
let size = args.named::<Length>("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<Value> {
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<Value> {
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<Linear>,
height: Option<Linear>,
) -> TypResult<Value> {
// 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::<Linear>("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)),
))
}
/// Places its child into a sizable and fillable shape. /// Places its child into a sizable and fillable shape.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct ShapeNode { pub struct ShapeNode<S: ShapeKind> {
/// Which shape to place the child into. /// Which shape to place the child into.
pub kind: ShapeKind, pub kind: S,
/// How to fill the shape.
pub fill: Option<Paint>,
/// How the stroke the shape.
pub stroke: Option<Stroke>,
/// The child node to place into the shape, if any. /// The child node to place into the shape, if any.
pub child: Option<PackedNode>, pub child: Option<PackedNode>,
} }
#[properties] #[class]
impl ShapeNode { impl<S: ShapeKind> ShapeNode<S> {
/// An URL the shape should link to. /// How to fill the shape.
pub const LINK: Option<String> = None; pub const FILL: Option<Paint> = None;
/// How the stroke the shape.
pub const STROKE: Smart<Option<Paint>> = 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<Node> {
let size = if !S::ROUND && S::QUADRATIC {
args.named::<Length>("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<S: ShapeKind> Layout for ShapeNode<S> {
fn layout( fn layout(
&self, &self,
ctx: &mut LayoutContext, ctx: &mut LayoutContext,
@ -115,12 +69,20 @@ impl Layout for ShapeNode {
) -> Vec<Constrained<Rc<Frame>>> { ) -> Vec<Constrained<Rc<Frame>>> {
let mut frames; let mut frames;
if let Some(child) = &self.child { 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); let mut pod = Regions::one(regions.current, regions.base, regions.expand);
frames = child.layout(ctx, &pod, styles); frames = child.layout(ctx, &pod, styles);
// Relayout with full expansion into square region to make sure // Relayout with full expansion into square region to make sure
// the result is really a square or circle. // 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 length = if regions.expand.x || regions.expand.y {
let target = regions.expand.select(regions.current, Size::zero()); let target = regions.expand.select(regions.current, Size::zero());
target.x.max(target.y) target.x.max(target.y)
@ -141,7 +103,7 @@ impl Layout for ShapeNode {
let mut size = let mut size =
Size::new(Length::pt(45.0), Length::pt(30.0)).min(regions.current); 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 length = if regions.expand.x || regions.expand.y {
let target = regions.expand.select(regions.current, Size::zero()); let target = regions.expand.select(regions.current, Size::zero());
target.x.max(target.y) target.x.max(target.y)
@ -159,23 +121,26 @@ impl Layout for ShapeNode {
let frame = Rc::make_mut(&mut frames[0].item); let frame = Rc::make_mut(&mut frames[0].item);
// Add fill and/or stroke. // Add fill and/or stroke.
if self.fill.is_some() || self.stroke.is_some() { let fill = styles.get(Self::FILL);
let geometry = match self.kind { let thickness = styles.get(Self::THICKNESS);
ShapeKind::Square | ShapeKind::Rect => Geometry::Rect(frame.size), let stroke = styles
ShapeKind::Circle | ShapeKind::Ellipse => Geometry::Ellipse(frame.size), .get(Self::STROKE)
}; .unwrap_or(fill.is_none().then(|| RgbaColor::BLACK.into()))
.map(|paint| Stroke { paint, thickness });
let shape = Shape {
geometry, if fill.is_some() || stroke.is_some() {
fill: self.fill, let geometry = if S::ROUND {
stroke: self.stroke, Geometry::Ellipse(frame.size)
} else {
Geometry::Rect(frame.size)
}; };
let shape = Shape { geometry, fill, stroke };
frame.prepend(Point::zero(), Element::Shape(shape)); frame.prepend(Point::zero(), Element::Shape(shape));
} }
// Apply link if it exists. // 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); frame.link(url);
} }
@ -183,27 +148,44 @@ impl Layout for ShapeNode {
} }
} }
/// The type of a shape. /// Categorizes shapes.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub trait ShapeKind: Debug + Default + Hash + 'static {
pub enum ShapeKind { const ROUND: bool;
/// A rectangle with equal side lengths. const QUADRATIC: bool;
Square,
/// A quadrilateral with four right angles.
Rect,
/// An ellipse with coinciding foci.
Circle,
/// A curve around two focal points.
Ellipse,
} }
impl ShapeKind { /// A rectangle with equal side lengths.
/// Whether the shape is curved. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub fn is_round(self) -> bool { pub struct Square;
matches!(self, Self::Circle | Self::Ellipse)
}
/// Whether the shape has a fixed 1-1 aspect ratio. impl ShapeKind for Square {
pub fn is_quadratic(self) -> bool { const ROUND: bool = false;
matches!(self, Self::Square | Self::Circle) 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;
} }

View File

@ -2,18 +2,27 @@
use super::prelude::*; use super::prelude::*;
/// `box`: Size content and place it into a paragraph. /// Size content and place it into a paragraph.
pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub struct BoxNode;
let width = args.named("width")?;
let height = args.named("height")?; #[class]
let body: PackedNode = args.find().unwrap_or_default(); impl BoxNode {
Ok(Value::inline(body.sized(Spec::new(width, height)))) fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
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. /// Place content into a separate flow.
pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub struct BlockNode;
let body: PackedNode = args.find().unwrap_or_default();
Ok(Value::block(body)) #[class]
impl BlockNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(Node::Block(args.find().unwrap_or_default()))
}
} }
/// A node that sizes its child. /// A node that sizes its child.

View File

@ -2,20 +2,24 @@
use super::prelude::*; use super::prelude::*;
/// `h`: Horizontal spacing. /// Horizontal spacing.
pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub struct HNode;
Ok(Value::Node(Node::Spacing(
SpecAxis::Horizontal, #[class]
args.expect("spacing")?, impl HNode {
))) fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(Node::Spacing(SpecAxis::Horizontal, args.expect("spacing")?))
}
} }
/// `v`: Vertical spacing. /// Vertical spacing.
pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub struct VNode;
Ok(Value::Node(Node::Spacing(
SpecAxis::Vertical, #[class]
args.expect("spacing")?, impl VNode {
))) fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(Node::Spacing(SpecAxis::Vertical, args.expect("spacing")?))
}
} }
/// Kinds of spacing. /// Kinds of spacing.

View File

@ -3,16 +3,7 @@
use super::prelude::*; use super::prelude::*;
use super::{AlignNode, SpacingKind}; use super::{AlignNode, SpacingKind};
/// `stack`: Stack children along an axis. /// Stack children along an axis.
pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
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.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct StackNode { pub struct StackNode {
/// The stacking direction. /// The stacking direction.
@ -23,6 +14,17 @@ pub struct StackNode {
pub children: Vec<StackChild>, pub children: Vec<StackChild>,
} }
#[class]
impl StackNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(Node::block(Self {
dir: args.named("dir")?.unwrap_or(Dir::TTB),
spacing: args.named("spacing")?,
children: args.all().collect(),
}))
}
}
impl Layout for StackNode { impl Layout for StackNode {
fn layout( fn layout(
&self, &self,

View File

@ -9,6 +9,7 @@ use rustybuzz::{Feature, UnicodeBuffer};
use ttf_parser::Tag; use ttf_parser::Tag;
use super::prelude::*; use super::prelude::*;
use super::{DecoLine, Decoration};
use crate::font::{ use crate::font::{
Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight, Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight,
VerticalFontMetric, VerticalFontMetric,
@ -20,7 +21,7 @@ use crate::util::{EcoString, SliceExt};
#[derive(Hash)] #[derive(Hash)]
pub struct TextNode(pub EcoString); pub struct TextNode(pub EcoString);
#[properties] #[class]
impl TextNode { impl TextNode {
/// A prioritized sequence of font families. /// A prioritized sequence of font families.
pub const FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif]; pub const FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif];
@ -52,7 +53,7 @@ impl TextNode {
pub const FILL: Paint = RgbaColor::BLACK.into(); pub const FILL: Paint = RgbaColor::BLACK.into();
/// Decorative lines. /// Decorative lines.
#[fold(|a, b| a.into_iter().chain(b).collect())] #[fold(|a, b| a.into_iter().chain(b).collect())]
pub const LINES: Vec<LineDecoration> = vec![]; pub const LINES: Vec<Decoration> = vec![];
/// An URL the text should link to. /// An URL the text should link to.
pub const LINK: Option<String> = None; pub const LINK: Option<String> = None;
@ -92,18 +93,14 @@ impl TextNode {
pub const FRACTIONS: bool = false; pub const FRACTIONS: bool = false;
/// Raw OpenType features to apply. /// Raw OpenType features to apply.
pub const FEATURES: Vec<(Tag, u32)> = vec![]; pub const FEATURES: Vec<(Tag, u32)> = vec![];
}
impl Construct for TextNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
// The text constructor is special: It doesn't create a text node. // The text constructor is special: It doesn't create a text node.
// Instead, it leaves the passed argument structurally unchanged, but // Instead, it leaves the passed argument structurally unchanged, but
// styles all text in it. // styles all text in it.
args.expect("body") args.expect("body")
} }
}
impl Set for TextNode {
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
let list = args.named("family")?.or_else(|| { let list = args.named("family")?.or_else(|| {
let families: Vec<_> = args.all().collect(); let families: Vec<_> = args.all().collect();
@ -382,60 +379,6 @@ castable! {
.collect(), .collect(),
} }
/// `strike`: Typeset striken-through text.
pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
line_impl(args, LineKind::Strikethrough)
}
/// `underline`: Typeset underlined text.
pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
line_impl(args, LineKind::Underline)
}
/// `overline`: Typeset text with an overline.
pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
line_impl(args, LineKind::Overline)
}
fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
let stroke = args.named("stroke")?.or_else(|| args.find());
let thickness = args.named::<Linear>("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<Paint>,
/// Thickness of the line's strokes (dependent on scaled font size), read
/// from the font tables if `None`.
pub thickness: Option<Linear>,
/// Position of the line relative to the baseline (dependent on scaled font
/// size), read from the font tables if `None`.
pub offset: Option<Linear>,
/// 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`]. /// Shape text into [`ShapedText`].
pub fn shape<'a>( pub fn shape<'a>(
fonts: &mut FontStore, fonts: &mut FontStore,
@ -848,23 +791,23 @@ impl<'a> ShapedText<'a> {
frame.push(pos, Element::Text(text)); frame.push(pos, Element::Text(text));
// Apply line decorations. // 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 face = fonts.get(face_id);
let metrics = match line.kind { let metrics = match deco.line {
LineKind::Underline => face.underline, DecoLine::Underline => face.underline,
LineKind::Strikethrough => face.strikethrough, DecoLine::Strikethrough => face.strikethrough,
LineKind::Overline => face.overline, DecoLine::Overline => face.overline,
}; };
let extent = line.extent.resolve(size); let extent = deco.extent.resolve(size);
let offset = line let offset = deco
.offset .offset
.map(|s| s.resolve(size)) .map(|s| s.resolve(size))
.unwrap_or(-metrics.position.resolve(size)); .unwrap_or(-metrics.position.resolve(size));
let stroke = Stroke { let stroke = Stroke {
paint: line.stroke.unwrap_or(fill), paint: deco.stroke.unwrap_or(fill),
thickness: line thickness: deco
.thickness .thickness
.map(|s| s.resolve(size)) .map(|s| s.resolve(size))
.unwrap_or(metrics.thickness.resolve(size)), .unwrap_or(metrics.thickness.resolve(size)),

View File

@ -3,64 +3,49 @@
use super::prelude::*; use super::prelude::*;
use crate::geom::Transform; use crate::geom::Transform;
/// `move`: Move content without affecting layout.
pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
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<Value> {
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<Value> {
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<Value> {
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. /// A node that transforms its child without affecting layout.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct TransformNode { pub struct TransformNode<T: TransformKind> {
/// Transformation to apply to the contents. /// Transformation to apply to the contents.
pub transform: Transform, pub kind: T,
/// The origin of the transformation.
pub origin: Spec<Align>,
/// The node whose contents should be transformed. /// The node whose contents should be transformed.
pub child: PackedNode, pub child: PackedNode,
} }
impl Layout for TransformNode { #[class]
impl<T: TransformKind> TransformNode<T> {
/// The origin of the transformation.
pub const ORIGIN: Spec<Option<Align>> = Spec::default();
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
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<T: TransformKind> Layout for TransformNode<T> {
fn layout( fn layout(
&self, &self,
ctx: &mut LayoutContext, ctx: &mut LayoutContext,
regions: &Regions, regions: &Regions,
styles: StyleChain, styles: StyleChain,
) -> Vec<Constrained<Rc<Frame>>> { ) -> Vec<Constrained<Rc<Frame>>> {
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); let mut frames = self.child.layout(ctx, regions, styles);
for Constrained { item: frame, .. } in &mut frames { 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) let transform = Transform::translation(x, y)
.pre_concat(self.transform) .pre_concat(matrix)
.pre_concat(Transform::translation(-x, -y)); .pre_concat(Transform::translation(-x, -y));
Rc::make_mut(frame).transform(transform); Rc::make_mut(frame).transform(transform);
@ -69,3 +54,58 @@ impl Layout for TransformNode {
frames frames
} }
} }
/// Kinds of transformations.
pub trait TransformKind: Debug + Hash + Sized + 'static {
fn construct(args: &mut Args) -> TypResult<Self>;
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<Self> {
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<Self> {
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<Self> {
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)
}
}

View File

@ -6,7 +6,7 @@ use std::str::FromStr;
use super::prelude::*; use super::prelude::*;
use crate::eval::Array; 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<Value> { pub fn assert(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect::<Spanned<bool>>("condition")?; let Spanned { v, span } = args.expect::<Spanned<bool>>("condition")?;
if !v { if !v {
@ -15,18 +15,17 @@ pub fn assert(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(Value::None) 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<Value> { pub fn type_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(args.expect::<Value>("value")?.type_name().into()) Ok(args.expect::<Value>("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<Value> { pub fn repr(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(args.expect::<Value>("value")?.repr().into()) Ok(args.expect::<Value>("value")?.repr().into())
} }
/// `join`: Join a sequence of values, optionally interspersing it with another /// Join a sequence of values, optionally interspersing it with another value.
/// value.
pub fn join(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn join(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let span = args.span; let span = args.span;
let sep = args.named::<Value>("sep")?.unwrap_or(Value::None); let sep = args.named::<Value>("sep")?.unwrap_or(Value::None);
@ -46,7 +45,7 @@ pub fn join(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(result) Ok(result)
} }
/// `int`: Convert a value to a integer. /// Convert a value to a integer.
pub fn int(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn int(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect("value")?; let Spanned { v, span } = args.expect("value")?;
Ok(Value::Int(match v { Ok(Value::Int(match v {
@ -61,7 +60,7 @@ pub fn int(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
})) }))
} }
/// `float`: Convert a value to a float. /// Convert a value to a float.
pub fn float(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn float(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect("value")?; let Spanned { v, span } = args.expect("value")?;
Ok(Value::Float(match v { Ok(Value::Float(match v {
@ -75,7 +74,7 @@ pub fn float(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
})) }))
} }
/// `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<Value> { pub fn str(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect("value")?; let Spanned { v, span } = args.expect("value")?;
Ok(Value::Str(match v { Ok(Value::Str(match v {
@ -86,7 +85,7 @@ pub fn str(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
})) }))
} }
/// `rgb`: Create an RGB(A) color. /// Create an RGB(A) color.
pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(Value::from( Ok(Value::from(
if let Some(string) = args.find::<Spanned<EcoString>>() { if let Some(string) = args.find::<Spanned<EcoString>>() {
@ -111,7 +110,7 @@ pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
)) ))
} }
/// `abs`: The absolute value of a numeric value. /// The absolute value of a numeric value.
pub fn abs(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn abs(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect("numeric value")?; let Spanned { v, span } = args.expect("numeric value")?;
Ok(match v { Ok(match v {
@ -126,12 +125,12 @@ pub fn abs(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
}) })
} }
/// `min`: The minimum of a sequence of values. /// The minimum of a sequence of values.
pub fn min(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn min(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
minmax(args, Ordering::Less) 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<Value> { pub fn max(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
minmax(args, Ordering::Greater) minmax(args, Ordering::Greater)
} }
@ -157,7 +156,7 @@ fn minmax(args: &mut Args, goal: Ordering) -> TypResult<Value> {
Ok(extremum) Ok(extremum)
} }
/// `range`: Create a sequence of numbers. /// Create a sequence of numbers.
pub fn range(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn range(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let first = args.expect::<i64>("end")?; let first = args.expect::<i64>("end")?;
let (start, end) = match args.eat::<i64>()? { let (start, end) = match args.eat::<i64>()? {
@ -182,17 +181,17 @@ pub fn range(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(Value::Array(Array::from_vec(seq))) 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<Value> { pub fn lower(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(args.expect::<EcoString>("string")?.to_lowercase().into()) Ok(args.expect::<EcoString>("string")?.to_lowercase().into())
} }
/// `upper`: Convert a string to uppercase. /// Convert a string to uppercase.
pub fn upper(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn upper(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(args.expect::<EcoString>("string")?.to_uppercase().into()) Ok(args.expect::<EcoString>("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<Value> { pub fn len(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect("collection")?; let Spanned { v, span } = args.expect("collection")?;
Ok(Value::Int(match v { Ok(Value::Int(match v {
@ -207,7 +206,7 @@ pub fn len(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
})) }))
} }
/// `sorted`: The sorted version of an array. /// The sorted version of an array.
pub fn sorted(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn sorted(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect::<Spanned<Array>>("array")?; let Spanned { v, span } = args.expect::<Spanned<Array>>("array")?;
Ok(Value::Array(v.sorted().at(span)?)) Ok(Value::Array(v.sorted().at(span)?))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -2,7 +2,8 @@
--- ---
// Test normal operation and RTL directions. // 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 text("Noto Sans Arabic", serif)
#set par(lang: "ar") #set par(lang: "ar")
@ -10,7 +11,7 @@
العديد من التفاعلات الكيميائية. (DNA) من أهم الأحماض النووية التي تُشكِّل العديد من التفاعلات الكيميائية. (DNA) من أهم الأحماض النووية التي تُشكِّل
إلى جانب كل من البروتينات والليبيدات والسكريات المتعددة إلى جانب كل من البروتينات والليبيدات والسكريات المتعددة
#rect(fill: eastern, height: 8pt, width: 6pt) #rect(fill: eastern, height: 8pt, width: 6pt)
الجزيئات الضخمة الأربعة الضرورية للحياة. الجزيئات الضخمة الأربعة الضرورية للحياة.
--- ---
// Test the `columns` function. // Test the `columns` function.
@ -28,7 +29,7 @@
#set page(height: 5cm, width: 7.05cm, columns: 2) #set page(height: 5cm, width: 7.05cm, columns: 2)
Lorem ipsum dolor sit amet is a common blind text 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)) #align(bottom, rect(fill: eastern, width: 100%, height: 12pt))
#colbreak() #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. // 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: 2.5cm, fill: conifer)
#rect(width: 100%, height: 2cm, fill: eastern) #rect(width: 100%, height: 2cm, fill: eastern)

View File

@ -9,7 +9,7 @@
// Test auto sizing. // Test auto sizing.
Auto-sized circle. \ Auto-sized circle. \
#circle(fill: rgb("eb5278"), thickness: 2pt, #circle(fill: rgb("eb5278"), stroke: black, thickness: 2pt,
align(center + horizon)[But, soft!] align(center + horizon)[But, soft!]
) )

View File

@ -13,7 +13,7 @@
rect(fill: eastern, stroke: none), rect(fill: eastern, stroke: none),
rect(fill: forest, stroke: none, thickness: 2pt), rect(fill: forest, stroke: none, thickness: 2pt),
rect(fill: forest, stroke: conifer), rect(fill: forest, stroke: conifer),
rect(fill: forest, thickness: 2pt), rect(fill: forest, stroke: black, thickness: 2pt),
rect(fill: forest, stroke: conifer, thickness: 2pt), rect(fill: forest, stroke: conifer, thickness: 2pt),
) { ) {
(align(horizon)[{i + 1}.], rect, []) (align(horizon)[{i + 1}.], rect, [])

View File

@ -25,11 +25,26 @@ World
You 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 // Test that paragraph break due to incompatibility respects
// spacing defined by the two adjacent paragraphs. // spacing defined by the two adjacent paragraphs.
#let a = [#set par(spacing: 40pt);Hello] #let a = [#set par(spacing: 40pt);Hello]
#let b = [#set par(spacing: 60pt);World] #let b = [#set par(spacing: 10pt);World]
{a}{b} {a}{b}
--- ---