summaryrefslogtreecommitdiff
path: root/src/library
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-10-31 15:52:16 +0100
committerLaurenz <laurmaedje@gmail.com>2021-10-31 15:52:35 +0100
commit5b344b663a3d224134923eea0d67ebf44c069b07 (patch)
tree34a5fb464a38b9d4cb11294379b3ddf351dfce21 /src/library
parentfeff013abb17f31bc5305fe77fe67cf615c19ff2 (diff)
Reorganize modules
Instead of separating functionality into layout and library, everything lives in the library now. This way, related things live side by side and there are no duplicate file names in the two directories.
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> {