summaryrefslogtreecommitdiff
path: root/src/library
diff options
context:
space:
mode:
Diffstat (limited to 'src/library')
-rw-r--r--src/library/align.rs51
-rw-r--r--src/library/container.rs27
-rw-r--r--src/library/deco.rs137
-rw-r--r--src/library/elements.rs104
-rw-r--r--src/library/grid.rs602
-rw-r--r--src/library/image.rs68
-rw-r--r--src/library/layout.rs332
-rw-r--r--src/library/mod.rs48
-rw-r--r--src/library/pad.rs101
-rw-r--r--src/library/page.rs76
-rw-r--r--src/library/par.rs674
-rw-r--r--src/library/shape.rs185
-rw-r--r--src/library/spacing.rs24
-rw-r--r--src/library/stack.rs274
-rw-r--r--src/library/text.rs420
-rw-r--r--src/library/transform.rs44
-rw-r--r--src/library/utility.rs3
17 files changed, 2648 insertions, 522 deletions
diff --git a/src/library/align.rs b/src/library/align.rs
new file mode 100644
index 00000000..c6f96a13
--- /dev/null
+++ b/src/library/align.rs
@@ -0,0 +1,51 @@
+use super::prelude::*;
+
+/// `align`: Configure the alignment along the layouting axes.
+pub fn align(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let first = args.find::<Align>();
+ let second = args.find::<Align>();
+ let body = args.find::<Template>();
+
+ let mut horizontal = args.named("horizontal")?;
+ let mut vertical = args.named("vertical")?;
+
+ for value in first.into_iter().chain(second) {
+ match value.axis() {
+ Some(SpecAxis::Horizontal) | None if horizontal.is_none() => {
+ horizontal = Some(value);
+ }
+ Some(SpecAxis::Vertical) | None if vertical.is_none() => {
+ vertical = Some(value);
+ }
+ _ => {}
+ }
+ }
+
+ let realign = |template: &mut Template| {
+ template.modify(move |style| {
+ if let Some(horizontal) = horizontal {
+ style.aligns.inline = horizontal;
+ }
+
+ if let Some(vertical) = vertical {
+ style.aligns.block = vertical;
+ }
+ });
+
+ if vertical.is_some() {
+ template.parbreak();
+ }
+ };
+
+ Ok(if let Some(body) = body {
+ let mut template = Template::new();
+ template.save();
+ realign(&mut template);
+ template += body;
+ template.restore();
+ Value::Template(template)
+ } else {
+ realign(&mut ctx.template);
+ Value::None
+ })
+}
diff --git a/src/library/container.rs b/src/library/container.rs
new file mode 100644
index 00000000..b2591cce
--- /dev/null
+++ b/src/library/container.rs
@@ -0,0 +1,27 @@
+use super::prelude::*;
+use super::{ShapeKind, ShapeNode};
+
+/// `box`: Place content in a rectangular box.
+pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+ let fill = args.named("fill")?;
+ let body: Template = args.find().unwrap_or_default();
+ Ok(Value::Template(Template::from_inline(move |style| {
+ ShapeNode {
+ shape: ShapeKind::Rect,
+ width,
+ height,
+ fill: fill.map(Paint::Color),
+ child: Some(body.to_stack(style).pack()),
+ }
+ })))
+}
+
+/// `block`: Place content in a block.
+pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let body: Template = args.expect("body")?;
+ Ok(Value::Template(Template::from_block(move |style| {
+ body.to_stack(style)
+ })))
+}
diff --git a/src/library/deco.rs b/src/library/deco.rs
new file mode 100644
index 00000000..18ca2bb1
--- /dev/null
+++ b/src/library/deco.rs
@@ -0,0 +1,137 @@
+use super::prelude::*;
+use crate::util::EcoString;
+
+/// `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: Template = args.expect("body")?;
+
+ Ok(Value::Template(body.decorate(Decoration::Line(
+ LineDecoration {
+ kind,
+ stroke: stroke.map(Paint::Color),
+ thickness,
+ offset,
+ extent,
+ },
+ ))))
+}
+
+/// `link`: Typeset text as a link.
+pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let url = args.expect::<Str>("url")?;
+ let body = args.find().unwrap_or_else(|| {
+ let mut template = Template::new();
+ template.text(url.trim_start_matches("mailto:").trim_start_matches("tel:"));
+ template
+ });
+
+ Ok(Value::Template(body.decorate(Decoration::Link(url.into()))))
+}
+
+/// A decoration for a frame.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub enum Decoration {
+ /// A link to an external resource.
+ Link(EcoString),
+ /// An underline/strikethrough/overline decoration.
+ Line(LineDecoration),
+}
+
+impl Decoration {
+ /// Apply a decoration to a child's frame.
+ pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) {
+ match self {
+ Decoration::Link(href) => {
+ let link = Element::Link(href.to_string(), frame.size);
+ frame.push(Point::zero(), link);
+ }
+ Decoration::Line(line) => {
+ line.apply(ctx, frame);
+ }
+ }
+ }
+}
+
+/// 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,
+}
+
+impl LineDecoration {
+ /// Apply a line decoration to a all text elements in a frame.
+ pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) {
+ for i in 0 .. frame.children.len() {
+ let (pos, child) = &frame.children[i];
+ if let FrameChild::Element(Element::Text(text)) = child {
+ let face = ctx.fonts.get(text.face_id);
+ let metrics = match self.kind {
+ LineKind::Underline => face.underline,
+ LineKind::Strikethrough => face.strikethrough,
+ LineKind::Overline => face.overline,
+ };
+
+ let stroke = self.stroke.unwrap_or(text.fill);
+
+ let thickness = self
+ .thickness
+ .map(|s| s.resolve(text.size))
+ .unwrap_or(metrics.strength.to_length(text.size));
+
+ let offset = self
+ .offset
+ .map(|s| s.resolve(text.size))
+ .unwrap_or(-metrics.position.to_length(text.size));
+
+ let extent = self.extent.resolve(text.size);
+
+ let subpos = Point::new(pos.x - extent, pos.y + offset);
+ let vector = Point::new(text.width + 2.0 * extent, Length::zero());
+ let line = Geometry::Line(vector, thickness);
+
+ frame.push(subpos, Element::Geometry(line, stroke));
+ }
+ }
+ }
+}
diff --git a/src/library/elements.rs b/src/library/elements.rs
deleted file mode 100644
index 01efb405..00000000
--- a/src/library/elements.rs
+++ /dev/null
@@ -1,104 +0,0 @@
-use std::io;
-
-use super::*;
-use crate::diag::Error;
-use crate::layout::{ImageNode, ShapeKind, ShapeNode};
-
-/// `image`: An image.
-pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let path = args.expect::<Spanned<Str>>("path to image file")?;
- let width = args.named("width")?;
- let height = args.named("height")?;
-
- 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),
- })
- })?;
-
- Ok(Value::Template(Template::from_inline(move |_| ImageNode {
- id,
- width,
- height,
- })))
-}
-
-/// `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")?;
- let fill = args.named("fill")?;
- let body = args.find();
- Ok(shape_impl(ShapeKind::Rect, width, height, fill, body))
-}
-
-/// `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,
- };
- let fill = args.named("fill")?;
- let body = args.find();
- Ok(shape_impl(ShapeKind::Square, width, height, fill, body))
-}
-
-/// `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")?;
- let fill = args.named("fill")?;
- let body = args.find();
- Ok(shape_impl(ShapeKind::Ellipse, width, height, fill, body))
-}
-
-/// `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,
- };
- let fill = args.named("fill")?;
- let body = args.find();
- Ok(shape_impl(ShapeKind::Circle, width, height, fill, body))
-}
-
-fn shape_impl(
- shape: ShapeKind,
- mut width: Option<Linear>,
- mut height: Option<Linear>,
- fill: Option<Color>,
- body: Option<Template>,
-) -> Value {
- // Set default shape size if there's no body.
- if body.is_none() {
- let v = Length::pt(30.0).into();
- height.get_or_insert(v);
- width.get_or_insert(match shape {
- ShapeKind::Square | ShapeKind::Circle => v,
- ShapeKind::Rect | ShapeKind::Ellipse => 1.5 * v,
- });
- }
-
- Value::Template(Template::from_inline(move |style| ShapeNode {
- shape,
- width,
- height,
- fill: Some(Paint::Color(
- fill.unwrap_or(Color::Rgba(RgbaColor::new(175, 175, 175, 255))),
- )),
- child: body.as_ref().map(|template| template.to_stack(style).pack()),
- }))
-}
diff --git a/src/library/grid.rs b/src/library/grid.rs
new file mode 100644
index 00000000..af486496
--- /dev/null
+++ b/src/library/grid.rs
@@ -0,0 +1,602 @@
+use super::prelude::*;
+
+/// `grid`: Arrange children into a grid.
+pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ castable! {
+ Vec<TrackSizing>: "integer or (auto, linear, fractional, or array thereof)",
+ Value::Auto => vec![TrackSizing::Auto],
+ Value::Length(v) => vec![TrackSizing::Linear(v.into())],
+ 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::Array(values) => values
+ .into_iter()
+ .filter_map(|v| v.cast().ok())
+ .collect(),
+ }
+
+ castable! {
+ TrackSizing: "auto, linear, or fractional",
+ Value::Auto => Self::Auto,
+ Value::Length(v) => Self::Linear(v.into()),
+ Value::Relative(v) => Self::Linear(v.into()),
+ Value::Linear(v) => Self::Linear(v),
+ Value::Fractional(v) => Self::Fractional(v),
+ }
+
+ let columns = args.named("columns")?.unwrap_or_default();
+ let rows = args.named("rows")?.unwrap_or_default();
+ let tracks = Spec::new(columns, rows);
+
+ 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")?;
+ let gutter = Spec::new(
+ column_gutter.unwrap_or_else(|| base_gutter.clone()),
+ row_gutter.unwrap_or(base_gutter),
+ );
+
+ let children: Vec<Template> = args.all().collect();
+
+ Ok(Value::Template(Template::from_block(move |style| {
+ GridNode {
+ tracks: tracks.clone(),
+ gutter: gutter.clone(),
+ children: children
+ .iter()
+ .map(|child| child.to_stack(&style).pack())
+ .collect(),
+ }
+ })))
+}
+
+/// A node that arranges its children in a grid.
+#[derive(Debug, Hash)]
+pub struct GridNode {
+ /// Defines sizing for content rows and columns.
+ pub tracks: Spec<Vec<TrackSizing>>,
+ /// Defines sizing of gutter rows and columns between content.
+ pub gutter: Spec<Vec<TrackSizing>>,
+ /// The nodes to be arranged in a grid.
+ pub children: Vec<BlockNode>,
+}
+
+/// Defines how to size a grid cell along an axis.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum TrackSizing {
+ /// Fit the cell to its contents.
+ Auto,
+ /// A length stated in absolute values and/or relative to the parent's size.
+ Linear(Linear),
+ /// A length that is the fraction of the remaining free space in the parent.
+ Fractional(Fractional),
+}
+
+impl BlockLevel for GridNode {
+ fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ regions: &Regions,
+ ) -> Vec<Constrained<Rc<Frame>>> {
+ // Prepare grid layout by unifying content and gutter tracks.
+ let mut layouter = GridLayouter::new(self, regions.clone());
+
+ // Determine all column sizes.
+ layouter.measure_columns(ctx);
+
+ // Layout the grid row-by-row.
+ layouter.layout(ctx)
+ }
+}
+
+/// Performs grid layout.
+struct GridLayouter<'a> {
+ /// The original expand state of the target region.
+ expand: Spec<bool>,
+ /// The column tracks including gutter tracks.
+ cols: Vec<TrackSizing>,
+ /// The row tracks including gutter tracks.
+ rows: Vec<TrackSizing>,
+ /// The children of the grid.
+ children: &'a [BlockNode],
+ /// The regions to layout into.
+ regions: Regions,
+ /// Resolved column sizes.
+ rcols: Vec<Length>,
+ /// The full block size of the current region.
+ full: Length,
+ /// The used-up size of the current region. The horizontal size is
+ /// determined once after columns are resolved and not touched again.
+ used: Size,
+ /// The sum of fractional ratios in the current region.
+ fr: Fractional,
+ /// Rows in the current region.
+ lrows: Vec<Row>,
+ /// Constraints for the active region.
+ cts: Constraints,
+ /// Frames for finished regions.
+ finished: Vec<Constrained<Rc<Frame>>>,
+}
+
+/// Produced by initial row layout, auto and linear rows are already finished,
+/// fractional rows not yet.
+enum Row {
+ /// Finished row frame of auto or linear row.
+ Frame(Frame),
+ /// Ratio of a fractional row and y index of the track.
+ Fr(Fractional, usize),
+}
+
+impl<'a> GridLayouter<'a> {
+ /// Prepare grid layout by unifying content and gutter tracks.
+ fn new(grid: &'a GridNode, mut regions: Regions) -> Self {
+ let mut cols = vec![];
+ let mut rows = vec![];
+
+ // Number of content columns: Always at least one.
+ let c = grid.tracks.x.len().max(1);
+
+ // Number of content rows: At least as many as given, but also at least
+ // as many as needed to place each item.
+ let r = {
+ let len = grid.children.len();
+ let given = grid.tracks.y.len();
+ let needed = len / c + (len % c).clamp(0, 1);
+ given.max(needed)
+ };
+
+ let auto = TrackSizing::Auto;
+ let zero = TrackSizing::Linear(Linear::zero());
+ let get_or = |tracks: &[_], idx, default| {
+ tracks.get(idx).or(tracks.last()).copied().unwrap_or(default)
+ };
+
+ // Collect content and gutter columns.
+ for x in 0 .. c {
+ cols.push(get_or(&grid.tracks.x, x, auto));
+ cols.push(get_or(&grid.gutter.x, x, zero));
+ }
+
+ // Collect content and gutter rows.
+ for y in 0 .. r {
+ rows.push(get_or(&grid.tracks.y, y, auto));
+ rows.push(get_or(&grid.gutter.y, y, zero));
+ }
+
+ // Remove superfluous gutter tracks.
+ cols.pop();
+ rows.pop();
+
+ // We use the regions only for auto row measurement and constraints.
+ let expand = regions.expand;
+ regions.expand = Spec::new(true, false);
+
+ Self {
+ children: &grid.children,
+ cts: Constraints::new(expand),
+ full: regions.current.h,
+ expand,
+ rcols: vec![Length::zero(); cols.len()],
+ lrows: vec![],
+ used: Size::zero(),
+ fr: Fractional::zero(),
+ finished: vec![],
+ cols,
+ rows,
+ regions,
+ }
+ }
+
+ /// Determine all column sizes.
+ fn measure_columns(&mut self, ctx: &mut LayoutContext) {
+ enum Case {
+ PurelyLinear,
+ Fitting,
+ Exact,
+ Overflowing,
+ }
+
+ // The different cases affecting constraints.
+ let mut case = Case::PurelyLinear;
+
+ // Sum of sizes of resolved linear tracks.
+ let mut linear = Length::zero();
+
+ // Sum of fractions of all fractional tracks.
+ let mut fr = Fractional::zero();
+
+ // Resolve the size of all linear columns and compute the sum of all
+ // fractional tracks.
+ for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
+ match col {
+ TrackSizing::Auto => {
+ case = Case::Fitting;
+ }
+ TrackSizing::Linear(v) => {
+ let resolved = v.resolve(self.regions.base.w);
+ *rcol = resolved;
+ linear += resolved;
+ }
+ TrackSizing::Fractional(v) => {
+ case = Case::Fitting;
+ fr += v;
+ }
+ }
+ }
+
+ // Size that is not used by fixed-size columns.
+ let available = self.regions.current.w - linear;
+ if available >= Length::zero() {
+ // Determine size of auto columns.
+ let (auto, count) = self.measure_auto_columns(ctx, available);
+
+ // If there is remaining space, distribute it to fractional columns,
+ // otherwise shrink auto columns.
+ let remaining = available - auto;
+ if remaining >= Length::zero() {
+ if !fr.is_zero() {
+ self.grow_fractional_columns(remaining, fr);
+ case = Case::Exact;
+ }
+ } else {
+ self.shrink_auto_columns(available, count);
+ case = Case::Exact;
+ }
+ } else if matches!(case, Case::Fitting) {
+ case = Case::Overflowing;
+ }
+
+ // Children could depend on base.
+ self.cts.base = self.regions.base.to_spec().map(Some);
+
+ // Set constraints depending on the case we hit.
+ match case {
+ Case::PurelyLinear => {}
+ Case::Fitting => self.cts.min.x = Some(self.used.w),
+ Case::Exact => self.cts.exact.x = Some(self.regions.current.w),
+ Case::Overflowing => self.cts.max.x = Some(linear),
+ }
+
+ // Sum up the resolved column sizes once here.
+ self.used.w = self.rcols.iter().sum();
+ }
+
+ /// Measure the size that is available to auto columns.
+ fn measure_auto_columns(
+ &mut self,
+ ctx: &mut LayoutContext,
+ available: Length,
+ ) -> (Length, usize) {
+ let mut auto = Length::zero();
+ let mut count = 0;
+
+ // Determine size of auto columns by laying out all cells in those
+ // columns, measuring them and finding the largest one.
+ for (x, &col) in self.cols.iter().enumerate() {
+ if col != TrackSizing::Auto {
+ continue;
+ }
+
+ let mut resolved = Length::zero();
+ for y in 0 .. self.rows.len() {
+ if let Some(node) = self.cell(x, y) {
+ let size = Size::new(available, Length::inf());
+ let mut regions =
+ Regions::one(size, self.regions.base, Spec::splat(false));
+
+ // For fractional rows, we can already resolve the correct
+ // base, for auto it's already correct and for fr we could
+ // only guess anyway.
+ if let TrackSizing::Linear(v) = self.rows[y] {
+ regions.base.h = v.resolve(self.regions.base.h);
+ }
+
+ let frame = node.layout(ctx, &regions).remove(0).item;
+ resolved.set_max(frame.size.w);
+ }
+ }
+
+ self.rcols[x] = resolved;
+ auto += resolved;
+ count += 1;
+ }
+
+ (auto, count)
+ }
+
+ /// Distribute remaining space to fractional columns.
+ fn grow_fractional_columns(&mut self, remaining: Length, fr: Fractional) {
+ for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
+ if let TrackSizing::Fractional(v) = col {
+ let ratio = v / fr;
+ if ratio.is_finite() {
+ *rcol = ratio * remaining;
+ }
+ }
+ }
+ }
+
+ /// Redistribute space to auto columns so that each gets a fair share.
+ fn shrink_auto_columns(&mut self, available: Length, count: usize) {
+ // The fair share each auto column may have.
+ let fair = available / count as f64;
+
+ // The number of overlarge auto columns and the space that will be
+ // equally redistributed to them.
+ let mut overlarge: usize = 0;
+ let mut redistribute = available;
+
+ // Find out the number of and space used by overlarge auto columns.
+ for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
+ if col == TrackSizing::Auto {
+ if *rcol > fair {
+ overlarge += 1;
+ } else {
+ redistribute -= *rcol;
+ }
+ }
+ }
+
+ // Redistribute the space equally.
+ let share = redistribute / overlarge as f64;
+ for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
+ if col == TrackSizing::Auto && *rcol > fair {
+ *rcol = share;
+ }
+ }
+ }
+
+ /// Layout the grid row-by-row.
+ fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> {
+ for y in 0 .. self.rows.len() {
+ match self.rows[y] {
+ TrackSizing::Auto => self.layout_auto_row(ctx, y),
+ TrackSizing::Linear(v) => self.layout_linear_row(ctx, v, y),
+ TrackSizing::Fractional(v) => {
+ self.cts.exact.y = Some(self.full);
+ self.lrows.push(Row::Fr(v, y));
+ self.fr += v;
+ }
+ }
+ }
+
+ self.finish_region(ctx);
+ self.finished
+ }
+
+ /// Layout a row with automatic size along the block axis. Such a row may
+ /// break across multiple regions.
+ fn layout_auto_row(&mut self, ctx: &mut LayoutContext, y: usize) {
+ let mut resolved: Vec<Length> = vec![];
+
+ // Determine the size for each region of the row.
+ for (x, &rcol) in self.rcols.iter().enumerate() {
+ if let Some(node) = self.cell(x, y) {
+ let mut regions = self.regions.clone();
+ regions.mutate(|size| size.w = rcol);
+
+ // Set the horizontal base back to the parent region's base for
+ // auto columns.
+ if self.cols[x] == TrackSizing::Auto {
+ regions.base.w = self.regions.base.w;
+ }
+
+ let mut sizes =
+ node.layout(ctx, &regions).into_iter().map(|frame| frame.item.size.h);
+
+ for (target, size) in resolved.iter_mut().zip(&mut sizes) {
+ target.set_max(size);
+ }
+
+ resolved.extend(sizes);
+ }
+ }
+
+ // Nothing to layout.
+ if resolved.is_empty() {
+ return;
+ }
+
+ // Layout into a single region.
+ if let &[first] = resolved.as_slice() {
+ let frame = self.layout_single_row(ctx, first, y);
+ self.push_row(frame);
+ return;
+ }
+
+ // Expand all but the last region if the space is not
+ // eaten up by any fr rows.
+ if self.fr.is_zero() {
+ let len = resolved.len();
+ for (target, (current, _)) in
+ resolved[.. len - 1].iter_mut().zip(self.regions.iter())
+ {
+ target.set_max(current.h);
+ }
+ }
+
+ // Layout into multiple regions.
+ let frames = self.layout_multi_row(ctx, &resolved, y);
+ let len = frames.len();
+ for (i, frame) in frames.into_iter().enumerate() {
+ self.push_row(frame);
+ if i + 1 < len {
+ self.cts.exact.y = Some(self.full);
+ self.finish_region(ctx);
+ }
+ }
+ }
+
+ /// Layout a row with linear sizing along the block axis. Such a row cannot
+ /// break across multiple regions, but it may force a region break.
+ fn layout_linear_row(&mut self, ctx: &mut LayoutContext, v: Linear, y: usize) {
+ let resolved = v.resolve(self.regions.base.h);
+ let frame = self.layout_single_row(ctx, resolved, y);
+
+ // Skip to fitting region.
+ let length = frame.size.h;
+ while !self.regions.current.h.fits(length) && !self.regions.in_full_last() {
+ self.cts.max.y = Some(self.used.h + length);
+ self.finish_region(ctx);
+
+ // Don't skip multiple regions for gutter and don't push a row.
+ if y % 2 == 1 {
+ return;
+ }
+ }
+
+ self.push_row(frame);
+ }
+
+ /// Layout a row with a fixed size along the block axis and return its frame.
+ fn layout_single_row(
+ &self,
+ ctx: &mut LayoutContext,
+ height: Length,
+ y: usize,
+ ) -> Frame {
+ let mut output = Frame::new(Size::new(self.used.w, height), height);
+ let mut pos = Point::zero();
+
+ for (x, &rcol) in self.rcols.iter().enumerate() {
+ if let Some(node) = self.cell(x, y) {
+ let size = Size::new(rcol, height);
+
+ // Set the base to the size for non-auto rows.
+ let mut base = self.regions.base;
+ if self.cols[x] != TrackSizing::Auto {
+ base.w = size.w;
+ }
+ if self.rows[y] != TrackSizing::Auto {
+ base.h = size.h;
+ }
+
+ let regions = Regions::one(size, base, Spec::splat(true));
+ let frame = node.layout(ctx, &regions).remove(0);
+ output.push_frame(pos, frame.item);
+ }
+
+ pos.x += rcol;
+ }
+
+ output
+ }
+
+ /// Layout a row spanning multiple regions.
+ fn layout_multi_row(
+ &self,
+ ctx: &mut LayoutContext,
+ resolved: &[Length],
+ y: usize,
+ ) -> Vec<Frame> {
+ // Prepare frames.
+ let mut outputs: Vec<_> = resolved
+ .iter()
+ .map(|&h| Frame::new(Size::new(self.used.w, h), h))
+ .collect();
+
+ // Prepare regions.
+ let size = Size::new(self.used.w, resolved[0]);
+ let mut regions = Regions::one(size, self.regions.base, Spec::splat(true));
+ regions.backlog = resolved[1 ..]
+ .iter()
+ .map(|&h| Size::new(self.used.w, h))
+ .collect::<Vec<_>>()
+ .into_iter();
+
+ // Layout the row.
+ let mut pos = Point::zero();
+ for (x, &rcol) in self.rcols.iter().enumerate() {
+ if let Some(node) = self.cell(x, y) {
+ regions.mutate(|size| size.w = rcol);
+
+ // Set the horizontal base back to the parent region's base for
+ // auto columns.
+ if self.cols[x] == TrackSizing::Auto {
+ regions.base.w = self.regions.base.w;
+ }
+
+ // Push the layouted frames into the individual output frames.
+ let frames = node.layout(ctx, &regions);
+ for (output, frame) in outputs.iter_mut().zip(frames) {
+ output.push_frame(pos, frame.item);
+ }
+ }
+
+ pos.x += rcol;
+ }
+
+ outputs
+ }
+
+ /// Push a row frame into the current region.
+ fn push_row(&mut self, frame: Frame) {
+ self.regions.current.h -= frame.size.h;
+ self.used.h += frame.size.h;
+ self.lrows.push(Row::Frame(frame));
+ }
+
+ /// Finish rows for one region.
+ fn finish_region(&mut self, ctx: &mut LayoutContext) {
+ // Determine the size that remains for fractional rows.
+ let remaining = self.full - self.used.h;
+
+ // Determine the size of the grid in this region, expanding fully if
+ // there are fr rows.
+ let mut size = self.used;
+ if !self.fr.is_zero() && self.full.is_finite() {
+ size.h = self.full;
+ }
+
+ self.cts.min.y = Some(size.h);
+
+ // The frame for the region.
+ let mut output = Frame::new(size, size.h);
+ let mut pos = Point::zero();
+
+ // Place finished rows and layout fractional rows.
+ for row in std::mem::take(&mut self.lrows) {
+ let frame = match row {
+ Row::Frame(frame) => frame,
+ Row::Fr(v, y) => {
+ let ratio = v / self.fr;
+ if remaining.is_finite() && ratio.is_finite() {
+ let resolved = ratio * remaining;
+ self.layout_single_row(ctx, resolved, y)
+ } else {
+ continue;
+ }
+ }
+ };
+
+ let height = frame.size.h;
+ output.merge_frame(pos, frame);
+ pos.y += height;
+ }
+
+ self.regions.next();
+ self.full = self.regions.current.h;
+ self.used.h = Length::zero();
+ self.fr = Fractional::zero();
+ self.finished.push(output.constrain(self.cts));
+ self.cts = Constraints::new(self.expand);
+ }
+
+ /// Get the node in the cell in column `x` and row `y`.
+ ///
+ /// Returns `None` if it's a gutter cell.
+ #[track_caller]
+ fn cell(&self, x: usize, y: usize) -> Option<&'a BlockNode> {
+ assert!(x < self.cols.len());
+ assert!(y < self.rows.len());
+
+ // Even columns and rows are children, odd ones are gutter.
+ if x % 2 == 0 && y % 2 == 0 {
+ let c = 1 + self.cols.len() / 2;
+ self.children.get((y / 2) * c + x / 2)
+ } else {
+ None
+ }
+ }
+}
diff --git a/src/library/image.rs b/src/library/image.rs
new file mode 100644
index 00000000..c2273502
--- /dev/null
+++ b/src/library/image.rs
@@ -0,0 +1,68 @@
+use std::io;
+
+use super::prelude::*;
+use crate::diag::Error;
+use crate::image::ImageId;
+
+/// `image`: An image.
+pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let path = args.expect::<Spanned<Str>>("path to image file")?;
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+
+ 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),
+ })
+ })?;
+
+ Ok(Value::Template(Template::from_inline(move |_| ImageNode {
+ id,
+ width,
+ height,
+ })))
+}
+
+/// An image node.
+#[derive(Debug, Hash)]
+pub struct ImageNode {
+ /// The id of the image file.
+ pub id: ImageId,
+ /// The fixed width, if any.
+ pub width: Option<Linear>,
+ /// The fixed height, if any.
+ pub height: Option<Linear>,
+}
+
+impl InlineLevel for ImageNode {
+ fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame {
+ let img = ctx.images.get(self.id);
+ let pixel_size = Spec::new(img.width() as f64, img.height() as f64);
+ let pixel_ratio = pixel_size.x / pixel_size.y;
+
+ let width = self.width.map(|w| w.resolve(base.w));
+ let height = self.height.map(|w| w.resolve(base.h));
+
+ let size = match (width, height) {
+ (Some(width), Some(height)) => Size::new(width, height),
+ (Some(width), None) => Size::new(width, width / pixel_ratio),
+ (None, Some(height)) => Size::new(height * pixel_ratio, height),
+ (None, None) => {
+ if space.is_finite() {
+ // Fit to width.
+ Size::new(space, space / pixel_ratio)
+ } else {
+ // Unbounded width, we have to make up something,
+ // so it is 1pt per pixel.
+ pixel_size.map(Length::pt).to_size()
+ }
+ }
+ };
+
+ let mut frame = Frame::new(size, size.h);
+ frame.push(Point::zero(), Element::Image(self.id, size));
+ frame
+ }
+}
diff --git a/src/library/layout.rs b/src/library/layout.rs
deleted file mode 100644
index e5650664..00000000
--- a/src/library/layout.rs
+++ /dev/null
@@ -1,332 +0,0 @@
-use super::*;
-use crate::layout::{
- GridNode, PadNode, ShapeKind, ShapeNode, StackChild, StackNode, TrackSizing,
-};
-use crate::style::{Paper, PaperClass};
-
-/// `page`: Configure pages.
-pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let paper = match args.named::<Spanned<Str>>("paper")?.or_else(|| args.find()) {
- Some(name) => match Paper::from_name(&name.v) {
- None => bail!(name.span, "invalid paper name"),
- paper => paper,
- },
- None => None,
- };
-
- let width = args.named("width")?;
- let height = args.named("height")?;
- let margins = args.named("margins")?;
- let left = args.named("left")?;
- let top = args.named("top")?;
- let right = args.named("right")?;
- let bottom = args.named("bottom")?;
- let flip = args.named("flip")?;
-
- ctx.template.modify(move |style| {
- let page = style.page_mut();
-
- if let Some(paper) = paper {
- page.class = paper.class();
- page.size = paper.size();
- }
-
- if let Some(width) = width {
- page.class = PaperClass::Custom;
- page.size.w = width;
- }
-
- if let Some(height) = height {
- page.class = PaperClass::Custom;
- page.size.h = height;
- }
-
- if let Some(margins) = margins {
- page.margins = Sides::splat(Some(margins));
- }
-
- if let Some(left) = left {
- page.margins.left = Some(left);
- }
-
- if let Some(top) = top {
- page.margins.top = Some(top);
- }
-
- if let Some(right) = right {
- page.margins.right = Some(right);
- }
-
- if let Some(bottom) = bottom {
- page.margins.bottom = Some(bottom);
- }
-
- if flip.unwrap_or(false) {
- std::mem::swap(&mut page.size.w, &mut page.size.h);
- }
- });
-
- ctx.template.pagebreak(false);
-
- Ok(Value::None)
-}
-
-/// `pagebreak`: Start a new page.
-pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.pagebreak(true);
- Ok(Value::Template(template))
-}
-
-/// `align`: Configure the alignment along the layouting axes.
-pub fn align(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let first = args.find::<Align>();
- let second = args.find::<Align>();
- let body = args.find::<Template>();
-
- let mut horizontal = args.named("horizontal")?;
- let mut vertical = args.named("vertical")?;
-
- for value in first.into_iter().chain(second) {
- match value.axis() {
- Some(SpecAxis::Horizontal) | None if horizontal.is_none() => {
- horizontal = Some(value);
- }
- Some(SpecAxis::Vertical) | None if vertical.is_none() => {
- vertical = Some(value);
- }
- _ => {}
- }
- }
-
- let realign = |template: &mut Template| {
- template.modify(move |style| {
- if let Some(horizontal) = horizontal {
- style.aligns.inline = horizontal;
- }
-
- if let Some(vertical) = vertical {
- style.aligns.block = vertical;
- }
- });
-
- if vertical.is_some() {
- template.parbreak();
- }
- };
-
- Ok(if let Some(body) = body {
- let mut template = Template::new();
- template.save();
- realign(&mut template);
- template += body;
- template.restore();
- Value::Template(template)
- } else {
- realign(&mut ctx.template);
- Value::None
- })
-}
-
-/// `h`: Horizontal spacing.
-pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.spacing(GenAxis::Inline, args.expect("spacing")?);
- Ok(Value::Template(template))
-}
-
-/// `v`: Vertical spacing.
-pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.spacing(GenAxis::Block, args.expect("spacing")?);
- Ok(Value::Template(template))
-}
-
-/// `box`: Place content in a rectangular box.
-pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let width = args.named("width")?;
- let height = args.named("height")?;
- let fill = args.named("fill")?;
- let body: Template = args.find().unwrap_or_default();
- Ok(Value::Template(Template::from_inline(move |style| {
- ShapeNode {
- shape: ShapeKind::Rect,
- width,
- height,
- fill: fill.map(Paint::Color),
- child: Some(body.to_stack(style).pack()),
- }
- })))
-}
-
-/// `block`: Place content in a block.
-pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let body: Template = args.expect("body")?;
- Ok(Value::Template(Template::from_block(move |style| {
- body.to_stack(style)
- })))
-}
-
-/// `pad`: 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: Template = 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::Template(Template::from_block(move |style| {
- PadNode {
- padding,
- child: body.to_stack(&style).pack(),
- }
- })))
-}
-
-/// `move`: Move content without affecting layout.
-pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- #[derive(Debug, Hash)]
- struct MoveNode {
- offset: Spec<Option<Linear>>,
- child: ShapeNode,
- }
-
- impl InlineLevel for MoveNode {
- fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame {
- let offset = Point::new(
- self.offset.x.map(|x| x.resolve(base.w)).unwrap_or_default(),
- self.offset.y.map(|y| y.resolve(base.h)).unwrap_or_default(),
- );
-
- let mut frame = self.child.layout(ctx, space, base);
- for (point, _) in &mut frame.children {
- *point += offset;
- }
-
- frame
- }
- }
-
- let x = args.named("x")?;
- let y = args.named("y")?;
- let body: Template = args.expect("body")?;
-
- Ok(Value::Template(Template::from_inline(move |style| {
- MoveNode {
- offset: Spec::new(x, y),
- child: ShapeNode {
- shape: ShapeKind::Rect,
- width: None,
- height: None,
- fill: None,
- child: Some(body.to_stack(style).pack()),
- },
- }
- })))
-}
-
-/// `stack`: Stack children along an axis.
-pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- enum Child {
- Spacing(Spacing),
- Any(Template),
- }
-
- castable! {
- Child: "linear, fractional or template",
- Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Linear(v) => Self::Spacing(Spacing::Linear(v)),
- Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)),
- Value::Template(v) => Self::Any(v),
- }
-
- let dir = args.named("dir")?.unwrap_or(Dir::TTB);
- let spacing = args.named("spacing")?;
- let list: Vec<Child> = args.all().collect();
-
- Ok(Value::Template(Template::from_block(move |style| {
- let mut children = vec![];
- let mut delayed = None;
-
- // Build the list of stack children.
- for child in &list {
- match child {
- Child::Spacing(v) => {
- children.push(StackChild::Spacing(*v));
- delayed = None;
- }
- Child::Any(template) => {
- if let Some(v) = delayed {
- children.push(StackChild::Spacing(v));
- }
-
- let node = template.to_stack(style).pack();
- children.push(StackChild::Node(node, style.aligns.block));
- delayed = spacing;
- }
- }
- }
-
- StackNode { dir, children }
- })))
-}
-
-/// `grid`: Arrange children into a grid.
-pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- castable! {
- Vec<TrackSizing>: "integer or (auto, linear, fractional, or array thereof)",
- Value::Auto => vec![TrackSizing::Auto],
- Value::Length(v) => vec![TrackSizing::Linear(v.into())],
- 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::Array(values) => values
- .into_iter()
- .filter_map(|v| v.cast().ok())
- .collect(),
- }
-
- castable! {
- TrackSizing: "auto, linear, or fractional",
- Value::Auto => Self::Auto,
- Value::Length(v) => Self::Linear(v.into()),
- Value::Relative(v) => Self::Linear(v.into()),
- Value::Linear(v) => Self::Linear(v),
- Value::Fractional(v) => Self::Fractional(v),
- }
-
- let columns = args.named("columns")?.unwrap_or_default();
- let rows = args.named("rows")?.unwrap_or_default();
- let tracks = Spec::new(columns, rows);
-
- 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")?;
- let gutter = Spec::new(
- column_gutter.unwrap_or_else(|| base_gutter.clone()),
- row_gutter.unwrap_or(base_gutter),
- );
-
- let children: Vec<Template> = args.all().collect();
-
- Ok(Value::Template(Template::from_block(move |style| {
- GridNode {
- tracks: tracks.clone(),
- gutter: gutter.clone(),
- children: children
- .iter()
- .map(|child| child.to_stack(&style).pack())
- .collect(),
- }
- })))
-}
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 6d3de11d..9a1dc2e9 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -3,26 +3,54 @@
//! Call [`new`] to obtain a [`Scope`] containing all standard library
//! definitions.
-mod elements;
-mod layout;
+mod align;
+mod container;
+mod deco;
+mod grid;
+mod image;
+mod pad;
+mod page;
+mod par;
+mod shape;
+mod spacing;
+mod stack;
mod text;
+mod transform;
mod utility;
-pub use elements::*;
-pub use layout::*;
+/// Helpful imports for creating library functionality.
+mod prelude {
+ pub use std::rc::Rc;
+
+ pub use crate::diag::{At, TypResult};
+ pub use crate::eval::{Args, EvalContext, Str, Template, Value};
+ pub use crate::frame::*;
+ pub use crate::geom::*;
+ pub use crate::layout::*;
+ pub use crate::syntax::{Span, Spanned};
+ pub use crate::util::OptionExt;
+}
+
+pub use self::image::*;
+pub use align::*;
+pub use container::*;
+pub use deco::*;
+pub use grid::*;
+pub use pad::*;
+pub use page::*;
+pub use par::*;
+pub use shape::*;
+pub use spacing::*;
+pub use stack::*;
pub use text::*;
+pub use transform::*;
pub use utility::*;
use std::convert::TryFrom;
-use std::rc::Rc;
-use crate::diag::{At, TypResult};
-use crate::eval::{Args, Array, EvalContext, Scope, Str, Template, Value};
+use crate::eval::{Scope, Value};
use crate::font::{FontFamily, FontStretch, FontStyle, FontWeight, VerticalFontMetric};
use crate::geom::*;
-use crate::layout::{BlockLevel, Frame, InlineLevel, LayoutContext, Spacing};
-use crate::style::Style;
-use crate::syntax::{Span, Spanned};
/// Construct a scope containing all standard library definitions.
pub fn new() -> Scope {
diff --git a/src/library/pad.rs b/src/library/pad.rs
new file mode 100644
index 00000000..6457f603
--- /dev/null
+++ b/src/library/pad.rs
@@ -0,0 +1,101 @@
+use super::prelude::*;
+
+/// `pad`: 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: Template = 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::Template(Template::from_block(move |style| {
+ PadNode {
+ padding,
+ child: body.to_stack(&style).pack(),
+ }
+ })))
+}
+
+/// A node that adds padding to its child.
+#[derive(Debug, Hash)]
+pub struct PadNode {
+ /// The amount of padding.
+ pub padding: Sides<Linear>,
+ /// The child node whose sides to pad.
+ pub child: BlockNode,
+}
+
+impl BlockLevel for PadNode {
+ fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ regions: &Regions,
+ ) -> Vec<Constrained<Rc<Frame>>> {
+ // Layout child into padded regions.
+ let mut frames = self.child.layout(
+ ctx,
+ &regions.map(|size| size - self.padding.resolve(size).size()),
+ );
+
+ for (Constrained { item: frame, cts }, (current, base)) in
+ frames.iter_mut().zip(regions.iter())
+ {
+ fn solve_axis(length: Length, padding: Linear) -> Length {
+ (length + padding.abs)
+ .div_finite(1.0 - padding.rel.get())
+ .unwrap_or_default()
+ }
+
+ // Solve for the size `padded` that satisfies (approximately):
+ // `padded - padding.resolve(padded).size() == size`
+ let padded = Size::new(
+ solve_axis(frame.size.w, self.padding.left + self.padding.right),
+ solve_axis(frame.size.h, self.padding.top + self.padding.bottom),
+ );
+
+ let padding = self.padding.resolve(padded);
+ let origin = Point::new(padding.left, padding.top);
+
+ // Create a new larger frame and place the child's frame inside it.
+ let empty = Frame::new(padded, frame.baseline + origin.y);
+ let prev = std::mem::replace(frame, Rc::new(empty));
+ let new = Rc::make_mut(frame);
+ new.push_frame(origin, prev);
+
+ // Inflate min and max contraints by the padding.
+ for spec in [&mut cts.min, &mut cts.max] {
+ if let Some(x) = spec.x.as_mut() {
+ *x += padding.size().w;
+ }
+ if let Some(y) = spec.y.as_mut() {
+ *y += padding.size().h;
+ }
+ }
+
+ // Set exact and base constraints if the child had them.
+ cts.exact.x.and_set(Some(current.w));
+ cts.exact.y.and_set(Some(current.h));
+ cts.base.x.and_set(Some(base.w));
+ cts.base.y.and_set(Some(base.h));
+
+ // Also set base constraints if the padding is relative.
+ if self.padding.left.is_relative() || self.padding.right.is_relative() {
+ cts.base.x = Some(base.w);
+ }
+
+ if self.padding.top.is_relative() || self.padding.bottom.is_relative() {
+ cts.base.y = Some(base.h);
+ }
+ }
+
+ frames
+ }
+}
diff --git a/src/library/page.rs b/src/library/page.rs
new file mode 100644
index 00000000..7e1bd05b
--- /dev/null
+++ b/src/library/page.rs
@@ -0,0 +1,76 @@
+use super::prelude::*;
+use crate::style::{Paper, PaperClass};
+
+/// `page`: Configure pages.
+pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let paper = match args.named::<Spanned<Str>>("paper")?.or_else(|| args.find()) {
+ Some(name) => match Paper::from_name(&name.v) {
+ None => bail!(name.span, "invalid paper name"),
+ paper => paper,
+ },
+ None => None,
+ };
+
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+ let margins = args.named("margins")?;
+ let left = args.named("left")?;
+ let top = args.named("top")?;
+ let right = args.named("right")?;
+ let bottom = args.named("bottom")?;
+ let flip = args.named("flip")?;
+
+ ctx.template.modify(move |style| {
+ let page = style.page_mut();
+
+ if let Some(paper) = paper {
+ page.class = paper.class();
+ page.size = paper.size();
+ }
+
+ if let Some(width) = width {
+ page.class = PaperClass::Custom;
+ page.size.w = width;
+ }
+
+ if let Some(height) = height {
+ page.class = PaperClass::Custom;
+ page.size.h = height;
+ }
+
+ if let Some(margins) = margins {
+ page.margins = Sides::splat(Some(margins));
+ }
+
+ if let Some(left) = left {
+ page.margins.left = Some(left);
+ }
+
+ if let Some(top) = top {
+ page.margins.top = Some(top);
+ }
+
+ if let Some(right) = right {
+ page.margins.right = Some(right);
+ }
+
+ if let Some(bottom) = bottom {
+ page.margins.bottom = Some(bottom);
+ }
+
+ if flip.unwrap_or(false) {
+ std::mem::swap(&mut page.size.w, &mut page.size.h);
+ }
+ });
+
+ ctx.template.pagebreak(false);
+
+ Ok(Value::None)
+}
+
+/// `pagebreak`: Start a new page.
+pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
+ let mut template = Template::new();
+ template.pagebreak(true);
+ Ok(Value::Template(template))
+}
diff --git a/src/library/par.rs b/src/library/par.rs
new file mode 100644
index 00000000..3330eedf
--- /dev/null
+++ b/src/library/par.rs
@@ -0,0 +1,674 @@
+use std::fmt::{self, Debug, Formatter};
+use std::rc::Rc;
+
+use itertools::Either;
+use unicode_bidi::{BidiInfo, Level};
+use xi_unicode::LineBreakIterator;
+
+use super::prelude::*;
+use super::{shape, Decoration, ShapedText, Spacing};
+use crate::style::TextStyle;
+use crate::util::{EcoString, RangeExt, SliceExt};
+
+/// `par`: Configure paragraphs.
+pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let spacing = args.named("spacing")?;
+ let leading = args.named("leading")?;
+
+ ctx.template.modify(move |style| {
+ let par = style.par_mut();
+
+ if let Some(spacing) = spacing {
+ par.spacing = spacing;
+ }
+
+ if let Some(leading) = leading {
+ par.leading = leading;
+ }
+ });
+
+ ctx.template.parbreak();
+
+ Ok(Value::None)
+}
+
+/// `lang`: Configure the language.
+pub fn lang(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let iso = args.find::<Str>();
+ let dir = if let Some(dir) = args.named::<Spanned<Dir>>("dir")? {
+ if dir.v.axis() == SpecAxis::Horizontal {
+ Some(dir.v)
+ } else {
+ bail!(dir.span, "must be horizontal");
+ }
+ } else {
+ iso.as_deref().map(lang_dir)
+ };
+
+ if let Some(dir) = dir {
+ ctx.template.modify(move |style| style.dir = dir);
+ }
+
+ ctx.template.parbreak();
+
+ Ok(Value::None)
+}
+
+/// The default direction for the language identified by the given `iso` code.
+fn lang_dir(iso: &str) -> Dir {
+ match iso.to_ascii_lowercase().as_str() {
+ "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
+ "en" | "fr" | "de" => Dir::LTR,
+ _ => Dir::LTR,
+ }
+}
+
+/// A node that arranges its children into a paragraph.
+#[derive(Debug, Hash)]
+pub struct ParNode {
+ /// The inline direction of this paragraph.
+ pub dir: Dir,
+ /// The spacing to insert between each line.
+ pub leading: Length,
+ /// The children to be arranged in a paragraph.
+ pub children: Vec<ParChild>,
+}
+
+/// A child of a paragraph node.
+#[derive(Hash)]
+pub enum ParChild {
+ /// Spacing between other nodes.
+ Spacing(Spacing),
+ /// A run of text and how to align it in its line.
+ Text(EcoString, Align, Rc<TextStyle>),
+ /// Any child node and how to align it in its line.
+ Node(InlineNode, Align),
+ /// A decoration that applies until a matching `Undecorate`.
+ Decorate(Decoration),
+ /// The end of a decoration.
+ Undecorate,
+}
+
+impl BlockLevel for ParNode {
+ fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ regions: &Regions,
+ ) -> Vec<Constrained<Rc<Frame>>> {
+ // Collect all text into one string used for BiDi analysis.
+ let text = self.collect_text();
+
+ // Find out the BiDi embedding levels.
+ let bidi = BidiInfo::new(&text, Level::from_dir(self.dir));
+
+ // Prepare paragraph layout by building a representation on which we can
+ // do line breaking without layouting each and every line from scratch.
+ let layouter = ParLayouter::new(self, ctx, regions, bidi);
+
+ // Find suitable linebreaks.
+ layouter.layout(ctx, regions.clone())
+ }
+}
+
+impl ParNode {
+ /// Concatenate all text in the paragraph into one string, replacing spacing
+ /// with a space character and other non-text nodes with the object
+ /// replacement character. Returns the full text alongside the range each
+ /// child spans in the text.
+ fn collect_text(&self) -> String {
+ let mut text = String::new();
+ for string in self.strings() {
+ text.push_str(string);
+ }
+ text
+ }
+
+ /// The range of each item in the collected text.
+ fn ranges(&self) -> impl Iterator<Item = Range> + '_ {
+ let mut cursor = 0;
+ self.strings().map(move |string| {
+ let start = cursor;
+ cursor += string.len();
+ start .. cursor
+ })
+ }
+
+ /// The string representation of each child.
+ fn strings(&self) -> impl Iterator<Item = &str> {
+ self.children.iter().map(|child| match child {
+ ParChild::Spacing(_) => " ",
+ ParChild::Text(ref piece, ..) => piece,
+ ParChild::Node(..) => "\u{FFFC}",
+ ParChild::Decorate(_) | ParChild::Undecorate => "",
+ })
+ }
+}
+
+impl Debug for ParChild {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Spacing(v) => write!(f, "Spacing({:?})", v),
+ Self::Text(text, ..) => write!(f, "Text({:?})", text),
+ Self::Node(node, ..) => node.fmt(f),
+ Self::Decorate(deco) => write!(f, "Decorate({:?})", deco),
+ Self::Undecorate => write!(f, "Undecorate"),
+ }
+ }
+}
+
+type Range = std::ops::Range<usize>;
+
+/// A paragraph representation in which children are already layouted and text
+/// is separated into shapable runs.
+struct ParLayouter<'a> {
+ /// The top-level direction.
+ dir: Dir,
+ /// The line spacing.
+ leading: Length,
+ /// Bidirectional text embedding levels for the paragraph.
+ bidi: BidiInfo<'a>,
+ /// Spacing, separated text runs and layouted nodes.
+ items: Vec<ParItem<'a>>,
+ /// The ranges of the items in `bidi.text`.
+ ranges: Vec<Range>,
+ /// The decorations and the ranges they span.
+ decos: Vec<(Range, &'a Decoration)>,
+}
+
+/// A prepared item in a paragraph layout.
+enum ParItem<'a> {
+ /// Absolute spacing between other items.
+ Absolute(Length),
+ /// Fractional spacing between other items.
+ Fractional(Fractional),
+ /// A shaped text run with consistent direction.
+ Text(ShapedText<'a>, Align),
+ /// A layouted child node.
+ Frame(Frame, Align),
+}
+
+impl<'a> ParLayouter<'a> {
+ /// Prepare initial shaped text and layouted children.
+ fn new(
+ par: &'a ParNode,
+ ctx: &mut LayoutContext,
+ regions: &Regions,
+ bidi: BidiInfo<'a>,
+ ) -> Self {
+ let mut items = vec![];
+ let mut ranges = vec![];
+ let mut starts = vec![];
+ let mut decos = vec![];
+
+ // Layout the children and collect them into items.
+ for (range, child) in par.ranges().zip(&par.children) {
+ match *child {
+ ParChild::Spacing(Spacing::Linear(v)) => {
+ let resolved = v.resolve(regions.current.w);
+ items.push(ParItem::Absolute(resolved));
+ ranges.push(range);
+ }
+ ParChild::Spacing(Spacing::Fractional(v)) => {
+ items.push(ParItem::Fractional(v));
+ ranges.push(range);
+ }
+ ParChild::Text(_, align, ref style) => {
+ // TODO: Also split by language and script.
+ let mut cursor = range.start;
+ for (level, group) in bidi.levels[range].group_by_key(|&lvl| lvl) {
+ let start = cursor;
+ cursor += group.len();
+ let subrange = start .. cursor;
+ let text = &bidi.text[subrange.clone()];
+ let shaped = shape(ctx, text, style, level.dir());
+ items.push(ParItem::Text(shaped, align));
+ ranges.push(subrange);
+ }
+ }
+ ParChild::Node(ref node, align) => {
+ let frame = node.layout(ctx, regions.current.w, regions.base);
+ items.push(ParItem::Frame(frame, align));
+ ranges.push(range);
+ }
+ ParChild::Decorate(ref deco) => {
+ starts.push((range.start, deco));
+ }
+ ParChild::Undecorate => {
+ let (start, deco) = starts.pop().unwrap();
+ decos.push((start .. range.end, deco));
+ }
+ }
+ }
+
+ Self {
+ dir: par.dir,
+ leading: par.leading,
+ bidi,
+ items,
+ ranges,
+ decos,
+ }
+ }
+
+ /// Find first-fit line breaks and build the paragraph.
+ fn layout(
+ self,
+ ctx: &mut LayoutContext,
+ regions: Regions,
+ ) -> Vec<Constrained<Rc<Frame>>> {
+ let mut stack = LineStack::new(self.leading, regions);
+
+ // The current line attempt.
+ // Invariant: Always fits into `stack.regions.current`.
+ let mut last = None;
+
+ // The start of the line in `last`.
+ let mut start = 0;
+
+ // Find suitable line breaks.
+ // TODO: Provide line break opportunities on alignment changes.
+ for (end, mandatory) in LineBreakIterator::new(self.bidi.text) {
+ // Compute the line and its size.
+ let mut line = LineLayout::new(ctx, &self, start .. end);
+
+ // If the line doesn't fit anymore, we push the last fitting attempt
+ // into the stack and rebuild the line from its end. The resulting
+ // line cannot be broken up further.
+ if !stack.regions.current.fits(line.size) {
+ if let Some((last_line, last_end)) = last.take() {
+ // Since the new line try did not fit, no region that would
+ // fit the line will yield the same line break. Therefore,
+ // the width of the region must not fit the width of the
+ // tried line.
+ if !stack.regions.current.w.fits(line.size.w) {
+ stack.cts.max.x.set_min(line.size.w);
+ }
+
+ // Same as above, but for height.
+ if !stack.regions.current.h.fits(line.size.h) {
+ let too_large = stack.size.h + self.leading + line.size.h;
+ stack.cts.max.y.set_min(too_large);
+ }
+
+ stack.push(last_line);
+
+ stack.cts.min.y = Some(stack.size.h);
+ start = last_end;
+ line = LineLayout::new(ctx, &self, start .. end);
+ }
+ }
+
+ // If the line does not fit vertically, we start a new region.
+ while !stack.regions.current.h.fits(line.size.h) {
+ if stack.regions.in_full_last() {
+ stack.overflowing = true;
+ break;
+ }
+
+ // Again, the line must not fit. It would if the space taken up
+ // plus the line height would fit, therefore the constraint
+ // below.
+ let too_large = stack.size.h + self.leading + line.size.h;
+ stack.cts.max.y.set_min(too_large);
+
+ stack.finish_region(ctx);
+ }
+
+ // If the line does not fit horizontally or we have a mandatory
+ // line break (i.e. due to "\n"), we push the line into the
+ // stack.
+ if mandatory || !stack.regions.current.w.fits(line.size.w) {
+ start = end;
+ last = None;
+
+ stack.push(line);
+
+ // If there is a trailing line break at the end of the
+ // paragraph, we want to force an empty line.
+ if mandatory && end == self.bidi.text.len() {
+ let line = LineLayout::new(ctx, &self, end .. end);
+ if stack.regions.current.h.fits(line.size.h) {
+ stack.push(line);
+ }
+ }
+
+ stack.cts.min.y = Some(stack.size.h);
+ } else {
+ // Otherwise, the line fits both horizontally and vertically
+ // and we remember it.
+ stack.cts.min.x.set_max(line.size.w);
+ last = Some((line, end));
+ }
+ }
+
+ if let Some((line, _)) = last {
+ stack.push(line);
+ stack.cts.min.y = Some(stack.size.h);
+ }
+
+ stack.finish(ctx)
+ }
+
+ /// Find the index of the item whose range contains the `text_offset`.
+ fn find(&self, text_offset: usize) -> Option<usize> {
+ self.ranges.binary_search_by(|r| r.locate(text_offset)).ok()
+ }
+}
+
+/// A lightweight representation of a line that spans a specific range in a
+/// paragraph's text. This type enables you to cheaply measure the size of a
+/// line in a range before comitting to building the line's frame.
+struct LineLayout<'a> {
+ /// Bidi information about the paragraph.
+ par: &'a ParLayouter<'a>,
+ /// The range the line spans in the paragraph.
+ line: Range,
+ /// A reshaped text item if the line sliced up a text item at the start.
+ first: Option<ParItem<'a>>,
+ /// Middle items which don't need to be reprocessed.
+ items: &'a [ParItem<'a>],
+ /// A reshaped text item if the line sliced up a text item at the end. If
+ /// there is only one text item, this takes precedence over `first`.
+ last: Option<ParItem<'a>>,
+ /// The ranges, indexed as `[first, ..items, last]`. The ranges for `first`
+ /// and `last` aren't trimmed to the line, but it doesn't matter because
+ /// we're just checking which range an index falls into.
+ ranges: &'a [Range],
+ /// The size of the line.
+ size: Size,
+ /// The baseline of the line.
+ baseline: Length,
+ /// The sum of fractional ratios in the line.
+ fr: Fractional,
+}
+
+impl<'a> LineLayout<'a> {
+ /// Create a line which spans the given range.
+ fn new(ctx: &mut LayoutContext, par: &'a ParLayouter<'a>, mut line: Range) -> Self {
+ // Find the items which bound the text range.
+ let last_idx = par.find(line.end.saturating_sub(1)).unwrap();
+ let first_idx = if line.is_empty() {
+ last_idx
+ } else {
+ par.find(line.start).unwrap()
+ };
+
+ // Slice out the relevant items and ranges.
+ let mut items = &par.items[first_idx ..= last_idx];
+ let ranges = &par.ranges[first_idx ..= last_idx];
+
+ // Reshape the last item if it's split in half.
+ let mut last = None;
+ if let Some((ParItem::Text(shaped, align), rest)) = items.split_last() {
+ // Compute the range we want to shape, trimming whitespace at the
+ // end of the line.
+ let base = par.ranges[last_idx].start;
+ let start = line.start.max(base);
+ let end = start + par.bidi.text[start .. line.end].trim_end().len();
+ let range = start - base .. end - base;
+
+ // Reshape if necessary.
+ if range.len() < shaped.text.len() {
+ // If start == end and the rest is empty, then we have an empty
+ // line. To make that line have the appropriate height, we shape the
+ // empty string.
+ if !range.is_empty() || rest.is_empty() {
+ // Reshape that part.
+ let reshaped = shaped.reshape(ctx, range);
+ last = Some(ParItem::Text(reshaped, *align));
+ }
+
+ items = rest;
+ line.end = end;
+ }
+ }
+
+ // Reshape the start item if it's split in half.
+ let mut first = None;
+ if let Some((ParItem::Text(shaped, align), rest)) = items.split_first() {
+ // Compute the range we want to shape.
+ let Range { start: base, end: first_end } = par.ranges[first_idx];
+ let start = line.start;
+ let end = line.end.min(first_end);
+ let range = start - base .. end - base;
+
+ // Reshape if necessary.
+ if range.len() < shaped.text.len() {
+ if !range.is_empty() {
+ let reshaped = shaped.reshape(ctx, range);
+ first = Some(ParItem::Text(reshaped, *align));
+ }
+
+ items = rest;
+ }
+ }
+
+ let mut width = Length::zero();
+ let mut top = Length::zero();
+ let mut bottom = Length::zero();
+ let mut fr = Fractional::zero();
+
+ // Measure the size of the line.
+ for item in first.iter().chain(items).chain(&last) {
+ match *item {
+ ParItem::Absolute(v) => width += v,
+ ParItem::Fractional(v) => fr += v,
+ ParItem::Text(ShapedText { size, baseline, .. }, _)
+ | ParItem::Frame(Frame { size, baseline, .. }, _) => {
+ width += size.w;
+ top.set_max(baseline);
+ bottom.set_max(size.h - baseline);
+ }
+ }
+ }
+
+ Self {
+ par,
+ line,
+ first,
+ items,
+ last,
+ ranges,
+ size: Size::new(width, top + bottom),
+ baseline: top,
+ fr,
+ }
+ }
+
+ /// Build the line's frame.
+ fn build(&self, ctx: &LayoutContext, width: Length) -> Frame {
+ let size = Size::new(self.size.w.max(width), self.size.h);
+ let remaining = size.w - self.size.w;
+
+ let mut output = Frame::new(size, self.baseline);
+ let mut offset = Length::zero();
+ let mut ruler = Align::Start;
+
+ for (range, item) in self.reordered() {
+ let mut position = |mut frame: Frame, align: Align| {
+ // Decorate.
+ for (deco_range, deco) in &self.par.decos {
+ if deco_range.contains(&range.start) {
+ deco.apply(ctx, &mut frame);
+ }
+ }
+
+ // FIXME: Ruler alignment for RTL.
+ ruler = ruler.max(align);
+ let x = ruler.resolve(self.par.dir, offset .. remaining + offset);
+ let y = self.baseline - frame.baseline;
+ offset += frame.size.w;
+
+ // Add to the line's frame.
+ output.merge_frame(Point::new(x, y), frame);
+ };
+
+ match *item {
+ ParItem::Absolute(v) => offset += v,
+ ParItem::Fractional(v) => {
+ let ratio = v / self.fr;
+ if remaining.is_finite() && ratio.is_finite() {
+ offset += ratio * remaining;
+ }
+ }
+ ParItem::Text(ref shaped, align) => position(shaped.build(), align),
+ ParItem::Frame(ref frame, align) => position(frame.clone(), align),
+ }
+ }
+
+ output
+ }
+
+ /// Iterate through the line's items in visual order.
+ fn reordered(&self) -> impl Iterator<Item = (Range, &ParItem<'a>)> {
+ // The bidi crate doesn't like empty lines.
+ let (levels, runs) = if !self.line.is_empty() {
+ // Find the paragraph that contains the line.
+ let para = self
+ .par
+ .bidi
+ .paragraphs
+ .iter()
+ .find(|para| para.range.contains(&self.line.start))
+ .unwrap();
+
+ // Compute the reordered ranges in visual order (left to right).
+ self.par.bidi.visual_runs(para, self.line.clone())
+ } else {
+ <_>::default()
+ };
+
+ runs.into_iter()
+ .flat_map(move |run| {
+ let first_idx = self.find(run.start).unwrap();
+ let last_idx = self.find(run.end - 1).unwrap();
+ let range = first_idx ..= last_idx;
+
+ // Provide the items forwards or backwards depending on the run's
+ // direction.
+ if levels[run.start].is_ltr() {
+ Either::Left(range)
+ } else {
+ Either::Right(range.rev())
+ }
+ })
+ .map(move |idx| (self.ranges[idx].clone(), self.get(idx).unwrap()))
+ }
+
+ /// Find the index of the item whose range contains the `text_offset`.
+ fn find(&self, text_offset: usize) -> Option<usize> {
+ self.ranges.binary_search_by(|r| r.locate(text_offset)).ok()
+ }
+
+ /// Get the item at the index.
+ fn get(&self, index: usize) -> Option<&ParItem<'a>> {
+ self.first.iter().chain(self.items).chain(&self.last).nth(index)
+ }
+}
+
+/// Stacks lines on top of each other.
+struct LineStack<'a> {
+ leading: Length,
+ full: Size,
+ regions: Regions,
+ size: Size,
+ lines: Vec<LineLayout<'a>>,
+ finished: Vec<Constrained<Rc<Frame>>>,
+ cts: Constraints,
+ overflowing: bool,
+ fractional: bool,
+}
+
+impl<'a> LineStack<'a> {
+ /// Create an empty line stack.
+ fn new(leading: Length, regions: Regions) -> Self {
+ Self {
+ leading,
+ full: regions.current,
+ cts: Constraints::new(regions.expand),
+ regions,
+ size: Size::zero(),
+ lines: vec![],
+ finished: vec![],
+ overflowing: false,
+ fractional: false,
+ }
+ }
+
+ /// Push a new line into the stack.
+ fn push(&mut self, line: LineLayout<'a>) {
+ self.regions.current.h -= line.size.h + self.leading;
+
+ self.size.w.set_max(line.size.w);
+ self.size.h += line.size.h;
+ if !self.lines.is_empty() {
+ self.size.h += self.leading;
+ }
+
+ self.fractional |= !line.fr.is_zero();
+ self.lines.push(line);
+ }
+
+ /// Finish the frame for one region.
+ fn finish_region(&mut self, ctx: &LayoutContext) {
+ if self.regions.expand.x || self.fractional {
+ self.size.w = self.regions.current.w;
+ self.cts.exact.x = Some(self.regions.current.w);
+ }
+
+ if self.overflowing {
+ self.cts.min.y = None;
+ self.cts.max.y = None;
+ self.cts.exact = self.full.to_spec().map(Some);
+ }
+
+ let mut output = Frame::new(self.size, self.size.h);
+ let mut offset = Length::zero();
+ let mut first = true;
+
+ for line in self.lines.drain(..) {
+ let frame = line.build(ctx, self.size.w);
+
+ let pos = Point::new(Length::zero(), offset);
+ if first {
+ output.baseline = pos.y + frame.baseline;
+ first = false;
+ }
+
+ offset += frame.size.h + self.leading;
+ output.merge_frame(pos, frame);
+ }
+
+ self.finished.push(output.constrain(self.cts));
+ self.regions.next();
+ self.full = self.regions.current;
+ self.cts = Constraints::new(self.regions.expand);
+ self.size = Size::zero();
+ }
+
+ /// Finish the last region and return the built frames.
+ fn finish(mut self, ctx: &LayoutContext) -> Vec<Constrained<Rc<Frame>>> {
+ self.finish_region(ctx);
+ self.finished
+ }
+}
+
+/// Additional methods for BiDi levels.
+trait LevelExt: Sized {
+ fn from_dir(dir: Dir) -> Option<Self>;
+ fn dir(self) -> Dir;
+}
+
+impl LevelExt for Level {
+ fn from_dir(dir: Dir) -> Option<Self> {
+ match dir {
+ Dir::LTR => Some(Level::ltr()),
+ Dir::RTL => Some(Level::rtl()),
+ _ => None,
+ }
+ }
+
+ fn dir(self) -> Dir {
+ if self.is_ltr() { Dir::LTR } else { Dir::RTL }
+ }
+}
diff --git a/src/library/shape.rs b/src/library/shape.rs
new file mode 100644
index 00000000..1eba2285
--- /dev/null
+++ b/src/library/shape.rs
@@ -0,0 +1,185 @@
+use std::f64::consts::SQRT_2;
+
+use super::prelude::*;
+use super::PadNode;
+use crate::util::RcExt;
+
+/// `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")?;
+ let fill = args.named("fill")?;
+ let body = args.find();
+ Ok(shape_impl(ShapeKind::Rect, width, height, fill, body))
+}
+
+/// `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,
+ };
+ let fill = args.named("fill")?;
+ let body = args.find();
+ Ok(shape_impl(ShapeKind::Square, width, height, fill, body))
+}
+
+/// `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")?;
+ let fill = args.named("fill")?;
+ let body = args.find();
+ Ok(shape_impl(ShapeKind::Ellipse, width, height, fill, body))
+}
+
+/// `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,
+ };
+ let fill = args.named("fill")?;
+ let body = args.find();
+ Ok(shape_impl(ShapeKind::Circle, width, height, fill, body))
+}
+
+fn shape_impl(
+ shape: ShapeKind,
+ mut width: Option<Linear>,
+ mut height: Option<Linear>,
+ fill: Option<Color>,
+ body: Option<Template>,
+) -> Value {
+ // Set default shape size if there's no body.
+ if body.is_none() {
+ let v = Length::pt(30.0).into();
+ height.get_or_insert(v);
+ width.get_or_insert(match shape {
+ ShapeKind::Square | ShapeKind::Circle => v,
+ ShapeKind::Rect | ShapeKind::Ellipse => 1.5 * v,
+ });
+ }
+
+ Value::Template(Template::from_inline(move |style| ShapeNode {
+ shape,
+ width,
+ height,
+ fill: Some(Paint::Color(
+ fill.unwrap_or(Color::Rgba(RgbaColor::new(175, 175, 175, 255))),
+ )),
+ child: body.as_ref().map(|template| template.to_stack(style).pack()),
+ }))
+}
+
+/// Places its child into a sizable and fillable shape.
+#[derive(Debug, Hash)]
+pub struct ShapeNode {
+ /// Which shape to place the child into.
+ pub shape: ShapeKind,
+ /// The width, if any.
+ pub width: Option<Linear>,
+ /// The height, if any.
+ pub height: Option<Linear>,
+ /// How to fill the shape, if at all.
+ pub fill: Option<Paint>,
+ /// The child node to place into the shape, if any.
+ pub child: Option<BlockNode>,
+}
+
+/// The type of a shape.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum ShapeKind {
+ /// A rectangle with equal side lengths.
+ Square,
+ /// A quadrilateral with four right angles.
+ Rect,
+ /// An ellipse with coinciding foci.
+ Circle,
+ /// A curve around two focal points.
+ Ellipse,
+}
+
+impl InlineLevel for ShapeNode {
+ fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame {
+ // Resolve width and height relative to the region's base.
+ let width = self.width.map(|w| w.resolve(base.w));
+ let height = self.height.map(|h| h.resolve(base.h));
+
+ // Layout.
+ let mut frame = if let Some(child) = &self.child {
+ let mut node: &dyn BlockLevel = child;
+
+ let padded;
+ if matches!(self.shape, ShapeKind::Circle | ShapeKind::Ellipse) {
+ // Padding with this ratio ensures that a rectangular child fits
+ // perfectly into a circle / an ellipse.
+ padded = PadNode {
+ padding: Sides::splat(Relative::new(0.5 - SQRT_2 / 4.0).into()),
+ child: child.clone(),
+ };
+ node = &padded;
+ }
+
+ // The "pod" is the region into which the child will be layouted.
+ let mut pod = {
+ let size = Size::new(width.unwrap_or(space), height.unwrap_or(base.h));
+
+ let base = Size::new(
+ if width.is_some() { size.w } else { base.w },
+ if height.is_some() { size.h } else { base.h },
+ );
+
+ let expand = Spec::new(width.is_some(), height.is_some());
+ Regions::one(size, base, expand)
+ };
+
+ // Now, layout the child.
+ let mut frames = node.layout(ctx, &pod);
+
+ if matches!(self.shape, ShapeKind::Square | ShapeKind::Circle) {
+ // Relayout with full expansion into square region to make sure
+ // the result is really a square or circle.
+ let size = frames[0].item.size;
+ pod.current.w = size.w.max(size.h).min(pod.current.w);
+ pod.current.h = pod.current.w;
+ pod.expand = Spec::splat(true);
+ frames = node.layout(ctx, &pod);
+ }
+
+ // Validate and set constraints.
+ assert_eq!(frames.len(), 1);
+ Rc::take(frames.into_iter().next().unwrap().item)
+ } else {
+ // Resolve shape size.
+ let size = Size::new(width.unwrap_or_default(), height.unwrap_or_default());
+ Frame::new(size, size.h)
+ };
+
+ // Add background shape if desired.
+ if let Some(fill) = self.fill {
+ let (pos, geometry) = match self.shape {
+ ShapeKind::Square | ShapeKind::Rect => {
+ (Point::zero(), Geometry::Rect(frame.size))
+ }
+ ShapeKind::Circle | ShapeKind::Ellipse => {
+ (frame.size.to_point() / 2.0, Geometry::Ellipse(frame.size))
+ }
+ };
+
+ frame.prepend(pos, Element::Geometry(geometry, fill));
+ }
+
+ frame
+ }
+}
diff --git a/src/library/spacing.rs b/src/library/spacing.rs
new file mode 100644
index 00000000..8ffd5e7a
--- /dev/null
+++ b/src/library/spacing.rs
@@ -0,0 +1,24 @@
+use super::prelude::*;
+
+/// `h`: Horizontal spacing.
+pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let mut template = Template::new();
+ template.spacing(GenAxis::Inline, args.expect("spacing")?);
+ Ok(Value::Template(template))
+}
+
+/// `v`: Vertical spacing.
+pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let mut template = Template::new();
+ template.spacing(GenAxis::Block, args.expect("spacing")?);
+ Ok(Value::Template(template))
+}
+
+/// Kinds of spacing.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Spacing {
+ /// A length stated in absolute values and/or relative to the parent's size.
+ Linear(Linear),
+ /// A length that is the fraction of the remaining free space in the parent.
+ Fractional(Fractional),
+}
diff --git a/src/library/stack.rs b/src/library/stack.rs
new file mode 100644
index 00000000..80ed507b
--- /dev/null
+++ b/src/library/stack.rs
@@ -0,0 +1,274 @@
+use std::fmt::{self, Debug, Formatter};
+
+use super::prelude::*;
+use super::Spacing;
+
+/// `stack`: Stack children along an axis.
+pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ enum Child {
+ Spacing(Spacing),
+ Any(Template),
+ }
+
+ castable! {
+ Child: "linear, fractional or template",
+ Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())),
+ Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())),
+ Value::Linear(v) => Self::Spacing(Spacing::Linear(v)),
+ Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)),
+ Value::Template(v) => Self::Any(v),
+ }
+
+ let dir = args.named("dir")?.unwrap_or(Dir::TTB);
+ let spacing = args.named("spacing")?;
+ let list: Vec<Child> = args.all().collect();
+
+ Ok(Value::Template(Template::from_block(move |style| {
+ let mut children = vec![];
+ let mut delayed = None;
+
+ // Build the list of stack children.
+ for child in &list {
+ match child {
+ Child::Spacing(v) => {
+ children.push(StackChild::Spacing(*v));
+ delayed = None;
+ }
+ Child::Any(template) => {
+ if let Some(v) = delayed {
+ children.push(StackChild::Spacing(v));
+ }
+
+ let node = template.to_stack(style).pack();
+ children.push(StackChild::Node(node, style.aligns.block));
+ delayed = spacing;
+ }
+ }
+ }
+
+ StackNode { dir, children }
+ })))
+}
+
+/// A node that stacks its children.
+#[derive(Debug, Hash)]
+pub struct StackNode {
+ /// The stacking direction.
+ pub dir: Dir,
+ /// The children to be stacked.
+ pub children: Vec<StackChild>,
+}
+
+/// A child of a stack node.
+#[derive(Hash)]
+pub enum StackChild {
+ /// Spacing between other nodes.
+ Spacing(Spacing),
+ /// Any block node and how to align it in the stack.
+ Node(BlockNode, Align),
+}
+
+impl BlockLevel for StackNode {
+ fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ regions: &Regions,
+ ) -> Vec<Constrained<Rc<Frame>>> {
+ StackLayouter::new(self, regions.clone()).layout(ctx)
+ }
+}
+
+impl Debug for StackChild {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Spacing(v) => write!(f, "Spacing({:?})", v),
+ Self::Node(node, _) => node.fmt(f),
+ }
+ }
+}
+
+/// Performs stack layout.
+struct StackLayouter<'a> {
+ /// The stack node to layout.
+ stack: &'a StackNode,
+ /// The axis of the block direction.
+ axis: SpecAxis,
+ /// Whether the stack should expand to fill the region.
+ expand: Spec<bool>,
+ /// The region to layout into.
+ regions: Regions,
+ /// The full size of `regions.current` that was available before we started
+ /// subtracting.
+ full: Size,
+ /// The generic size used by the frames for the current region.
+ used: Gen<Length>,
+ /// The sum of fractional ratios in the current region.
+ fr: Fractional,
+ /// Spacing and layouted nodes.
+ items: Vec<StackItem>,
+ /// Finished frames for previous regions.
+ finished: Vec<Constrained<Rc<Frame>>>,
+}
+
+/// A prepared item in a stack layout.
+enum StackItem {
+ /// Absolute spacing between other items.
+ Absolute(Length),
+ /// Fractional spacing between other items.
+ Fractional(Fractional),
+ /// A layouted child node.
+ Frame(Rc<Frame>, Align),
+}
+
+impl<'a> StackLayouter<'a> {
+ /// Create a new stack layouter.
+ fn new(stack: &'a StackNode, mut regions: Regions) -> Self {
+ // Disable expansion along the block axis for children.
+ let axis = stack.dir.axis();
+ let expand = regions.expand;
+ regions.expand.set(axis, false);
+
+ Self {
+ stack,
+ axis,
+ expand,
+ full: regions.current,
+ regions,
+ used: Gen::zero(),
+ fr: Fractional::zero(),
+ items: vec![],
+ finished: vec![],
+ }
+ }
+
+ /// Layout all children.
+ fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> {
+ for child in &self.stack.children {
+ match *child {
+ StackChild::Spacing(Spacing::Linear(v)) => {
+ self.layout_absolute(v);
+ }
+ StackChild::Spacing(Spacing::Fractional(v)) => {
+ self.items.push(StackItem::Fractional(v));
+ self.fr += v;
+ }
+ StackChild::Node(ref node, align) => {
+ self.layout_node(ctx, node, align);
+ }
+ }
+ }
+
+ self.finish_region();
+ self.finished
+ }
+
+ /// Layout absolute spacing.
+ fn layout_absolute(&mut self, amount: Linear) {
+ // Resolve the linear, limiting it to the remaining available space.
+ let remaining = self.regions.current.get_mut(self.axis);
+ let resolved = amount.resolve(self.full.get(self.axis));
+ let limited = resolved.min(*remaining);
+ *remaining -= limited;
+ self.used.block += limited;
+ self.items.push(StackItem::Absolute(resolved));
+ }
+
+ /// Layout a block node.
+ fn layout_node(&mut self, ctx: &mut LayoutContext, node: &BlockNode, align: Align) {
+ let frames = node.layout(ctx, &self.regions);
+ let len = frames.len();
+ for (i, frame) in frames.into_iter().enumerate() {
+ // Grow our size.
+ let size = frame.item.size.to_gen(self.axis);
+ self.used.block += size.block;
+ self.used.inline.set_max(size.inline);
+
+ // Remember the frame and shrink available space in the region for the
+ // following children.
+ self.items.push(StackItem::Frame(frame.item, align));
+ *self.regions.current.get_mut(self.axis) -= size.block;
+
+ if i + 1 < len {
+ self.finish_region();
+ }
+ }
+ }
+
+ /// Finish the frame for one region.
+ fn finish_region(&mut self) {
+ // Determine the size that remains for fractional spacing.
+ let remaining = self.full.get(self.axis) - self.used.block;
+
+ // Determine the size of the stack in this region dependening on whether
+ // the region expands.
+ let used = self.used.to_size(self.axis);
+ let mut size = Size::new(
+ if self.expand.x { self.full.w } else { used.w },
+ if self.expand.y { self.full.h } else { used.h },
+ );
+
+ // Expand fully if there are fr spacings.
+ let full = self.full.get(self.axis);
+ if !self.fr.is_zero() && full.is_finite() {
+ size.set(self.axis, full);
+ }
+
+ let mut output = Frame::new(size, size.h);
+ let mut before = Length::zero();
+ let mut ruler = Align::Start;
+ let mut first = true;
+
+ // Place all frames.
+ for item in self.items.drain(..) {
+ match item {
+ StackItem::Absolute(v) => before += v,
+ StackItem::Fractional(v) => {
+ let ratio = v / self.fr;
+ if remaining.is_finite() && ratio.is_finite() {
+ before += ratio * remaining;
+ }
+ }
+ StackItem::Frame(frame, align) => {
+ ruler = ruler.max(align);
+
+ let parent = size.to_gen(self.axis);
+ let child = frame.size.to_gen(self.axis);
+
+ // Align along the block axis.
+ let block = ruler.resolve(
+ self.stack.dir,
+ if self.stack.dir.is_positive() {
+ let after = self.used.block - before;
+ before .. parent.block - after
+ } else {
+ let before_with_self = before + child.block;
+ let after = self.used.block - before_with_self;
+ after .. parent.block - before_with_self
+ },
+ );
+
+ let pos = Gen::new(Length::zero(), block).to_point(self.axis);
+ if first {
+ // The baseline of the stack is that of the first frame.
+ output.baseline = pos.y + frame.baseline;
+ first = false;
+ }
+
+ output.push_frame(pos, frame);
+ before += child.block;
+ }
+ }
+ }
+
+ // Generate tight constraints for now.
+ let mut cts = Constraints::new(self.expand);
+ cts.exact = self.full.to_spec().map(Some);
+ cts.base = self.regions.base.to_spec().map(Some);
+
+ self.regions.next();
+ self.full = self.regions.current;
+ self.used = Gen::zero();
+ self.fr = Fractional::zero();
+ self.finished.push(output.constrain(cts));
+ }
+}
diff --git a/src/library/text.rs b/src/library/text.rs
index e4d52396..b33a002d 100644
--- a/src/library/text.rs
+++ b/src/library/text.rs
@@ -1,5 +1,13 @@
-use super::*;
-use crate::layout::{Decoration, LineDecoration, LineKind};
+use std::borrow::Cow;
+use std::ops::Range;
+
+use rustybuzz::UnicodeBuffer;
+
+use super::prelude::*;
+use crate::font::{Face, FaceId, FontFamily, FontVariant};
+use crate::geom::{Dir, Em, Length, Point, Size};
+use crate::style::{Style, TextStyle};
+use crate::util::SliceExt;
/// `font`: Configure the font.
pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
@@ -108,100 +116,362 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
})
}
-/// `par`: Configure paragraphs.
-pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let spacing = args.named("spacing")?;
- let leading = args.named("leading")?;
+/// Shape text into [`ShapedText`].
+pub fn shape<'a>(
+ ctx: &mut LayoutContext,
+ text: &'a str,
+ style: &'a TextStyle,
+ dir: Dir,
+) -> ShapedText<'a> {
+ let mut glyphs = vec![];
+ if !text.is_empty() {
+ shape_segment(
+ ctx,
+ &mut glyphs,
+ 0,
+ text,
+ style.size,
+ style.variant(),
+ style.families(),
+ None,
+ dir,
+ );
+ }
- ctx.template.modify(move |style| {
- let par = style.par_mut();
+ let (size, baseline) = measure(ctx, &glyphs, style);
+ ShapedText {
+ text,
+ dir,
+ style,
+ size,
+ baseline,
+ glyphs: Cow::Owned(glyphs),
+ }
+}
- if let Some(spacing) = spacing {
- par.spacing = spacing;
- }
+/// The result of shaping text.
+///
+/// This type contains owned or borrowed shaped text runs, which can be
+/// measured, used to reshape substrings more quickly and converted into a
+/// frame.
+#[derive(Debug, Clone)]
+pub struct ShapedText<'a> {
+ /// The text that was shaped.
+ pub text: &'a str,
+ /// The text direction.
+ pub dir: Dir,
+ /// The properties used for font selection.
+ pub style: &'a TextStyle,
+ /// The font size.
+ pub size: Size,
+ /// The baseline from the top of the frame.
+ pub baseline: Length,
+ /// The shaped glyphs.
+ pub glyphs: Cow<'a, [ShapedGlyph]>,
+}
- if let Some(leading) = leading {
- par.leading = leading;
- }
- });
+/// A single glyph resulting from shaping.
+#[derive(Debug, Copy, Clone)]
+pub struct ShapedGlyph {
+ /// The font face the glyph is contained in.
+ pub face_id: FaceId,
+ /// The glyph's index in the face.
+ pub glyph_id: u16,
+ /// The advance width of the glyph.
+ pub x_advance: Em,
+ /// The horizontal offset of the glyph.
+ pub x_offset: Em,
+ /// The start index of the glyph in the source text.
+ pub text_index: usize,
+ /// Whether splitting the shaping result before this glyph would yield the
+ /// same results as shaping the parts to both sides of `text_index`
+ /// separately.
+ pub safe_to_break: bool,
+}
- ctx.template.parbreak();
+impl<'a> ShapedText<'a> {
+ /// Build the shaped text's frame.
+ pub fn build(&self) -> Frame {
+ let mut frame = Frame::new(self.size, self.baseline);
+ let mut offset = Length::zero();
+
+ for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
+ let pos = Point::new(offset, self.baseline);
+
+ let mut text = Text {
+ face_id,
+ size: self.style.size,
+ width: Length::zero(),
+ fill: self.style.fill,
+ glyphs: vec![],
+ };
+
+ for glyph in group {
+ text.glyphs.push(Glyph {
+ id: glyph.glyph_id,
+ x_advance: glyph.x_advance,
+ x_offset: glyph.x_offset,
+ });
+ text.width += glyph.x_advance.to_length(text.size);
+ }
+
+ offset += text.width;
+ frame.push(pos, Element::Text(text));
+ }
- Ok(Value::None)
-}
+ frame
+ }
-/// `lang`: Configure the language.
-pub fn lang(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let iso = args.find::<Str>();
- let dir = if let Some(dir) = args.named::<Spanned<Dir>>("dir")? {
- if dir.v.axis() == SpecAxis::Horizontal {
- Some(dir.v)
+ /// Reshape a range of the shaped text, reusing information from this
+ /// shaping process if possible.
+ pub fn reshape(
+ &'a self,
+ ctx: &mut LayoutContext,
+ text_range: Range<usize>,
+ ) -> ShapedText<'a> {
+ if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
+ let (size, baseline) = measure(ctx, glyphs, self.style);
+ Self {
+ text: &self.text[text_range],
+ dir: self.dir,
+ style: self.style,
+ size,
+ baseline,
+ glyphs: Cow::Borrowed(glyphs),
+ }
} else {
- bail!(dir.span, "must be horizontal");
+ shape(ctx, &self.text[text_range], self.style, self.dir)
+ }
+ }
+
+ /// Find the subslice of glyphs that represent the given text range if both
+ /// sides are safe to break.
+ fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
+ let Range { mut start, mut end } = text_range;
+ if !self.dir.is_positive() {
+ std::mem::swap(&mut start, &mut end);
}
- } else {
- iso.as_deref().map(lang_dir)
- };
- if let Some(dir) = dir {
- ctx.template.modify(move |style| style.dir = dir);
+ let left = self.find_safe_to_break(start, Side::Left)?;
+ let right = self.find_safe_to_break(end, Side::Right)?;
+ Some(&self.glyphs[left .. right])
}
- ctx.template.parbreak();
+ /// Find the glyph offset matching the text index that is most towards the
+ /// given side and safe-to-break.
+ fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
+ let ltr = self.dir.is_positive();
+
+ // Handle edge cases.
+ let len = self.glyphs.len();
+ if text_index == 0 {
+ return Some(if ltr { 0 } else { len });
+ } else if text_index == self.text.len() {
+ return Some(if ltr { len } else { 0 });
+ }
- Ok(Value::None)
-}
+ // Find any glyph with the text index.
+ let mut idx = self
+ .glyphs
+ .binary_search_by(|g| {
+ let ordering = g.text_index.cmp(&text_index);
+ if ltr { ordering } else { ordering.reverse() }
+ })
+ .ok()?;
+
+ let next = match towards {
+ Side::Left => usize::checked_sub,
+ Side::Right => usize::checked_add,
+ };
+
+ // Search for the outermost glyph with the text index.
+ while let Some(next) = next(idx, 1) {
+ if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
+ break;
+ }
+ idx = next;
+ }
+
+ // RTL needs offset one because the left side of the range should be
+ // exclusive and the right side inclusive, contrary to the normal
+ // behaviour of ranges.
+ if !ltr {
+ idx += 1;
+ }
-/// The default direction for the language identified by the given `iso` code.
-fn lang_dir(iso: &str) -> Dir {
- match iso.to_ascii_lowercase().as_str() {
- "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
- "en" | "fr" | "de" => Dir::LTR,
- _ => Dir::LTR,
+ self.glyphs[idx].safe_to_break.then(|| idx)
}
}
-/// `strike`: Typeset striken-through text.
-pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Strikethrough)
+/// A visual side.
+enum Side {
+ Left,
+ Right,
}
-/// `underline`: Typeset underlined text.
-pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Underline)
-}
+/// Shape text with font fallback using the `families` iterator.
+fn shape_segment<'a>(
+ ctx: &mut LayoutContext,
+ glyphs: &mut Vec<ShapedGlyph>,
+ base: usize,
+ text: &str,
+ size: Length,
+ variant: FontVariant,
+ mut families: impl Iterator<Item = &'a str> + Clone,
+ mut first_face: Option<FaceId>,
+ dir: Dir,
+) {
+ // Select the font family.
+ let (face_id, fallback) = loop {
+ // Try to load the next available font family.
+ match families.next() {
+ Some(family) => {
+ if let Some(id) = ctx.fonts.select(family, variant) {
+ break (id, true);
+ }
+ }
+ // We're out of families, so we don't do any more fallback and just
+ // shape the tofus with the first face we originally used.
+ None => match first_face {
+ Some(id) => break (id, false),
+ None => return,
+ },
+ }
+ };
-/// `overline`: Typeset text with an overline.
-pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Overline)
-}
+ // Remember the id if this the first available face since we use that one to
+ // shape tofus.
+ first_face.get_or_insert(face_id);
+
+ // Fill the buffer with our text.
+ let mut buffer = UnicodeBuffer::new();
+ buffer.push_str(text);
+ buffer.set_direction(match dir {
+ Dir::LTR => rustybuzz::Direction::LeftToRight,
+ Dir::RTL => rustybuzz::Direction::RightToLeft,
+ _ => unimplemented!(),
+ });
+
+ // Shape!
+ let mut face = ctx.fonts.get(face_id);
+ let buffer = rustybuzz::shape(face.ttf(), &[], buffer);
+ let infos = buffer.glyph_infos();
+ let pos = buffer.glyph_positions();
+
+ // Collect the shaped glyphs, doing fallback and shaping parts again with
+ // the next font if necessary.
+ let mut i = 0;
+ while i < infos.len() {
+ let info = &infos[i];
+ let cluster = info.cluster as usize;
+
+ if info.glyph_id != 0 || !fallback {
+ // Add the glyph to the shaped output.
+ // TODO: Don't ignore y_advance and y_offset.
+ glyphs.push(ShapedGlyph {
+ face_id,
+ glyph_id: info.glyph_id as u16,
+ x_advance: face.to_em(pos[i].x_advance),
+ x_offset: face.to_em(pos[i].x_offset),
+ text_index: base + cluster,
+ safe_to_break: !info.unsafe_to_break(),
+ });
+ } else {
+ // Determine the source text range for the tofu sequence.
+ let range = {
+ // First, search for the end of the tofu sequence.
+ let k = i;
+ while infos.get(i + 1).map_or(false, |info| info.glyph_id == 0) {
+ i += 1;
+ }
+
+ // Then, determine the start and end text index.
+ //
+ // Examples:
+ // Everything is shown in visual order. Tofus are written as "_".
+ // We want to find out that the tofus span the text `2..6`.
+ // Note that the clusters are longer than 1 char.
+ //
+ // Left-to-right:
+ // Text: h a l i h a l l o
+ // Glyphs: A _ _ C E
+ // Clusters: 0 2 4 6 8
+ // k=1 i=2
+ //
+ // Right-to-left:
+ // Text: O L L A H I L A H
+ // Glyphs: E C _ _ A
+ // Clusters: 8 6 4 2 0
+ // k=2 i=3
+
+ let ltr = dir.is_positive();
+ let first = if ltr { k } else { i };
+ let start = infos[first].cluster as usize;
+
+ let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) };
+ let end = last
+ .and_then(|last| infos.get(last))
+ .map_or(text.len(), |info| info.cluster as usize);
+
+ start .. end
+ };
+
+ // Recursively shape the tofu sequence with the next family.
+ shape_segment(
+ ctx,
+ glyphs,
+ base + range.start,
+ &text[range],
+ size,
+ variant,
+ families.clone(),
+ first_face,
+ dir,
+ );
+
+ face = ctx.fonts.get(face_id);
+ }
-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: Template = args.expect("body")?;
-
- Ok(Value::Template(body.decorate(Decoration::Line(
- LineDecoration {
- kind,
- stroke: stroke.map(Paint::Color),
- thickness,
- offset,
- extent,
- },
- ))))
+ i += 1;
+ }
}
-/// `link`: Typeset text as a link.
-pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let url = args.expect::<Str>("url")?;
- let body = args.find().unwrap_or_else(|| {
- let mut template = Template::new();
- template.text(url.trim_start_matches("mailto:").trim_start_matches("tel:"));
- template
- });
+/// Measure the size and baseline of a run of shaped glyphs with the given
+/// properties.
+fn measure(
+ ctx: &mut LayoutContext,
+ glyphs: &[ShapedGlyph],
+ style: &TextStyle,
+) -> (Size, Length) {
+ let mut width = Length::zero();
+ let mut top = Length::zero();
+ let mut bottom = Length::zero();
+
+ // Expand top and bottom by reading the face's vertical metrics.
+ let mut expand = |face: &Face| {
+ top.set_max(face.vertical_metric(style.top_edge, style.size));
+ bottom.set_max(-face.vertical_metric(style.bottom_edge, style.size));
+ };
+
+ if glyphs.is_empty() {
+ // When there are no glyphs, we just use the vertical metrics of the
+ // first available font.
+ for family in style.families() {
+ if let Some(face_id) = ctx.fonts.select(family, style.variant) {
+ expand(ctx.fonts.get(face_id));
+ break;
+ }
+ }
+ } else {
+ for (face_id, group) in glyphs.group_by_key(|g| g.face_id) {
+ let face = ctx.fonts.get(face_id);
+ expand(face);
+
+ for glyph in group {
+ width += glyph.x_advance.to_length(style.size);
+ }
+ }
+ }
- Ok(Value::Template(body.decorate(Decoration::Link(url.into()))))
+ (Size::new(width, top + bottom), top)
}
diff --git a/src/library/transform.rs b/src/library/transform.rs
new file mode 100644
index 00000000..846a7262
--- /dev/null
+++ b/src/library/transform.rs
@@ -0,0 +1,44 @@
+use super::prelude::*;
+use super::{ShapeKind, ShapeNode};
+
+/// `move`: Move content without affecting layout.
+pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let x = args.named("x")?;
+ let y = args.named("y")?;
+ let body: Template = args.expect("body")?;
+
+ Ok(Value::Template(Template::from_inline(move |style| {
+ MoveNode {
+ offset: Spec::new(x, y),
+ child: ShapeNode {
+ shape: ShapeKind::Rect,
+ width: None,
+ height: None,
+ fill: None,
+ child: Some(body.to_stack(style).pack()),
+ },
+ }
+ })))
+}
+
+#[derive(Debug, Hash)]
+struct MoveNode {
+ offset: Spec<Option<Linear>>,
+ child: ShapeNode,
+}
+
+impl InlineLevel for MoveNode {
+ fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame {
+ let offset = Point::new(
+ self.offset.x.map(|x| x.resolve(base.w)).unwrap_or_default(),
+ self.offset.y.map(|y| y.resolve(base.h)).unwrap_or_default(),
+ );
+
+ let mut frame = self.child.layout(ctx, space, base);
+ for (point, _) in &mut frame.children {
+ *point += offset;
+ }
+
+ frame
+ }
+}
diff --git a/src/library/utility.rs b/src/library/utility.rs
index 15f2bcf5..05b7639b 100644
--- a/src/library/utility.rs
+++ b/src/library/utility.rs
@@ -1,7 +1,8 @@
use std::cmp::Ordering;
use std::str::FromStr;
-use super::*;
+use super::prelude::*;
+use crate::eval::Array;
/// `assert`: Ensure that a condition is fulfilled.
pub fn assert(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {