diff options
Diffstat (limited to 'src/library')
| -rw-r--r-- | src/library/align.rs | 51 | ||||
| -rw-r--r-- | src/library/container.rs | 27 | ||||
| -rw-r--r-- | src/library/deco.rs | 137 | ||||
| -rw-r--r-- | src/library/elements.rs | 104 | ||||
| -rw-r--r-- | src/library/grid.rs | 602 | ||||
| -rw-r--r-- | src/library/image.rs | 68 | ||||
| -rw-r--r-- | src/library/layout.rs | 332 | ||||
| -rw-r--r-- | src/library/mod.rs | 48 | ||||
| -rw-r--r-- | src/library/pad.rs | 101 | ||||
| -rw-r--r-- | src/library/page.rs | 76 | ||||
| -rw-r--r-- | src/library/par.rs | 674 | ||||
| -rw-r--r-- | src/library/shape.rs | 185 | ||||
| -rw-r--r-- | src/library/spacing.rs | 24 | ||||
| -rw-r--r-- | src/library/stack.rs | 274 | ||||
| -rw-r--r-- | src/library/text.rs | 420 | ||||
| -rw-r--r-- | src/library/transform.rs | 44 | ||||
| -rw-r--r-- | src/library/utility.rs | 3 |
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, ®ions).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, ®ions).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, ®ions).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, ®ions); + 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, + ®ions.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> { |
