Reorganize modules
Instead of separating functionality into layout and library, everything lives in the library now. This way, related things live side by side and there are no duplicate file names in the two directories.
This commit is contained in:
parent
feff013abb
commit
5b344b663a
@ -8,9 +8,9 @@ use std::rc::Rc;
|
||||
use super::Str;
|
||||
use crate::diag::StrResult;
|
||||
use crate::geom::{Align, Dir, GenAxis, Length, Linear, Sides, Size};
|
||||
use crate::layout::{
|
||||
BlockLevel, BlockNode, Decoration, InlineLevel, InlineNode, PadNode, PageNode,
|
||||
ParChild, ParNode, Spacing, StackChild, StackNode,
|
||||
use crate::layout::{BlockLevel, BlockNode, InlineLevel, InlineNode, PageNode};
|
||||
use crate::library::{
|
||||
Decoration, PadNode, ParChild, ParNode, Spacing, StackChild, StackNode,
|
||||
};
|
||||
use crate::style::Style;
|
||||
use crate::util::EcoString;
|
||||
|
@ -3,7 +3,8 @@ use std::rc::Rc;
|
||||
use super::{Eval, EvalContext, Str, Template, Value};
|
||||
use crate::diag::TypResult;
|
||||
use crate::geom::Align;
|
||||
use crate::layout::{BlockLevel, ParChild, ParNode, Spacing, StackChild, StackNode};
|
||||
use crate::layout::BlockLevel;
|
||||
use crate::library::{ParChild, ParNode, Spacing, StackChild, StackNode};
|
||||
use crate::syntax::*;
|
||||
use crate::util::BoolExt;
|
||||
|
||||
|
@ -14,9 +14,9 @@ use ttf_parser::{name_id, GlyphId, Tag};
|
||||
|
||||
use super::subset;
|
||||
use crate::font::{find_name, FaceId, FontStore};
|
||||
use crate::frame::{Element, Frame, Geometry};
|
||||
use crate::geom::{self, Color, Em, Length, Paint, Size};
|
||||
use crate::image::{Image, ImageId, ImageStore};
|
||||
use crate::layout::{Element, Frame, Geometry};
|
||||
use crate::Context;
|
||||
|
||||
/// Export a collection of frames into a PDF document.
|
||||
|
@ -1,9 +1,10 @@
|
||||
//! Finished layouts.
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::rc::Rc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Constrained, Constraints};
|
||||
use crate::font::FaceId;
|
||||
use crate::geom::{Em, Length, Paint, Path, Point, Size};
|
||||
use crate::image::ImageId;
|
||||
@ -58,11 +59,6 @@ impl Frame {
|
||||
pub fn elements(&self) -> Elements {
|
||||
Elements { stack: vec![(0, Point::zero(), self)] }
|
||||
}
|
||||
|
||||
/// Wrap the frame with constraints.
|
||||
pub fn constrain(self, cts: Constraints) -> Constrained<Rc<Self>> {
|
||||
Constrained { item: Rc::new(self), cts }
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Frame {
|
@ -1,4 +1,19 @@
|
||||
use super::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::frame::Frame;
|
||||
use crate::geom::{Length, Size, Spec};
|
||||
|
||||
/// Constrain a frame with constraints.
|
||||
pub trait Constrain {
|
||||
/// Reference-count the frame and wrap it with constraints.
|
||||
fn constrain(self, cts: Constraints) -> Constrained<Rc<Frame>>;
|
||||
}
|
||||
|
||||
impl Constrain for Frame {
|
||||
fn constrain(self, cts: Constraints) -> Constrained<Rc<Frame>> {
|
||||
Constrained::new(Rc::new(self), cts)
|
||||
}
|
||||
}
|
||||
|
||||
/// Carries an item that is only valid in certain regions and the constraints
|
||||
/// that describe these regions.
|
||||
@ -10,6 +25,13 @@ pub struct Constrained<T> {
|
||||
pub cts: Constraints,
|
||||
}
|
||||
|
||||
impl<T> Constrained<T> {
|
||||
/// Constrain an item with constraints.
|
||||
pub fn new(item: T, cts: Constraints) -> Self {
|
||||
Self { item, cts }
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe regions that match them.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct Constraints {
|
||||
|
@ -1,10 +1,12 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use decorum::N32;
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::*;
|
||||
use super::{Constrained, Regions};
|
||||
use crate::frame::Frame;
|
||||
|
||||
const TEMP_LEN: usize = 5;
|
||||
const TEMP_LAST: usize = TEMP_LEN - 1;
|
||||
@ -396,6 +398,8 @@ impl PatternProperties {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::geom::{Size, Spec};
|
||||
use crate::layout::Constraints;
|
||||
|
||||
fn empty_frames() -> Vec<Constrained<Rc<Frame>>> {
|
||||
vec![Constrained {
|
||||
|
199
src/layout/levels.rs
Normal file
199
src/layout/levels.rs
Normal file
@ -0,0 +1,199 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::*;
|
||||
use crate::geom::{Length, Size};
|
||||
|
||||
/// Page-level nodes directly produce frames representing pages.
|
||||
///
|
||||
/// Such nodes create their own regions instead of being supplied with them from
|
||||
/// some parent.
|
||||
pub trait PageLevel: Debug {
|
||||
/// Layout the node, producing one frame per page.
|
||||
fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>>;
|
||||
}
|
||||
|
||||
/// Layouts its children onto one or multiple pages.
|
||||
#[derive(Debug)]
|
||||
pub struct PageNode {
|
||||
/// The size of the page.
|
||||
pub size: Size,
|
||||
/// The node that produces the actual pages.
|
||||
pub child: BlockNode,
|
||||
}
|
||||
|
||||
impl PageLevel for PageNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
|
||||
// When one of the lengths is infinite the page fits its content along
|
||||
// that axis.
|
||||
let expand = self.size.to_spec().map(Length::is_finite);
|
||||
let regions = Regions::repeat(self.size, self.size, expand);
|
||||
self.child.layout(ctx, ®ions).into_iter().map(|c| c.item).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PageLevel for T
|
||||
where
|
||||
T: AsRef<[PageNode]> + Debug + ?Sized,
|
||||
{
|
||||
fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
|
||||
self.as_ref().iter().flat_map(|node| node.layout(ctx)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Block-level nodes can be layouted into a sequence of regions.
|
||||
///
|
||||
/// They return one frame per used region alongside constraints that define
|
||||
/// whether the result is reusable in other regions.
|
||||
pub trait BlockLevel: Debug {
|
||||
/// Layout the node into the given regions, producing constrained frames.
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>>;
|
||||
|
||||
/// Convert to a packed block-level node.
|
||||
fn pack(self) -> BlockNode
|
||||
where
|
||||
Self: Sized + Hash + 'static,
|
||||
{
|
||||
BlockNode {
|
||||
#[cfg(feature = "layout-cache")]
|
||||
hash: hash_node(&self),
|
||||
node: Rc::new(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A packed [block-level](BlockLevel) layouting node with precomputed hash.
|
||||
#[derive(Clone)]
|
||||
pub struct BlockNode {
|
||||
node: Rc<dyn BlockLevel>,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
impl BlockLevel for BlockNode {
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
#[cfg(not(feature = "layout-cache"))]
|
||||
return self.node.layout(ctx, regions);
|
||||
|
||||
#[cfg(feature = "layout-cache")]
|
||||
ctx.layouts.get(self.hash, regions).unwrap_or_else(|| {
|
||||
ctx.level += 1;
|
||||
let frames = self.node.layout(ctx, regions);
|
||||
ctx.level -= 1;
|
||||
|
||||
let entry = FramesEntry::new(frames.clone(), ctx.level);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if !entry.check(regions) {
|
||||
eprintln!("node: {:#?}", self.node);
|
||||
eprintln!("regions: {:#?}", regions);
|
||||
eprintln!(
|
||||
"constraints: {:#?}",
|
||||
frames.iter().map(|c| c.cts).collect::<Vec<_>>()
|
||||
);
|
||||
panic!("constraints did not match regions they were created for");
|
||||
}
|
||||
|
||||
ctx.layouts.insert(self.hash, entry);
|
||||
frames
|
||||
})
|
||||
}
|
||||
|
||||
fn pack(self) -> BlockNode
|
||||
where
|
||||
Self: Sized + Hash + 'static,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for BlockNode {
|
||||
fn hash<H: Hasher>(&self, _state: &mut H) {
|
||||
#[cfg(feature = "layout-cache")]
|
||||
_state.write_u64(self.hash);
|
||||
#[cfg(not(feature = "layout-cache"))]
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for BlockNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.node.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Inline-level nodes are layouted as part of paragraph layout.
|
||||
///
|
||||
/// They only know the width and not the height of the paragraph's region and
|
||||
/// return only a single frame.
|
||||
pub trait InlineLevel: Debug {
|
||||
/// Layout the node into a frame.
|
||||
fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame;
|
||||
|
||||
/// Convert to a packed inline-level node.
|
||||
fn pack(self) -> InlineNode
|
||||
where
|
||||
Self: Sized + Hash + 'static,
|
||||
{
|
||||
InlineNode {
|
||||
#[cfg(feature = "layout-cache")]
|
||||
hash: hash_node(&self),
|
||||
node: Rc::new(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A packed [inline-level](InlineLevel) layouting node with precomputed hash.
|
||||
#[derive(Clone)]
|
||||
pub struct InlineNode {
|
||||
node: Rc<dyn InlineLevel>,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
impl InlineLevel for InlineNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame {
|
||||
self.node.layout(ctx, space, base)
|
||||
}
|
||||
|
||||
fn pack(self) -> InlineNode
|
||||
where
|
||||
Self: Sized + Hash + 'static,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for InlineNode {
|
||||
fn hash<H: Hasher>(&self, _state: &mut H) {
|
||||
#[cfg(feature = "layout-cache")]
|
||||
_state.write_u64(self.hash);
|
||||
#[cfg(not(feature = "layout-cache"))]
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for InlineNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.node.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash a node alongside its type id.
|
||||
#[cfg(feature = "layout-cache")]
|
||||
fn hash_node(node: &(impl Hash + 'static)) -> u64 {
|
||||
use std::any::Any;
|
||||
let mut state = fxhash::FxHasher64::default();
|
||||
node.type_id().hash(&mut state);
|
||||
node.hash(&mut state);
|
||||
state.finish()
|
||||
}
|
@ -1,41 +1,22 @@
|
||||
//! Layouting.
|
||||
|
||||
mod constraints;
|
||||
mod deco;
|
||||
mod frame;
|
||||
mod grid;
|
||||
mod image;
|
||||
#[cfg(feature = "layout-cache")]
|
||||
mod incremental;
|
||||
mod pad;
|
||||
mod par;
|
||||
mod levels;
|
||||
mod regions;
|
||||
mod shape;
|
||||
mod stack;
|
||||
mod text;
|
||||
|
||||
pub use self::image::*;
|
||||
pub use constraints::*;
|
||||
pub use deco::*;
|
||||
pub use frame::*;
|
||||
pub use grid::*;
|
||||
#[cfg(feature = "layout-cache")]
|
||||
pub use incremental::*;
|
||||
pub use pad::*;
|
||||
pub use par::*;
|
||||
pub use levels::*;
|
||||
pub use regions::*;
|
||||
pub use shape::*;
|
||||
pub use stack::*;
|
||||
pub use text::*;
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::font::FontStore;
|
||||
use crate::geom::*;
|
||||
use crate::frame::Frame;
|
||||
use crate::image::ImageStore;
|
||||
use crate::util::OptionExt;
|
||||
use crate::Context;
|
||||
|
||||
/// Layout a page-level node into a collection of frames.
|
||||
@ -74,205 +55,3 @@ impl<'a> LayoutContext<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Page-level nodes directly produce frames representing pages.
|
||||
///
|
||||
/// Such nodes create their own regions instead of being supplied with them from
|
||||
/// some parent.
|
||||
pub trait PageLevel: Debug {
|
||||
/// Layout the node, producing one frame per page.
|
||||
fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>>;
|
||||
}
|
||||
|
||||
/// Layouts its children onto one or multiple pages.
|
||||
#[derive(Debug)]
|
||||
pub struct PageNode {
|
||||
/// The size of the page.
|
||||
pub size: Size,
|
||||
/// The node that produces the actual pages.
|
||||
pub child: BlockNode,
|
||||
}
|
||||
|
||||
impl PageLevel for PageNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
|
||||
// When one of the lengths is infinite the page fits its content along
|
||||
// that axis.
|
||||
let expand = self.size.to_spec().map(Length::is_finite);
|
||||
let regions = Regions::repeat(self.size, self.size, expand);
|
||||
self.child.layout(ctx, ®ions).into_iter().map(|c| c.item).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PageLevel for T
|
||||
where
|
||||
T: AsRef<[PageNode]> + Debug + ?Sized,
|
||||
{
|
||||
fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
|
||||
self.as_ref().iter().flat_map(|node| node.layout(ctx)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Block-level nodes can be layouted into a sequence of regions.
|
||||
///
|
||||
/// They return one frame per used region alongside constraints that define
|
||||
/// whether the result is reusable in other regions.
|
||||
pub trait BlockLevel: Debug {
|
||||
/// Layout the node into the given regions, producing constrained frames.
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>>;
|
||||
|
||||
/// Convert to a packed block-level node.
|
||||
fn pack(self) -> BlockNode
|
||||
where
|
||||
Self: Sized + Hash + 'static,
|
||||
{
|
||||
BlockNode {
|
||||
#[cfg(feature = "layout-cache")]
|
||||
hash: hash_node(&self),
|
||||
node: Rc::new(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A packed [block-level](BlockLevel) layouting node with precomputed hash.
|
||||
#[derive(Clone)]
|
||||
pub struct BlockNode {
|
||||
node: Rc<dyn BlockLevel>,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
impl BlockLevel for BlockNode {
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
#[cfg(not(feature = "layout-cache"))]
|
||||
return self.node.layout(ctx, regions);
|
||||
|
||||
#[cfg(feature = "layout-cache")]
|
||||
ctx.layouts.get(self.hash, regions).unwrap_or_else(|| {
|
||||
ctx.level += 1;
|
||||
let frames = self.node.layout(ctx, regions);
|
||||
ctx.level -= 1;
|
||||
|
||||
let entry = FramesEntry::new(frames.clone(), ctx.level);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if !entry.check(regions) {
|
||||
eprintln!("node: {:#?}", self.node);
|
||||
eprintln!("regions: {:#?}", regions);
|
||||
eprintln!(
|
||||
"constraints: {:#?}",
|
||||
frames.iter().map(|c| c.cts).collect::<Vec<_>>()
|
||||
);
|
||||
panic!("constraints did not match regions they were created for");
|
||||
}
|
||||
|
||||
ctx.layouts.insert(self.hash, entry);
|
||||
frames
|
||||
})
|
||||
}
|
||||
|
||||
fn pack(self) -> BlockNode
|
||||
where
|
||||
Self: Sized + Hash + 'static,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for BlockNode {
|
||||
fn hash<H: Hasher>(&self, _state: &mut H) {
|
||||
#[cfg(feature = "layout-cache")]
|
||||
_state.write_u64(self.hash);
|
||||
#[cfg(not(feature = "layout-cache"))]
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for BlockNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.node.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Inline-level nodes are layouted as part of paragraph layout.
|
||||
///
|
||||
/// They only know the width and not the height of the paragraph's region and
|
||||
/// return only a single frame.
|
||||
pub trait InlineLevel: Debug {
|
||||
/// Layout the node into a frame.
|
||||
fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame;
|
||||
|
||||
/// Convert to a packed inline-level node.
|
||||
fn pack(self) -> InlineNode
|
||||
where
|
||||
Self: Sized + Hash + 'static,
|
||||
{
|
||||
InlineNode {
|
||||
#[cfg(feature = "layout-cache")]
|
||||
hash: hash_node(&self),
|
||||
node: Rc::new(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A packed [inline-level](InlineLevel) layouting node with precomputed hash.
|
||||
#[derive(Clone)]
|
||||
pub struct InlineNode {
|
||||
node: Rc<dyn InlineLevel>,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
impl InlineLevel for InlineNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame {
|
||||
self.node.layout(ctx, space, base)
|
||||
}
|
||||
|
||||
fn pack(self) -> InlineNode
|
||||
where
|
||||
Self: Sized + Hash + 'static,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for InlineNode {
|
||||
fn hash<H: Hasher>(&self, _state: &mut H) {
|
||||
#[cfg(feature = "layout-cache")]
|
||||
_state.write_u64(self.hash);
|
||||
#[cfg(not(feature = "layout-cache"))]
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for InlineNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.node.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash a node alongside its type id.
|
||||
#[cfg(feature = "layout-cache")]
|
||||
fn hash_node(node: &(impl Hash + 'static)) -> u64 {
|
||||
use std::any::Any;
|
||||
let mut state = fxhash::FxHasher64::default();
|
||||
node.type_id().hash(&mut state);
|
||||
node.hash(&mut state);
|
||||
state.finish()
|
||||
}
|
||||
|
||||
/// 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),
|
||||
}
|
||||
|
@ -1,370 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
use std::ops::Range;
|
||||
|
||||
use rustybuzz::UnicodeBuffer;
|
||||
|
||||
use super::*;
|
||||
use crate::font::{Face, FaceId, FontVariant};
|
||||
use crate::geom::{Dir, Em, Length, Point, Size};
|
||||
use crate::style::TextStyle;
|
||||
use crate::util::SliceExt;
|
||||
|
||||
/// 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,
|
||||
);
|
||||
}
|
||||
|
||||
let (size, baseline) = measure(ctx, &glyphs, style);
|
||||
ShapedText {
|
||||
text,
|
||||
dir,
|
||||
style,
|
||||
size,
|
||||
baseline,
|
||||
glyphs: Cow::Owned(glyphs),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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]>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
frame
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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);
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
/// 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 });
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
self.glyphs[idx].safe_to_break.then(|| idx)
|
||||
}
|
||||
}
|
||||
|
||||
/// A visual side.
|
||||
enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(Size::new(width, top + bottom), top)
|
||||
}
|
@ -33,6 +33,7 @@ pub mod diag;
|
||||
pub mod eval;
|
||||
pub mod export;
|
||||
pub mod font;
|
||||
pub mod frame;
|
||||
pub mod geom;
|
||||
pub mod image;
|
||||
pub mod layout;
|
||||
@ -49,10 +50,11 @@ use std::rc::Rc;
|
||||
use crate::diag::TypResult;
|
||||
use crate::eval::{Module, Scope};
|
||||
use crate::font::FontStore;
|
||||
use crate::frame::Frame;
|
||||
use crate::image::ImageStore;
|
||||
use crate::layout::PageNode;
|
||||
#[cfg(feature = "layout-cache")]
|
||||
use crate::layout::{EvictionPolicy, LayoutCache};
|
||||
use crate::layout::{Frame, PageNode};
|
||||
use crate::loading::Loader;
|
||||
use crate::source::{SourceId, SourceStore};
|
||||
use crate::style::Style;
|
||||
|
51
src/library/align.rs
Normal file
51
src/library/align.rs
Normal file
@ -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
|
||||
})
|
||||
}
|
27
src/library/container.rs
Normal file
27
src/library/container.rs
Normal file
@ -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)
|
||||
})))
|
||||
}
|
@ -1,6 +1,51 @@
|
||||
use super::*;
|
||||
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 {
|
@ -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()),
|
||||
}))
|
||||
}
|
@ -1,4 +1,55 @@
|
||||
use super::*;
|
||||
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)]
|
@ -1,6 +1,30 @@
|
||||
use super::*;
|
||||
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 {
|
@ -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(),
|
||||
}
|
||||
})))
|
||||
}
|
@ -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 {
|
||||
|
@ -1,4 +1,28 @@
|
||||
use super::*;
|
||||
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)]
|
76
src/library/page.rs
Normal file
76
src/library/page.rs
Normal file
@ -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))
|
||||
}
|
@ -5,11 +5,63 @@ use itertools::Either;
|
||||
use unicode_bidi::{BidiInfo, Level};
|
||||
use xi_unicode::LineBreakIterator;
|
||||
|
||||
use super::*;
|
||||
use super::prelude::*;
|
||||
use super::{shape, Decoration, ShapedText, Spacing};
|
||||
use crate::style::TextStyle;
|
||||
use crate::util::{EcoString, RangeExt, SliceExt};
|
||||
|
||||
type Range = std::ops::Range<usize>;
|
||||
/// `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)]
|
||||
@ -104,6 +156,8 @@ impl Debug for ParChild {
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
@ -1,8 +1,87 @@
|
||||
use std::f64::consts::SQRT_2;
|
||||
|
||||
use super::*;
|
||||
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 {
|
24
src/library/spacing.rs
Normal file
24
src/library/spacing.rs
Normal file
@ -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),
|
||||
}
|
@ -1,6 +1,54 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use super::*;
|
||||
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)]
|
@ -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();
|
||||
|
||||
if let Some(spacing) = spacing {
|
||||
par.spacing = spacing;
|
||||
}
|
||||
|
||||
if let Some(leading) = leading {
|
||||
par.leading = leading;
|
||||
}
|
||||
});
|
||||
|
||||
ctx.template.parbreak();
|
||||
|
||||
Ok(Value::None)
|
||||
let (size, baseline) = measure(ctx, &glyphs, style);
|
||||
ShapedText {
|
||||
text,
|
||||
dir,
|
||||
style,
|
||||
size,
|
||||
baseline,
|
||||
glyphs: Cow::Owned(glyphs),
|
||||
}
|
||||
}
|
||||
|
||||
/// `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");
|
||||
/// 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]>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
frame
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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);
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
/// 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 });
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
self.glyphs[idx].safe_to_break.then(|| idx)
|
||||
}
|
||||
}
|
||||
|
||||
/// A visual side.
|
||||
enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
iso.as_deref().map(lang_dir)
|
||||
};
|
||||
|
||||
if let Some(dir) = dir {
|
||||
ctx.template.modify(move |style| style.dir = dir);
|
||||
}
|
||||
// Remember the id if this the first available face since we use that one to
|
||||
// shape tofus.
|
||||
first_face.get_or_insert(face_id);
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// `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
|
||||
// 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!(),
|
||||
});
|
||||
|
||||
Ok(Value::Template(body.decorate(Decoration::Link(url.into()))))
|
||||
// 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);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(Size::new(width, top + bottom), top)
|
||||
}
|
||||
|
44
src/library/transform.rs
Normal file
44
src/library/transform.rs
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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> {
|
||||
|
@ -12,13 +12,14 @@ use walkdir::WalkDir;
|
||||
use typst::diag::Error;
|
||||
use typst::eval::Value;
|
||||
use typst::font::Face;
|
||||
use typst::frame::{Element, Frame, Geometry, Text};
|
||||
use typst::geom::{
|
||||
self, Color, Length, Paint, PathElement, Point, RgbaColor, Sides, Size,
|
||||
};
|
||||
use typst::image::Image;
|
||||
use typst::layout::layout;
|
||||
#[cfg(feature = "layout-cache")]
|
||||
use typst::layout::PageNode;
|
||||
use typst::layout::{layout, Element, Frame, Geometry, Text};
|
||||
use typst::loading::FsLoader;
|
||||
use typst::parse::Scanner;
|
||||
use typst::source::SourceFile;
|
||||
|
Loading…
x
Reference in New Issue
Block a user