diff options
| author | Laurenz <laurmaedje@gmail.com> | 2021-12-20 14:18:29 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2021-12-21 00:20:24 +0100 |
| commit | 11565a40b315212474f52eb576a9fd92b11f1132 (patch) | |
| tree | c6b7afb35103065bc92b407094ca905bb75cfc73 /src/library | |
| parent | 958f74f77707340f34ee36d09492bdb74523aa2a (diff) | |
Set Rules Episode IX: The Rise of Testing
Diffstat (limited to 'src/library')
| -rw-r--r-- | src/library/document.rs | 20 | ||||
| -rw-r--r-- | src/library/grid.rs | 2 | ||||
| -rw-r--r-- | src/library/heading.rs | 63 | ||||
| -rw-r--r-- | src/library/list.rs | 102 | ||||
| -rw-r--r-- | src/library/mod.rs | 37 | ||||
| -rw-r--r-- | src/library/page.rs | 15 | ||||
| -rw-r--r-- | src/library/par.rs | 11 | ||||
| -rw-r--r-- | src/library/text.rs | 116 |
8 files changed, 283 insertions, 83 deletions
diff --git a/src/library/document.rs b/src/library/document.rs deleted file mode 100644 index 84673270..00000000 --- a/src/library/document.rs +++ /dev/null @@ -1,20 +0,0 @@ -use super::prelude::*; -use super::PageNode; - -/// The root layout node, a document consisting of top-level page runs. -#[derive(Hash)] -pub struct DocumentNode(pub Vec<PageNode>); - -impl DocumentNode { - /// Layout the document into a sequence of frames, one per page. - pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> { - self.0.iter().flat_map(|node| node.layout(ctx)).collect() - } -} - -impl Debug for DocumentNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Document ")?; - f.debug_list().entries(&self.0).finish() - } -} diff --git a/src/library/grid.rs b/src/library/grid.rs index e82f09ef..d341cf5b 100644 --- a/src/library/grid.rs +++ b/src/library/grid.rs @@ -10,7 +10,7 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { Value::Relative(v) => vec![TrackSizing::Linear(v.into())], Value::Linear(v) => vec![TrackSizing::Linear(v)], Value::Fractional(v) => vec![TrackSizing::Fractional(v)], - Value::Int(count) => vec![TrackSizing::Auto; count.max(0) as usize], + Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast()?], Value::Array(values) => values .into_iter() .filter_map(|v| v.cast().ok()) diff --git a/src/library/heading.rs b/src/library/heading.rs new file mode 100644 index 00000000..c9777577 --- /dev/null +++ b/src/library/heading.rs @@ -0,0 +1,63 @@ +use super::prelude::*; +use super::{FontFamily, TextNode}; + +/// A section heading. +#[derive(Debug, Hash)] +pub struct HeadingNode { + /// The node that produces the heading's contents. + pub child: PackedNode, + /// The logical nesting depth of the section, starting from one. In the + /// default style, this controls the text size of the heading. + pub level: usize, +} + +#[properties] +impl HeadingNode { + /// The heading's font family. + pub const FAMILY: Smart<String> = Smart::Auto; + /// The fill color of heading in the text. Just the surrounding text color + /// if `auto`. + pub const FILL: Smart<Paint> = Smart::Auto; +} + +impl Construct for HeadingNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + Ok(Node::block(Self { + child: args.expect::<Node>("body")?.into_block(), + level: args.named("level")?.unwrap_or(1), + })) + } +} + +impl Set for HeadingNode { + fn set(styles: &mut Styles, args: &mut Args) -> TypResult<()> { + styles.set_opt(Self::FAMILY, args.named("family")?); + styles.set_opt(Self::FILL, args.named("fill")?); + Ok(()) + } +} + +impl Layout for HeadingNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec<Constrained<Rc<Frame>>> { + let upscale = (1.6 - 0.1 * self.level as f64).max(0.75); + ctx.styles.set(TextNode::STRONG, true); + ctx.styles.set(TextNode::SIZE, Relative::new(upscale).into()); + + if let Smart::Custom(family) = ctx.styles.get_ref(Self::FAMILY) { + let list: Vec<_> = std::iter::once(FontFamily::named(family)) + .chain(ctx.styles.get_ref(TextNode::FAMILY_LIST).iter().cloned()) + .collect(); + ctx.styles.set(TextNode::FAMILY_LIST, list); + } + + if let Smart::Custom(fill) = ctx.styles.get(Self::FILL) { + ctx.styles.set(TextNode::FILL, fill); + } + + self.child.layout(ctx, regions) + } +} diff --git a/src/library/list.rs b/src/library/list.rs new file mode 100644 index 00000000..74f0abe8 --- /dev/null +++ b/src/library/list.rs @@ -0,0 +1,102 @@ +use std::hash::Hash; + +use super::prelude::*; +use super::{GridNode, TextNode, TrackSizing}; + +/// An unordered or ordered list. +#[derive(Debug, Hash)] +pub struct ListNode<L> { + /// The node that produces the item's body. + pub child: PackedNode, + /// The list labelling style -- unordered or ordered. + pub labelling: L, +} + +#[properties] +impl<L: Labelling> ListNode<L> { + /// 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<L: Labelling> Construct for ListNode<L> { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + Ok(args + .all() + .map(|node: Node| { + Node::block(Self { + child: node.into_block(), + labelling: L::default(), + }) + }) + .sum()) + } +} + +impl<L: Labelling> Set for ListNode<L> { + fn set(styles: &mut Styles, args: &mut Args) -> TypResult<()> { + styles.set_opt(Self::LABEL_INDENT, args.named("label-indent")?); + styles.set_opt(Self::BODY_INDENT, args.named("body-indent")?); + Ok(()) + } +} + +impl<L: Labelling> Layout for ListNode<L> { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec<Constrained<Rc<Frame>>> { + let em = ctx.styles.get(TextNode::SIZE).abs; + let label_indent = ctx.styles.get(Self::LABEL_INDENT).resolve(em); + let body_indent = ctx.styles.get(Self::BODY_INDENT).resolve(em); + + let columns = vec![ + TrackSizing::Linear(label_indent.into()), + TrackSizing::Auto, + TrackSizing::Linear(body_indent.into()), + TrackSizing::Auto, + ]; + + let children = vec![ + PackedNode::default(), + Node::Text(self.labelling.label()).into_block(), + PackedNode::default(), + self.child.clone(), + ]; + + GridNode { + tracks: Spec::new(columns, vec![]), + gutter: Spec::default(), + children, + } + .layout(ctx, regions) + } +} + +/// How to label a list. +pub trait Labelling: Debug + Default + Hash + 'static { + /// Return the item's label. + fn label(&self) -> EcoString; +} + +/// Unordered list labelling style. +#[derive(Debug, Default, Hash)] +pub struct Unordered; + +impl Labelling for Unordered { + fn label(&self) -> EcoString { + '•'.into() + } +} + +/// Ordered list labelling style. +#[derive(Debug, Default, Hash)] +pub struct Ordered(pub Option<usize>); + +impl Labelling 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 5852f2bb..b2dd0dbe 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -4,11 +4,12 @@ //! definitions. mod align; -mod document; mod flow; mod grid; +mod heading; mod image; mod link; +mod list; mod pad; mod page; mod par; @@ -41,10 +42,11 @@ mod prelude { pub use self::image::*; pub use align::*; -pub use document::*; pub use flow::*; pub use grid::*; +pub use heading::*; pub use link::*; +pub use list::*; pub use pad::*; pub use page::*; pub use par::*; @@ -68,21 +70,29 @@ pub fn new() -> Scope { std.def_class::<PageNode>("page"); std.def_class::<ParNode>("par"); std.def_class::<TextNode>("text"); + std.def_class::<HeadingNode>("heading"); + std.def_class::<ListNode<Unordered>>("list"); + std.def_class::<ListNode<Ordered>>("enum"); // Text functions. + // TODO(style): These should be classes, once that works for inline nodes. std.def_func("strike", strike); std.def_func("underline", underline); std.def_func("overline", overline); std.def_func("link", link); - // Layout functions. + // Break and spacing functions. + std.def_func("pagebreak", pagebreak); + std.def_func("parbreak", parbreak); + std.def_func("linebreak", linebreak); std.def_func("h", h); std.def_func("v", v); + + // Layout functions. + // TODO(style): Decide which of these should be classes + // (and which of their properties should be settable). std.def_func("box", box_); std.def_func("block", block); - std.def_func("pagebreak", pagebreak); - std.def_func("parbreak", parbreak); - std.def_func("linebreak", linebreak); std.def_func("stack", stack); std.def_func("grid", grid); std.def_func("pad", pad); @@ -91,8 +101,6 @@ pub fn new() -> Scope { std.def_func("move", move_); std.def_func("scale", scale); std.def_func("rotate", rotate); - - // Element functions. std.def_func("image", image); std.def_func("rect", rect); std.def_func("square", square); @@ -118,6 +126,7 @@ pub fn new() -> Scope { std.def_func("sorted", sorted); // Predefined colors. + // TODO: More colors. std.def_const("white", RgbaColor::WHITE); std.def_const("black", RgbaColor::BLACK); std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); @@ -151,3 +160,15 @@ castable! { Expected: "color", Value::Color(color) => Paint::Solid(color), } + +castable! { + usize, + Expected: "non-negative integer", + Value::Int(int) => int.try_into().map_err(|_| "must be at least zero")?, +} + +castable! { + String, + Expected: "string", + Value::Str(string) => string.into(), +} diff --git a/src/library/page.rs b/src/library/page.rs index 3bb5cbd3..7fbcd058 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -44,8 +44,6 @@ impl PageNode { impl Construct for PageNode { fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { - // TODO(set): Make sure it's really a page so that it doesn't merge - // with adjacent pages. Ok(Node::Page(args.expect::<Node>("body")?.into_block())) } } @@ -69,13 +67,12 @@ impl Set for PageNode { } let margins = args.named("margins")?; - - set!(styles, Self::FLIPPED => args.named("flipped")?); - set!(styles, Self::LEFT => args.named("left")?.or(margins)); - set!(styles, Self::TOP => args.named("top")?.or(margins)); - set!(styles, Self::RIGHT => args.named("right")?.or(margins)); - set!(styles, Self::BOTTOM => args.named("bottom")?.or(margins)); - set!(styles, Self::FILL => args.named("fill")?); + styles.set_opt(Self::FLIPPED, args.named("flipped")?); + styles.set_opt(Self::LEFT, args.named("left")?.or(margins)); + styles.set_opt(Self::TOP, args.named("top")?.or(margins)); + styles.set_opt(Self::RIGHT, args.named("right")?.or(margins)); + styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(margins)); + styles.set_opt(Self::FILL, args.named("fill")?); Ok(()) } diff --git a/src/library/par.rs b/src/library/par.rs index 9a70b2c7..5dffd1c0 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -74,10 +74,10 @@ impl Set for ParNode { align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right }); } - set!(styles, Self::DIR => dir); - set!(styles, Self::ALIGN => align); - set!(styles, Self::LEADING => leading); - set!(styles, Self::SPACING => spacing); + styles.set_opt(Self::DIR, dir); + styles.set_opt(Self::ALIGN, align); + styles.set_opt(Self::LEADING, leading); + styles.set_opt(Self::SPACING, spacing); Ok(()) } @@ -93,8 +93,7 @@ impl Layout for ParNode { let text = self.collect_text(); // Find out the BiDi embedding levels. - let default_level = Level::from_dir(ctx.styles.get(Self::DIR)); - let bidi = BidiInfo::new(&text, default_level); + let bidi = BidiInfo::new(&text, Level::from_dir(ctx.styles.get(Self::DIR))); // Prepare paragraph layout by building a representation on which we can // do line breaking without layouting each and every line from scratch. diff --git a/src/library/text.rs b/src/library/text.rs index e0cbb1ad..4ff9b5cd 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -56,11 +56,11 @@ impl TextNode { /// A prioritized sequence of font families. pub const FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif]; /// The serif font family/families. - pub const SERIF_LIST: Vec<String> = vec!["ibm plex serif".into()]; + pub const SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")]; /// The sans-serif font family/families. - pub const SANS_SERIF_LIST: Vec<String> = vec!["ibm plex sans".into()]; + pub const SANS_SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")]; /// The monospace font family/families. - pub const MONOSPACE_LIST: Vec<String> = vec!["ibm plex mono".into()]; + pub const MONOSPACE_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Mono")]; /// Whether to allow font fallback when the primary font list contains no /// match. pub const FALLBACK: bool = true; @@ -139,32 +139,38 @@ impl Set for TextNode { (!families.is_empty()).then(|| families) }); - set!(styles, Self::FAMILY_LIST => list); - set!(styles, Self::SERIF_LIST => args.named("serif")?); - set!(styles, Self::SANS_SERIF_LIST => args.named("sans-serif")?); - set!(styles, Self::MONOSPACE_LIST => args.named("monospace")?); - set!(styles, Self::FALLBACK => args.named("fallback")?); - set!(styles, Self::STYLE => args.named("style")?); - set!(styles, Self::WEIGHT => args.named("weight")?); - set!(styles, Self::STRETCH => args.named("stretch")?); - set!(styles, Self::FILL => args.named("fill")?.or_else(|| args.find())); - set!(styles, Self::SIZE => args.named("size")?.or_else(|| args.find())); - set!(styles, Self::TRACKING => args.named("tracking")?.map(Em::new)); - set!(styles, Self::TOP_EDGE => args.named("top-edge")?); - set!(styles, Self::BOTTOM_EDGE => args.named("bottom-edge")?); - set!(styles, Self::KERNING => args.named("kerning")?); - set!(styles, Self::SMALLCAPS => args.named("smallcaps")?); - set!(styles, Self::ALTERNATES => args.named("alternates")?); - set!(styles, Self::STYLISTIC_SET => args.named("stylistic-set")?); - set!(styles, Self::LIGATURES => args.named("ligatures")?); - set!(styles, Self::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?); - set!(styles, Self::HISTORICAL_LIGATURES => args.named("historical-ligatures")?); - set!(styles, Self::NUMBER_TYPE => args.named("number-type")?); - set!(styles, Self::NUMBER_WIDTH => args.named("number-width")?); - set!(styles, Self::NUMBER_POSITION => args.named("number-position")?); - set!(styles, Self::SLASHED_ZERO => args.named("slashed-zero")?); - set!(styles, Self::FRACTIONS => args.named("fractions")?); - set!(styles, Self::FEATURES => args.named("features")?); + styles.set_opt(Self::FAMILY_LIST, list); + styles.set_opt(Self::SERIF_LIST, args.named("serif")?); + styles.set_opt(Self::SANS_SERIF_LIST, args.named("sans-serif")?); + styles.set_opt(Self::MONOSPACE_LIST, args.named("monospace")?); + styles.set_opt(Self::FALLBACK, args.named("fallback")?); + styles.set_opt(Self::STYLE, args.named("style")?); + styles.set_opt(Self::WEIGHT, args.named("weight")?); + styles.set_opt(Self::STRETCH, args.named("stretch")?); + styles.set_opt(Self::FILL, args.named("fill")?.or_else(|| args.find())); + styles.set_opt(Self::SIZE, args.named("size")?.or_else(|| args.find())); + styles.set_opt(Self::TRACKING, args.named("tracking")?.map(Em::new)); + styles.set_opt(Self::TOP_EDGE, args.named("top-edge")?); + styles.set_opt(Self::BOTTOM_EDGE, args.named("bottom-edge")?); + styles.set_opt(Self::KERNING, args.named("kerning")?); + styles.set_opt(Self::SMALLCAPS, args.named("smallcaps")?); + styles.set_opt(Self::ALTERNATES, args.named("alternates")?); + styles.set_opt(Self::STYLISTIC_SET, args.named("stylistic-set")?); + styles.set_opt(Self::LIGATURES, args.named("ligatures")?); + styles.set_opt( + Self::DISCRETIONARY_LIGATURES, + args.named("discretionary-ligatures")?, + ); + styles.set_opt( + Self::HISTORICAL_LIGATURES, + args.named("historical-ligatures")?, + ); + styles.set_opt(Self::NUMBER_TYPE, args.named("number-type")?); + styles.set_opt(Self::NUMBER_WIDTH, args.named("number-width")?); + styles.set_opt(Self::NUMBER_POSITION, args.named("number-position")?); + styles.set_opt(Self::SLASHED_ZERO, args.named("slashed-zero")?); + styles.set_opt(Self::FRACTIONS, args.named("fractions")?); + styles.set_opt(Self::FEATURES, args.named("features")?); Ok(()) } @@ -188,8 +194,15 @@ pub enum FontFamily { SansSerif, /// A family in which (almost) all glyphs are of equal width. Monospace, - /// A specific family with a name. - Named(String), + /// A specific font family like "Arial". + Named(NamedFamily), +} + +impl FontFamily { + /// Create a named font family variant, directly from a string. + pub fn named(string: &str) -> Self { + Self::Named(NamedFamily::new(string)) + } } impl Debug for FontFamily { @@ -203,15 +216,37 @@ impl Debug for FontFamily { } } +/// A specific font family like "Arial". +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct NamedFamily(String); + +impl NamedFamily { + /// Create a named font family variant. + pub fn new(string: &str) -> Self { + Self(string.to_lowercase()) + } + + /// The lowercased family name. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Debug for NamedFamily { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + dynamic! { FontFamily: "font family", - Value::Str(string) => Self::Named(string.to_lowercase().into()), + Value::Str(string) => Self::named(&string), } castable! { Vec<FontFamily>, Expected: "string, generic family or array thereof", - Value::Str(string) => vec![FontFamily::Named(string.to_lowercase().into())], + Value::Str(string) => vec![FontFamily::named(&string)], Value::Array(values) => { values.into_iter().filter_map(|v| v.cast().ok()).collect() }, @@ -219,13 +254,13 @@ castable! { } castable! { - Vec<String>, + Vec<NamedFamily>, Expected: "string or array of strings", - Value::Str(string) => vec![string.to_lowercase().into()], + Value::Str(string) => vec![NamedFamily::new(&string)], Value::Array(values) => values .into_iter() .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| string.to_lowercase().into()) + .map(|string: EcoString| NamedFamily::new(&string)) .collect(), } @@ -243,7 +278,10 @@ castable! { castable! { FontWeight, Expected: "integer or string", - Value::Int(v) => v.try_into().map_or(Self::BLACK, Self::from_number), + Value::Int(v) => Value::Int(v) + .cast::<usize>()? + .try_into() + .map_or(Self::BLACK, Self::from_number), Value::Str(string) => match string.as_str() { "thin" => Self::THIN, "extralight" => Self::EXTRALIGHT, @@ -681,7 +719,7 @@ fn families(styles: &Styles) -> impl Iterator<Item = &str> + Clone { head.iter() .chain(core) - .map(String::as_str) + .map(|named| named.as_str()) .chain(tail.iter().copied()) } @@ -770,7 +808,7 @@ pub struct ShapedText<'a> { /// The text direction. pub dir: Dir, /// The text's style properties. - // TODO(set): Go back to reference. + // TODO(style): Go back to reference. pub styles: Styles, /// The font size. pub size: Size, |
