summaryrefslogtreecommitdiff
path: root/src/library
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-12-20 14:18:29 +0100
committerLaurenz <laurmaedje@gmail.com>2021-12-21 00:20:24 +0100
commit11565a40b315212474f52eb576a9fd92b11f1132 (patch)
treec6b7afb35103065bc92b407094ca905bb75cfc73 /src/library
parent958f74f77707340f34ee36d09492bdb74523aa2a (diff)
Set Rules Episode IX: The Rise of Testing
Diffstat (limited to 'src/library')
-rw-r--r--src/library/document.rs20
-rw-r--r--src/library/grid.rs2
-rw-r--r--src/library/heading.rs63
-rw-r--r--src/library/list.rs102
-rw-r--r--src/library/mod.rs37
-rw-r--r--src/library/page.rs15
-rw-r--r--src/library/par.rs11
-rw-r--r--src/library/text.rs116
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,