mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Refactor alignments & directions 📐
- Adds lang function - Refactors execution context - Adds StackChild and ParChild enums
This commit is contained in:
parent
e8057a5385
commit
76fc4cca62
@ -39,7 +39,6 @@ pub fn eval(env: &mut Env, tree: &Tree, scope: &Scope) -> Pass<NodeMap> {
|
||||
pub type NodeMap = HashMap<*const Node, Value>;
|
||||
|
||||
/// The context for evaluation.
|
||||
#[derive(Debug)]
|
||||
pub struct EvalContext<'a> {
|
||||
/// The environment from which resources are gathered.
|
||||
pub env: &'a mut Env,
|
||||
|
@ -4,15 +4,14 @@ use super::{Exec, FontFamily, State};
|
||||
use crate::diag::{Diag, DiagSet, Pass};
|
||||
use crate::env::Env;
|
||||
use crate::eval::TemplateValue;
|
||||
use crate::geom::{Dir, Gen, Linear, Sides, Size};
|
||||
use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size};
|
||||
use crate::layout::{
|
||||
Node, PadNode, PageRun, ParNode, SpacingNode, StackNode, TextNode, Tree,
|
||||
AnyNode, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, TextNode, Tree,
|
||||
};
|
||||
use crate::parse::{is_newline, Scanner};
|
||||
use crate::syntax::{Span, Spanned};
|
||||
use crate::syntax::Span;
|
||||
|
||||
/// The context for execution.
|
||||
#[derive(Debug)]
|
||||
pub struct ExecContext<'a> {
|
||||
/// The environment from which resources are gathered.
|
||||
pub env: &'a mut Env,
|
||||
@ -22,13 +21,11 @@ pub struct ExecContext<'a> {
|
||||
pub diags: DiagSet,
|
||||
/// The tree of finished page runs.
|
||||
tree: Tree,
|
||||
/// Metrics of the active page.
|
||||
page: Option<PageInfo>,
|
||||
/// The content of the active stack. This may be the top-level stack for the
|
||||
/// page or a lower one created by [`exec`](Self::exec).
|
||||
stack: StackNode,
|
||||
/// The content of the active paragraph.
|
||||
par: ParNode,
|
||||
/// When we are building the top-level stack, this contains metrics of the
|
||||
/// page. While building a group stack through `exec_group`, this is `None`.
|
||||
page: Option<PageBuilder>,
|
||||
/// The currently built stack of paragraphs.
|
||||
stack: StackBuilder,
|
||||
}
|
||||
|
||||
impl<'a> ExecContext<'a> {
|
||||
@ -38,9 +35,8 @@ impl<'a> ExecContext<'a> {
|
||||
env,
|
||||
diags: DiagSet::new(),
|
||||
tree: Tree { runs: vec![] },
|
||||
page: Some(PageInfo::new(&state, true)),
|
||||
stack: StackNode::new(&state),
|
||||
par: ParNode::new(&state),
|
||||
page: Some(PageBuilder::new(&state, true)),
|
||||
stack: StackBuilder::new(&state),
|
||||
state,
|
||||
}
|
||||
}
|
||||
@ -50,45 +46,23 @@ impl<'a> ExecContext<'a> {
|
||||
self.diags.insert(diag);
|
||||
}
|
||||
|
||||
/// Set the directions.
|
||||
///
|
||||
/// Produces an error if the axes aligned.
|
||||
pub fn set_dirs(&mut self, new: Gen<Option<Spanned<Dir>>>) {
|
||||
let dirs = Gen::new(
|
||||
new.main.map(|s| s.v).unwrap_or(self.state.dirs.main),
|
||||
new.cross.map(|s| s.v).unwrap_or(self.state.dirs.cross),
|
||||
);
|
||||
|
||||
if dirs.main.axis() != dirs.cross.axis() {
|
||||
self.state.dirs = dirs;
|
||||
} else {
|
||||
for dir in new.main.iter().chain(new.cross.iter()) {
|
||||
self.diag(error!(dir.span, "aligned axis"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the font to monospace.
|
||||
pub fn set_monospace(&mut self) {
|
||||
let families = self.state.font.families_mut();
|
||||
families.list.insert(0, FontFamily::Monospace);
|
||||
}
|
||||
|
||||
/// Push a layout node into the active paragraph.
|
||||
///
|
||||
/// Spacing nodes will be handled according to their
|
||||
/// [`softness`](SpacingNode::softness).
|
||||
pub fn push(&mut self, node: impl Into<Node>) {
|
||||
push(&mut self.par.children, node.into());
|
||||
}
|
||||
/// Execute a template and return the result as a stack node.
|
||||
pub fn exec_group(&mut self, template: &TemplateValue) -> StackNode {
|
||||
let snapshot = self.state.clone();
|
||||
let page = self.page.take();
|
||||
let stack = mem::replace(&mut self.stack, StackBuilder::new(&self.state));
|
||||
|
||||
/// Push a word space into the active paragraph.
|
||||
pub fn push_space(&mut self) {
|
||||
let em = self.state.font.resolve_size();
|
||||
self.push(SpacingNode {
|
||||
amount: self.state.par.word_spacing.resolve(em),
|
||||
softness: 1,
|
||||
});
|
||||
template.exec(self);
|
||||
|
||||
self.state = snapshot;
|
||||
self.page = page;
|
||||
mem::replace(&mut self.stack, stack).build()
|
||||
}
|
||||
|
||||
/// Push text into the active paragraph.
|
||||
@ -97,96 +71,85 @@ impl<'a> ExecContext<'a> {
|
||||
pub fn push_text(&mut self, text: &str) {
|
||||
let mut scanner = Scanner::new(text);
|
||||
let mut line = String::new();
|
||||
let push = |this: &mut Self, text| {
|
||||
let props = this.state.font.resolve_props();
|
||||
let node = TextNode { text, props };
|
||||
let align = this.state.aligns.cross;
|
||||
this.stack.par.folder.push(ParChild::Text(node, align))
|
||||
};
|
||||
|
||||
while let Some(c) = scanner.eat_merging_crlf() {
|
||||
if is_newline(c) {
|
||||
self.push(TextNode::new(mem::take(&mut line), &self.state));
|
||||
push(self, mem::take(&mut line));
|
||||
self.push_linebreak();
|
||||
} else {
|
||||
line.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
self.push(TextNode::new(line, &self.state));
|
||||
push(self, line);
|
||||
}
|
||||
|
||||
/// Push a word space.
|
||||
pub fn push_word_space(&mut self) {
|
||||
let em = self.state.font.resolve_size();
|
||||
let amount = self.state.par.word_spacing.resolve(em);
|
||||
self.push_spacing(GenAxis::Cross, amount, 1);
|
||||
}
|
||||
|
||||
/// Apply a forced line break.
|
||||
pub fn push_linebreak(&mut self) {
|
||||
let em = self.state.font.resolve_size();
|
||||
self.push_into_stack(SpacingNode {
|
||||
amount: self.state.par.leading.resolve(em),
|
||||
softness: 2,
|
||||
});
|
||||
let amount = self.state.par.leading.resolve(em);
|
||||
self.push_spacing(GenAxis::Main, amount, 2);
|
||||
}
|
||||
|
||||
/// Apply a forced paragraph break.
|
||||
pub fn push_parbreak(&mut self) {
|
||||
let em = self.state.font.resolve_size();
|
||||
self.push_into_stack(SpacingNode {
|
||||
amount: self.state.par.spacing.resolve(em),
|
||||
softness: 1,
|
||||
});
|
||||
let amount = self.state.par.spacing.resolve(em);
|
||||
self.push_spacing(GenAxis::Main, amount, 1);
|
||||
}
|
||||
|
||||
/// Push a node directly into the stack above the paragraph. This finishes
|
||||
/// the active paragraph and starts a new one.
|
||||
pub fn push_into_stack(&mut self, node: impl Into<Node>) {
|
||||
self.finish_par();
|
||||
push(&mut self.stack.children, node.into());
|
||||
}
|
||||
|
||||
/// Execute a template and return the result as a stack node.
|
||||
pub fn exec(&mut self, template: &TemplateValue) -> StackNode {
|
||||
let page = self.page.take();
|
||||
let stack = mem::replace(&mut self.stack, StackNode::new(&self.state));
|
||||
let par = mem::replace(&mut self.par, ParNode::new(&self.state));
|
||||
|
||||
template.exec(self);
|
||||
let result = self.finish_stack();
|
||||
|
||||
self.page = page;
|
||||
self.stack = stack;
|
||||
self.par = par;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Finish the active paragraph.
|
||||
fn finish_par(&mut self) {
|
||||
let mut par = mem::replace(&mut self.par, ParNode::new(&self.state));
|
||||
trim(&mut par.children);
|
||||
|
||||
if !par.children.is_empty() {
|
||||
self.stack.children.push(par.into());
|
||||
/// Push spacing into paragraph or stack depending on `axis`.
|
||||
///
|
||||
/// The `softness` configures how the spacing interacts with surrounding
|
||||
/// spacing.
|
||||
pub fn push_spacing(&mut self, axis: GenAxis, amount: Length, softness: u8) {
|
||||
match axis {
|
||||
GenAxis::Main => {
|
||||
let spacing = StackChild::Spacing(amount);
|
||||
self.stack.finish_par(&self.state);
|
||||
self.stack.folder.push_soft(spacing, softness);
|
||||
}
|
||||
GenAxis::Cross => {
|
||||
let spacing = ParChild::Spacing(amount);
|
||||
self.stack.par.folder.push_soft(spacing, softness);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish the active stack.
|
||||
fn finish_stack(&mut self) -> StackNode {
|
||||
self.finish_par();
|
||||
/// Push any node into the active paragraph.
|
||||
pub fn push_into_par(&mut self, node: impl Into<AnyNode>) {
|
||||
let align = self.state.aligns.cross;
|
||||
self.stack.par.folder.push(ParChild::Any(node.into(), align));
|
||||
}
|
||||
|
||||
let mut stack = mem::replace(&mut self.stack, StackNode::new(&self.state));
|
||||
trim(&mut stack.children);
|
||||
|
||||
stack
|
||||
/// Push any node directly into the stack of paragraphs.
|
||||
///
|
||||
/// This finishes the active paragraph and starts a new one.
|
||||
pub fn push_into_stack(&mut self, node: impl Into<AnyNode>) {
|
||||
let aligns = self.state.aligns;
|
||||
self.stack.finish_par(&self.state);
|
||||
self.stack.folder.push(StackChild::Any(node.into(), aligns));
|
||||
}
|
||||
|
||||
/// Finish the active page.
|
||||
pub fn finish_page(&mut self, keep: bool, hard: bool, source: Span) {
|
||||
if let Some(info) = &mut self.page {
|
||||
let info = mem::replace(info, PageInfo::new(&self.state, hard));
|
||||
let stack = self.finish_stack();
|
||||
|
||||
if !stack.children.is_empty() || (keep && info.hard) {
|
||||
self.tree.runs.push(PageRun {
|
||||
size: info.size,
|
||||
child: PadNode {
|
||||
padding: info.padding,
|
||||
child: stack.into(),
|
||||
}
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
if let Some(builder) = &mut self.page {
|
||||
let page = mem::replace(builder, PageBuilder::new(&self.state, hard));
|
||||
let stack = mem::replace(&mut self.stack, StackBuilder::new(&self.state));
|
||||
self.tree.runs.extend(page.build(stack.build(), keep));
|
||||
} else {
|
||||
self.diag(error!(source, "cannot modify page from here"));
|
||||
}
|
||||
@ -200,44 +163,13 @@ impl<'a> ExecContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a node into a list, taking care of spacing softness.
|
||||
fn push(nodes: &mut Vec<Node>, node: Node) {
|
||||
if let Node::Spacing(spacing) = node {
|
||||
if nodes.is_empty() && spacing.softness > 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(&Node::Spacing(other)) = nodes.last() {
|
||||
if spacing.softness > 0 && spacing.softness >= other.softness {
|
||||
return;
|
||||
}
|
||||
|
||||
if spacing.softness < other.softness {
|
||||
nodes.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
/// Remove trailing soft spacing from a node list.
|
||||
fn trim(nodes: &mut Vec<Node>) {
|
||||
if let Some(&Node::Spacing(spacing)) = nodes.last() {
|
||||
if spacing.softness > 0 {
|
||||
nodes.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PageInfo {
|
||||
struct PageBuilder {
|
||||
size: Size,
|
||||
padding: Sides<Linear>,
|
||||
hard: bool,
|
||||
}
|
||||
|
||||
impl PageInfo {
|
||||
impl PageBuilder {
|
||||
fn new(state: &State, hard: bool) -> Self {
|
||||
Self {
|
||||
size: state.page.size,
|
||||
@ -245,37 +177,119 @@ impl PageInfo {
|
||||
hard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StackNode {
|
||||
fn new(state: &State) -> Self {
|
||||
Self {
|
||||
dirs: state.dirs,
|
||||
aligns: state.aligns,
|
||||
children: vec![],
|
||||
}
|
||||
fn build(self, child: StackNode, keep: bool) -> Option<PageRun> {
|
||||
let Self { size, padding, hard } = self;
|
||||
(!child.children.is_empty() || (keep && hard)).then(|| PageRun {
|
||||
size,
|
||||
child: PadNode { padding, child: child.into() }.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ParNode {
|
||||
struct StackBuilder {
|
||||
dirs: Gen<Dir>,
|
||||
folder: SoftFolder<StackChild>,
|
||||
par: ParBuilder,
|
||||
}
|
||||
|
||||
impl StackBuilder {
|
||||
fn new(state: &State) -> Self {
|
||||
Self {
|
||||
dirs: Gen::new(Dir::TTB, state.lang.dir),
|
||||
folder: SoftFolder::new(),
|
||||
par: ParBuilder::new(state),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_par(&mut self, state: &State) {
|
||||
let par = mem::replace(&mut self.par, ParBuilder::new(state));
|
||||
self.folder.extend(par.build());
|
||||
}
|
||||
|
||||
fn build(self) -> StackNode {
|
||||
let Self { dirs, mut folder, par } = self;
|
||||
folder.extend(par.build());
|
||||
StackNode { dirs, children: folder.finish() }
|
||||
}
|
||||
}
|
||||
|
||||
struct ParBuilder {
|
||||
aligns: Gen<Align>,
|
||||
dir: Dir,
|
||||
line_spacing: Length,
|
||||
folder: SoftFolder<ParChild>,
|
||||
}
|
||||
|
||||
impl ParBuilder {
|
||||
fn new(state: &State) -> Self {
|
||||
let em = state.font.resolve_size();
|
||||
Self {
|
||||
dirs: state.dirs,
|
||||
aligns: state.aligns,
|
||||
dir: state.lang.dir,
|
||||
line_spacing: state.par.leading.resolve(em),
|
||||
children: vec![],
|
||||
folder: SoftFolder::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build(self) -> Option<StackChild> {
|
||||
let Self { aligns, dir, line_spacing, folder } = self;
|
||||
let children = folder.finish();
|
||||
(!children.is_empty()).then(|| {
|
||||
let node = ParNode { dir, line_spacing, children };
|
||||
StackChild::Any(node.into(), aligns)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TextNode {
|
||||
fn new(text: String, state: &State) -> Self {
|
||||
Self {
|
||||
text,
|
||||
dir: state.dirs.cross,
|
||||
aligns: state.aligns,
|
||||
props: state.font.resolve_props(),
|
||||
/// This is used to remove leading and trailing word/line/paragraph spacing
|
||||
/// as well as collapse sequences of spacings into just one.
|
||||
struct SoftFolder<N> {
|
||||
nodes: Vec<N>,
|
||||
last: Last<N>,
|
||||
}
|
||||
|
||||
enum Last<N> {
|
||||
None,
|
||||
Hard,
|
||||
Soft(N, u8),
|
||||
}
|
||||
|
||||
impl<N> SoftFolder<N> {
|
||||
fn new() -> Self {
|
||||
Self { nodes: vec![], last: Last::Hard }
|
||||
}
|
||||
|
||||
fn push(&mut self, node: N) {
|
||||
let last = mem::replace(&mut self.last, Last::None);
|
||||
if let Last::Soft(soft, _) = last {
|
||||
self.nodes.push(soft);
|
||||
}
|
||||
self.nodes.push(node);
|
||||
}
|
||||
|
||||
fn push_soft(&mut self, node: N, softness: u8) {
|
||||
if softness == 0 {
|
||||
self.last = Last::Hard;
|
||||
self.nodes.push(node);
|
||||
} else {
|
||||
match self.last {
|
||||
Last::Hard => {}
|
||||
Last::Soft(_, other) if softness >= other => {}
|
||||
_ => self.last = Last::Soft(node, softness),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Vec<N> {
|
||||
self.nodes
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> Extend<N> for SoftFolder<N> {
|
||||
fn extend<T: IntoIterator<Item = N>>(&mut self, iter: T) {
|
||||
for elem in iter {
|
||||
self.push(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ impl ExecWithMap for Node {
|
||||
fn exec_with_map(&self, ctx: &mut ExecContext, map: &NodeMap) {
|
||||
match self {
|
||||
Node::Text(text) => ctx.push_text(text),
|
||||
Node::Space => ctx.push_space(),
|
||||
Node::Space => ctx.push_word_space(),
|
||||
_ => map[&(self as *const _)].exec(ctx),
|
||||
}
|
||||
}
|
||||
|
@ -12,30 +12,43 @@ use crate::paper::{Paper, PaperClass, PAPER_A4};
|
||||
/// The evaluation state.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct State {
|
||||
/// The current directions along which layouts are placed in their parents.
|
||||
pub dirs: LayoutDirs,
|
||||
/// The current alignments of layouts in their parents.
|
||||
pub aligns: LayoutAligns,
|
||||
/// The current language-related settings.
|
||||
pub lang: LangState,
|
||||
/// The current page settings.
|
||||
pub page: PageState,
|
||||
/// The current paragraph settings.
|
||||
pub par: ParState,
|
||||
/// The current font settings.
|
||||
pub font: FontState,
|
||||
/// The current alignments of layouts in their parents.
|
||||
pub aligns: Gen<Align>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dirs: LayoutDirs::new(Dir::TTB, Dir::LTR),
|
||||
aligns: LayoutAligns::new(Align::Start, Align::Start),
|
||||
lang: LangState::default(),
|
||||
page: PageState::default(),
|
||||
par: ParState::default(),
|
||||
font: FontState::default(),
|
||||
aligns: Gen::new(Align::Start, Align::Start),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines language properties.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct LangState {
|
||||
/// The direction for text and other inline objects.
|
||||
pub dir: Dir,
|
||||
}
|
||||
|
||||
impl Default for LangState {
|
||||
fn default() -> Self {
|
||||
Self { dir: Dir::LTR }
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines page properties.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct PageState {
|
||||
|
@ -1,8 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
/// The alignments of a layout in its parent.
|
||||
pub type LayoutAligns = Gen<Align>;
|
||||
|
||||
/// Where to align something along a directed axis.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum Align {
|
||||
|
@ -1,8 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
/// The directions along which layouts are placed in their parent.
|
||||
pub type LayoutDirs = Gen<Dir>;
|
||||
|
||||
/// The four directions into which content can be laid out.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum Dir {
|
||||
|
@ -50,8 +50,8 @@ impl<T> Get<GenAxis> for Gen<T> {
|
||||
impl<T> Switch for Gen<T> {
|
||||
type Other = Spec<T>;
|
||||
|
||||
fn switch(self, dirs: LayoutDirs) -> Self::Other {
|
||||
match dirs.main.axis() {
|
||||
fn switch(self, main: SpecAxis) -> Self::Other {
|
||||
match main {
|
||||
SpecAxis::Horizontal => Spec::new(self.main, self.cross),
|
||||
SpecAxis::Vertical => Spec::new(self.cross, self.main),
|
||||
}
|
||||
@ -86,10 +86,10 @@ impl GenAxis {
|
||||
impl Switch for GenAxis {
|
||||
type Other = SpecAxis;
|
||||
|
||||
fn switch(self, dirs: LayoutDirs) -> Self::Other {
|
||||
fn switch(self, main: SpecAxis) -> Self::Other {
|
||||
match self {
|
||||
Self::Main => dirs.main.axis(),
|
||||
Self::Cross => dirs.cross.axis(),
|
||||
Self::Main => main,
|
||||
Self::Cross => main.other(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,6 @@ pub trait Switch {
|
||||
/// The type of the other version.
|
||||
type Other;
|
||||
|
||||
/// The other version of this type based on the current layouting
|
||||
/// directions.
|
||||
fn switch(self, dirs: LayoutDirs) -> Self::Other;
|
||||
/// The other version of this type based on the current main axis.
|
||||
fn switch(self, main: SpecAxis) -> Self::Other;
|
||||
}
|
||||
|
@ -45,8 +45,8 @@ impl Get<SpecAxis> for Point {
|
||||
impl Switch for Point {
|
||||
type Other = Gen<Length>;
|
||||
|
||||
fn switch(self, dirs: LayoutDirs) -> Self::Other {
|
||||
match dirs.main.axis() {
|
||||
fn switch(self, main: SpecAxis) -> Self::Other {
|
||||
match main {
|
||||
SpecAxis::Horizontal => Gen::new(self.x, self.y),
|
||||
SpecAxis::Vertical => Gen::new(self.y, self.x),
|
||||
}
|
||||
|
@ -74,8 +74,8 @@ impl Get<SpecAxis> for Size {
|
||||
impl Switch for Size {
|
||||
type Other = Gen<Length>;
|
||||
|
||||
fn switch(self, dirs: LayoutDirs) -> Self::Other {
|
||||
match dirs.main.axis() {
|
||||
fn switch(self, main: SpecAxis) -> Self::Other {
|
||||
match main {
|
||||
SpecAxis::Horizontal => Gen::new(self.width, self.height),
|
||||
SpecAxis::Vertical => Gen::new(self.height, self.width),
|
||||
}
|
||||
|
@ -66,8 +66,8 @@ impl<T> Get<SpecAxis> for Spec<T> {
|
||||
impl<T> Switch for Spec<T> {
|
||||
type Other = Gen<T>;
|
||||
|
||||
fn switch(self, dirs: LayoutDirs) -> Self::Other {
|
||||
match dirs.main.axis() {
|
||||
fn switch(self, main: SpecAxis) -> Self::Other {
|
||||
match main {
|
||||
SpecAxis::Horizontal => Gen::new(self.horizontal, self.vertical),
|
||||
SpecAxis::Vertical => Gen::new(self.vertical, self.horizontal),
|
||||
}
|
||||
@ -102,13 +102,8 @@ impl SpecAxis {
|
||||
impl Switch for SpecAxis {
|
||||
type Other = GenAxis;
|
||||
|
||||
fn switch(self, dirs: LayoutDirs) -> Self::Other {
|
||||
if self == dirs.main.axis() {
|
||||
GenAxis::Main
|
||||
} else {
|
||||
debug_assert_eq!(self, dirs.cross.axis());
|
||||
GenAxis::Cross
|
||||
}
|
||||
fn switch(self, main: SpecAxis) -> Self::Other {
|
||||
if self == main { GenAxis::Main } else { GenAxis::Cross }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ pub struct BackgroundNode {
|
||||
/// The background fill.
|
||||
pub fill: Fill,
|
||||
/// The child node to be filled.
|
||||
pub child: Node,
|
||||
pub child: AnyNode,
|
||||
}
|
||||
|
||||
/// The kind of shape to use as a background.
|
||||
@ -19,10 +19,10 @@ pub enum BackgroundShape {
|
||||
}
|
||||
|
||||
impl Layout for BackgroundNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
let mut fragment = self.child.layout(ctx, areas);
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
|
||||
let mut frames = self.child.layout(ctx, areas);
|
||||
|
||||
for frame in fragment.frames_mut() {
|
||||
for frame in &mut frames {
|
||||
let (point, shape) = match self.shape {
|
||||
BackgroundShape::Rect => (Point::ZERO, Shape::Rect(frame.size)),
|
||||
BackgroundShape::Ellipse => {
|
||||
@ -34,7 +34,7 @@ impl Layout for BackgroundNode {
|
||||
frame.elements.insert(0, (point, element));
|
||||
}
|
||||
|
||||
fragment
|
||||
frames
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,11 +12,11 @@ pub struct FixedNode {
|
||||
/// The resulting frame will satisfy `width = aspect * height`.
|
||||
pub aspect: Option<f64>,
|
||||
/// The child node whose size to fix.
|
||||
pub child: Node,
|
||||
pub child: AnyNode,
|
||||
}
|
||||
|
||||
impl Layout for FixedNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
|
||||
let Areas { current, full, .. } = areas;
|
||||
|
||||
let full = Size::new(
|
||||
|
@ -3,24 +3,21 @@
|
||||
mod background;
|
||||
mod fixed;
|
||||
mod frame;
|
||||
mod node;
|
||||
mod pad;
|
||||
mod par;
|
||||
mod shaping;
|
||||
mod spacing;
|
||||
mod stack;
|
||||
mod text;
|
||||
|
||||
pub use background::*;
|
||||
pub use fixed::*;
|
||||
pub use frame::*;
|
||||
pub use node::*;
|
||||
pub use pad::*;
|
||||
pub use par::*;
|
||||
pub use shaping::*;
|
||||
pub use spacing::*;
|
||||
pub use stack::*;
|
||||
pub use text::*;
|
||||
|
||||
use std::any::Any;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use crate::env::Env;
|
||||
use crate::geom::*;
|
||||
@ -51,25 +48,88 @@ pub struct PageRun {
|
||||
pub size: Size,
|
||||
/// The layout node that produces the actual pages (typically a
|
||||
/// [`StackNode`]).
|
||||
pub child: Node,
|
||||
pub child: AnyNode,
|
||||
}
|
||||
|
||||
impl PageRun {
|
||||
/// Layout the page run.
|
||||
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Frame> {
|
||||
let areas = Areas::repeat(self.size, Spec::uniform(Expand::Fill));
|
||||
self.child.layout(ctx, &areas).into_frames()
|
||||
self.child.layout(ctx, &areas)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a dynamic layouting node.
|
||||
pub struct AnyNode(Box<dyn Bounds>);
|
||||
|
||||
impl AnyNode {
|
||||
/// Create a new instance from any node that satisifies the required bounds.
|
||||
pub fn new<T>(any: T) -> Self
|
||||
where
|
||||
T: Layout + Debug + Clone + PartialEq + 'static,
|
||||
{
|
||||
Self(Box::new(any))
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout for AnyNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
|
||||
self.0.layout(ctx, areas)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for AnyNode {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.dyn_clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AnyNode {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.dyn_eq(other.0.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for AnyNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
trait Bounds: Layout + Debug + 'static {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn dyn_eq(&self, other: &dyn Bounds) -> bool;
|
||||
fn dyn_clone(&self) -> Box<dyn Bounds>;
|
||||
}
|
||||
|
||||
impl<T> Bounds for T
|
||||
where
|
||||
T: Layout + Debug + PartialEq + Clone + 'static,
|
||||
{
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn dyn_eq(&self, other: &dyn Bounds) -> bool {
|
||||
if let Some(other) = other.as_any().downcast_ref::<Self>() {
|
||||
self == other
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn Bounds> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout a node.
|
||||
pub trait Layout {
|
||||
/// Layout the node into the given areas.
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment;
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame>;
|
||||
}
|
||||
|
||||
/// The context for layouting.
|
||||
#[derive(Debug)]
|
||||
pub struct LayoutContext<'a> {
|
||||
/// The environment from which fonts are gathered.
|
||||
pub env: &'a mut Env,
|
||||
@ -183,44 +243,3 @@ impl Expand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of layouting a node.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Fragment {
|
||||
/// Spacing that should be added to the parent.
|
||||
Spacing(Length),
|
||||
/// A layout that should be added to and aligned in the parent.
|
||||
Frame(Frame, LayoutAligns),
|
||||
/// Multiple layouts.
|
||||
Frames(Vec<Frame>, LayoutAligns),
|
||||
}
|
||||
|
||||
impl Fragment {
|
||||
/// Return a reference to all frames contained in this variant (zero, one or
|
||||
/// arbitrarily many).
|
||||
pub fn frames(&self) -> &[Frame] {
|
||||
match self {
|
||||
Self::Spacing(_) => &[],
|
||||
Self::Frame(frame, _) => std::slice::from_ref(frame),
|
||||
Self::Frames(frames, _) => frames,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a mutable reference to all frames contained in this variant.
|
||||
pub fn frames_mut(&mut self) -> &mut [Frame] {
|
||||
match self {
|
||||
Self::Spacing(_) => &mut [],
|
||||
Self::Frame(frame, _) => std::slice::from_mut(frame),
|
||||
Self::Frames(frames, _) => frames,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all frames contained in this varian.
|
||||
pub fn into_frames(self) -> Vec<Frame> {
|
||||
match self {
|
||||
Self::Spacing(_) => vec![],
|
||||
Self::Frame(frame, _) => vec![frame],
|
||||
Self::Frames(frames, _) => frames,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,108 +0,0 @@
|
||||
use std::any::Any;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A self-contained layout node.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum Node {
|
||||
/// A text node.
|
||||
Text(TextNode),
|
||||
/// A spacing node.
|
||||
Spacing(SpacingNode),
|
||||
/// A dynamic node that can implement custom layouting behaviour.
|
||||
Any(AnyNode),
|
||||
}
|
||||
|
||||
impl Layout for Node {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
match self {
|
||||
Self::Spacing(spacing) => spacing.layout(ctx, areas),
|
||||
Self::Text(text) => text.layout(ctx, areas),
|
||||
Self::Any(any) => any.layout(ctx, areas),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Node {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Spacing(spacing) => spacing.fmt(f),
|
||||
Self::Text(text) => text.fmt(f),
|
||||
Self::Any(any) => any.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a dynamic layouting node.
|
||||
pub struct AnyNode(Box<dyn Bounds>);
|
||||
|
||||
impl AnyNode {
|
||||
/// Create a new instance from any node that satisifies the required bounds.
|
||||
pub fn new<T>(any: T) -> Self
|
||||
where
|
||||
T: Layout + Debug + Clone + PartialEq + 'static,
|
||||
{
|
||||
Self(Box::new(any))
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout for AnyNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
self.0.layout(ctx, areas)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for AnyNode {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.dyn_clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AnyNode {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.dyn_eq(other.0.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for AnyNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Node
|
||||
where
|
||||
T: Into<AnyNode>,
|
||||
{
|
||||
fn from(t: T) -> Self {
|
||||
Self::Any(t.into())
|
||||
}
|
||||
}
|
||||
|
||||
trait Bounds: Layout + Debug + 'static {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn dyn_eq(&self, other: &dyn Bounds) -> bool;
|
||||
fn dyn_clone(&self) -> Box<dyn Bounds>;
|
||||
}
|
||||
|
||||
impl<T> Bounds for T
|
||||
where
|
||||
T: Layout + Debug + PartialEq + Clone + 'static,
|
||||
{
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn dyn_eq(&self, other: &dyn Bounds) -> bool {
|
||||
if let Some(other) = other.as_any().downcast_ref::<Self>() {
|
||||
self == other
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn Bounds> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
@ -6,19 +6,17 @@ pub struct PadNode {
|
||||
/// The amount of padding.
|
||||
pub padding: Sides<Linear>,
|
||||
/// The child node whose sides to pad.
|
||||
pub child: Node,
|
||||
pub child: AnyNode,
|
||||
}
|
||||
|
||||
impl Layout for PadNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
|
||||
let areas = shrink(areas, self.padding);
|
||||
|
||||
let mut fragment = self.child.layout(ctx, &areas);
|
||||
for frame in fragment.frames_mut() {
|
||||
let mut frames = self.child.layout(ctx, &areas);
|
||||
for frame in &mut frames {
|
||||
pad(frame, self.padding);
|
||||
}
|
||||
|
||||
fragment
|
||||
frames
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,38 +1,63 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use super::*;
|
||||
use crate::exec::FontProps;
|
||||
|
||||
/// A node that arranges its children into a paragraph.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ParNode {
|
||||
/// The `main` and `cross` directions of this paragraph.
|
||||
///
|
||||
/// The children are placed in lines along the `cross` direction. The lines
|
||||
/// are stacked along the `main` direction.
|
||||
pub dirs: LayoutDirs,
|
||||
/// How to align this paragraph in its parent.
|
||||
pub aligns: LayoutAligns,
|
||||
/// The spacing to insert after each line.
|
||||
/// The inline direction of this paragraph.
|
||||
pub dir: Dir,
|
||||
/// The spacing to insert between each line.
|
||||
pub line_spacing: Length,
|
||||
/// The nodes to be arranged in a paragraph.
|
||||
pub children: Vec<Node>,
|
||||
pub children: Vec<ParChild>,
|
||||
}
|
||||
|
||||
/// A child of a paragraph node.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ParChild {
|
||||
/// Spacing between other nodes.
|
||||
Spacing(Length),
|
||||
/// A run of text and how to align it in its line.
|
||||
Text(TextNode, Align),
|
||||
/// Any child node and how to align it in its line.
|
||||
Any(AnyNode, Align),
|
||||
}
|
||||
|
||||
/// A consecutive, styled run of text.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct TextNode {
|
||||
/// The text.
|
||||
pub text: String,
|
||||
/// Properties used for font selection and layout.
|
||||
pub props: FontProps,
|
||||
}
|
||||
|
||||
impl Debug for TextNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "Text({})", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout for ParNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
let mut layouter = ParLayouter::new(self.dirs, self.line_spacing, areas.clone());
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
|
||||
let mut layouter = ParLayouter::new(self.dir, self.line_spacing, areas.clone());
|
||||
for child in &self.children {
|
||||
match child.layout(ctx, &layouter.areas) {
|
||||
Fragment::Spacing(spacing) => layouter.push_spacing(spacing),
|
||||
Fragment::Frame(frame, aligns) => {
|
||||
layouter.push_frame(frame, aligns.cross)
|
||||
match *child {
|
||||
ParChild::Spacing(amount) => layouter.push_spacing(amount),
|
||||
ParChild::Text(ref node, align) => {
|
||||
let frame = shape(&node.text, &mut ctx.env.fonts, &node.props);
|
||||
layouter.push_frame(frame, align);
|
||||
}
|
||||
Fragment::Frames(frames, aligns) => {
|
||||
for frame in frames {
|
||||
layouter.push_frame(frame, aligns.cross);
|
||||
ParChild::Any(ref node, align) => {
|
||||
for frame in node.layout(ctx, &layouter.areas) {
|
||||
layouter.push_frame(frame, align);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Fragment::Frames(layouter.finish(), self.aligns)
|
||||
layouter.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,30 +68,30 @@ impl From<ParNode> for AnyNode {
|
||||
}
|
||||
|
||||
struct ParLayouter {
|
||||
dirs: Gen<Dir>,
|
||||
main: SpecAxis,
|
||||
cross: SpecAxis,
|
||||
dirs: LayoutDirs,
|
||||
line_spacing: Length,
|
||||
areas: Areas,
|
||||
finished: Vec<Frame>,
|
||||
lines: Vec<(Length, Frame, Align)>,
|
||||
lines_size: Gen<Length>,
|
||||
stack: Vec<(Length, Frame, Align)>,
|
||||
stack_size: Gen<Length>,
|
||||
line: Vec<(Length, Frame, Align)>,
|
||||
line_size: Gen<Length>,
|
||||
line_ruler: Align,
|
||||
}
|
||||
|
||||
impl ParLayouter {
|
||||
fn new(dirs: LayoutDirs, line_spacing: Length, areas: Areas) -> Self {
|
||||
fn new(dir: Dir, line_spacing: Length, areas: Areas) -> Self {
|
||||
Self {
|
||||
main: dirs.main.axis(),
|
||||
cross: dirs.cross.axis(),
|
||||
dirs,
|
||||
dirs: Gen::new(Dir::TTB, dir),
|
||||
main: SpecAxis::Vertical,
|
||||
cross: SpecAxis::Horizontal,
|
||||
line_spacing,
|
||||
areas,
|
||||
finished: vec![],
|
||||
lines: vec![],
|
||||
lines_size: Gen::ZERO,
|
||||
stack: vec![],
|
||||
stack_size: Gen::ZERO,
|
||||
line: vec![],
|
||||
line_size: Gen::ZERO,
|
||||
line_ruler: Align::Start,
|
||||
@ -122,12 +147,10 @@ impl ParLayouter {
|
||||
}
|
||||
}
|
||||
|
||||
let size = frame.size.switch(self.dirs);
|
||||
|
||||
// A line can contain frames with different alignments. They exact
|
||||
// positions are calculated later depending on the alignments.
|
||||
let size = frame.size.switch(self.main);
|
||||
self.line.push((self.line_size.cross, frame, align));
|
||||
|
||||
self.line_size.cross += size.cross;
|
||||
self.line_size.main = self.line_size.main.max(size.main);
|
||||
self.line_ruler = align;
|
||||
@ -135,15 +158,15 @@ impl ParLayouter {
|
||||
|
||||
fn finish_line(&mut self) {
|
||||
let full_size = {
|
||||
let expand = self.areas.expand.switch(self.dirs);
|
||||
let full = self.areas.full.switch(self.dirs);
|
||||
let expand = self.areas.expand.get(self.cross);
|
||||
let full = self.areas.full.get(self.cross);
|
||||
Gen::new(
|
||||
self.line_size.main,
|
||||
expand.cross.resolve(self.line_size.cross, full.cross),
|
||||
expand.resolve(self.line_size.cross, full),
|
||||
)
|
||||
};
|
||||
|
||||
let mut output = Frame::new(full_size.switch(self.dirs).to_size());
|
||||
let mut output = Frame::new(full_size.switch(self.main).to_size());
|
||||
|
||||
for (before, frame, align) in std::mem::take(&mut self.line) {
|
||||
let child_cross_size = frame.size.get(self.cross);
|
||||
@ -158,49 +181,47 @@ impl ParLayouter {
|
||||
full_size.cross - before_with_self .. after
|
||||
});
|
||||
|
||||
let pos = Gen::new(Length::ZERO, cross).switch(self.dirs).to_point();
|
||||
let pos = Gen::new(Length::ZERO, cross).switch(self.main).to_point();
|
||||
output.push_frame(pos, frame);
|
||||
}
|
||||
|
||||
// Add line spacing, but only between lines.
|
||||
if !self.lines.is_empty() {
|
||||
self.lines_size.main += self.line_spacing;
|
||||
if !self.stack.is_empty() {
|
||||
self.stack_size.main += self.line_spacing;
|
||||
*self.areas.current.get_mut(self.main) -= self.line_spacing;
|
||||
}
|
||||
|
||||
// Update metrics of the whole paragraph.
|
||||
self.lines.push((self.lines_size.main, output, self.line_ruler));
|
||||
self.lines_size.main += full_size.main;
|
||||
self.lines_size.cross = self.lines_size.cross.max(full_size.cross);
|
||||
// Update metrics of paragraph and reset for line.
|
||||
self.stack.push((self.stack_size.main, output, self.line_ruler));
|
||||
self.stack_size.main += full_size.main;
|
||||
self.stack_size.cross = self.stack_size.cross.max(full_size.cross);
|
||||
*self.areas.current.get_mut(self.main) -= full_size.main;
|
||||
|
||||
// Reset metrics for the single line.
|
||||
self.line_size = Gen::ZERO;
|
||||
self.line_ruler = Align::Start;
|
||||
}
|
||||
|
||||
fn finish_area(&mut self) {
|
||||
let size = self.lines_size;
|
||||
let mut output = Frame::new(size.switch(self.dirs).to_size());
|
||||
let full_size = self.stack_size;
|
||||
let mut output = Frame::new(full_size.switch(self.main).to_size());
|
||||
|
||||
for (before, line, cross_align) in std::mem::take(&mut self.lines) {
|
||||
let child_size = line.size.switch(self.dirs);
|
||||
for (before, line, cross_align) in std::mem::take(&mut self.stack) {
|
||||
let child_size = line.size.switch(self.main);
|
||||
|
||||
// Position along the main axis.
|
||||
let main = if self.dirs.main.is_positive() {
|
||||
before
|
||||
} else {
|
||||
size.main - (before + child_size.main)
|
||||
full_size.main - (before + child_size.main)
|
||||
};
|
||||
|
||||
// Align along the cross axis.
|
||||
let cross = cross_align.resolve(if self.dirs.cross.is_positive() {
|
||||
Length::ZERO .. size.cross - child_size.cross
|
||||
Length::ZERO .. full_size.cross - child_size.cross
|
||||
} else {
|
||||
size.cross - child_size.cross .. Length::ZERO
|
||||
full_size.cross - child_size.cross .. Length::ZERO
|
||||
});
|
||||
|
||||
let pos = Gen::new(main, cross).switch(self.dirs).to_point();
|
||||
let pos = Gen::new(main, cross).switch(self.main).to_point();
|
||||
output.push_frame(pos, line);
|
||||
}
|
||||
|
||||
@ -208,7 +229,7 @@ impl ParLayouter {
|
||||
self.areas.next();
|
||||
|
||||
// Reset metrics for the whole paragraph.
|
||||
self.lines_size = Gen::ZERO;
|
||||
self.stack_size = Gen::ZERO;
|
||||
}
|
||||
|
||||
fn finish(mut self) -> Vec<Frame> {
|
||||
|
@ -1,35 +0,0 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A node that adds spacing to its parent.
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub struct SpacingNode {
|
||||
/// The amount of spacing to insert.
|
||||
pub amount: Length,
|
||||
/// Defines how spacing interacts with surrounding spacing.
|
||||
///
|
||||
/// Hard spacing (`softness = 0`) assures that a fixed amount of spacing
|
||||
/// will always be inserted. Soft spacing (`softness >= 1`) will be consumed
|
||||
/// by other spacing with lower softness and can be used to insert
|
||||
/// overridable spacing, e.g. between words or paragraphs.
|
||||
pub softness: u8,
|
||||
}
|
||||
|
||||
impl Layout for SpacingNode {
|
||||
fn layout(&self, _: &mut LayoutContext, _: &Areas) -> Fragment {
|
||||
Fragment::Spacing(self.amount)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SpacingNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "Spacing({}, {})", self.amount, self.softness)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SpacingNode> for Node {
|
||||
fn from(spacing: SpacingNode) -> Self {
|
||||
Self::Spacing(spacing)
|
||||
}
|
||||
}
|
@ -7,28 +7,34 @@ pub struct StackNode {
|
||||
///
|
||||
/// The children are stacked along the `main` direction. The `cross`
|
||||
/// direction is required for aligning the children.
|
||||
pub dirs: LayoutDirs,
|
||||
/// How to align this stack in its parent.
|
||||
pub aligns: LayoutAligns,
|
||||
pub dirs: Gen<Dir>,
|
||||
/// The nodes to be stacked.
|
||||
pub children: Vec<Node>,
|
||||
pub children: Vec<StackChild>,
|
||||
}
|
||||
|
||||
/// A child of a stack node.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum StackChild {
|
||||
/// Spacing between other nodes.
|
||||
Spacing(Length),
|
||||
/// Any child node and how to align it in the stack.
|
||||
Any(AnyNode, Gen<Align>),
|
||||
}
|
||||
|
||||
impl Layout for StackNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
|
||||
let mut layouter = StackLayouter::new(self.dirs, areas.clone());
|
||||
for child in &self.children {
|
||||
match child.layout(ctx, &layouter.areas) {
|
||||
Fragment::Spacing(spacing) => layouter.push_spacing(spacing),
|
||||
Fragment::Frame(frame, aligns) => layouter.push_frame(frame, aligns),
|
||||
Fragment::Frames(frames, aligns) => {
|
||||
for frame in frames {
|
||||
match *child {
|
||||
StackChild::Spacing(amount) => layouter.push_spacing(amount),
|
||||
StackChild::Any(ref node, aligns) => {
|
||||
for frame in node.layout(ctx, &layouter.areas) {
|
||||
layouter.push_frame(frame, aligns);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Fragment::Frames(layouter.finish(), self.aligns)
|
||||
layouter.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,24 +45,24 @@ impl From<StackNode> for AnyNode {
|
||||
}
|
||||
|
||||
struct StackLayouter {
|
||||
dirs: Gen<Dir>,
|
||||
main: SpecAxis,
|
||||
dirs: LayoutDirs,
|
||||
areas: Areas,
|
||||
finished: Vec<Frame>,
|
||||
frames: Vec<(Length, Frame, LayoutAligns)>,
|
||||
used: Gen<Length>,
|
||||
frames: Vec<(Length, Frame, Gen<Align>)>,
|
||||
size: Gen<Length>,
|
||||
ruler: Align,
|
||||
}
|
||||
|
||||
impl StackLayouter {
|
||||
fn new(dirs: LayoutDirs, areas: Areas) -> Self {
|
||||
fn new(dirs: Gen<Dir>, areas: Areas) -> Self {
|
||||
Self {
|
||||
main: dirs.main.axis(),
|
||||
dirs,
|
||||
main: dirs.main.axis(),
|
||||
areas,
|
||||
finished: vec![],
|
||||
frames: vec![],
|
||||
used: Gen::ZERO,
|
||||
size: Gen::ZERO,
|
||||
ruler: Align::Start,
|
||||
}
|
||||
}
|
||||
@ -65,10 +71,10 @@ impl StackLayouter {
|
||||
let main_rest = self.areas.current.get_mut(self.main);
|
||||
let capped = amount.min(*main_rest);
|
||||
*main_rest -= capped;
|
||||
self.used.main += capped;
|
||||
self.size.main += capped;
|
||||
}
|
||||
|
||||
fn push_frame(&mut self, frame: Frame, aligns: LayoutAligns) {
|
||||
fn push_frame(&mut self, frame: Frame, aligns: Gen<Align>) {
|
||||
if self.ruler > aligns.main {
|
||||
self.finish_area();
|
||||
}
|
||||
@ -82,21 +88,18 @@ impl StackLayouter {
|
||||
}
|
||||
}
|
||||
|
||||
let size = frame.size.switch(self.dirs);
|
||||
self.frames.push((self.used.main, frame, aligns));
|
||||
|
||||
*self.areas.current.get_mut(self.main) -= size.main;
|
||||
self.used.main += size.main;
|
||||
self.used.cross = self.used.cross.max(size.cross);
|
||||
let size = frame.size.switch(self.main);
|
||||
self.frames.push((self.size.main, frame, aligns));
|
||||
self.ruler = aligns.main;
|
||||
self.size.main += size.main;
|
||||
self.size.cross = self.size.cross.max(size.cross);
|
||||
*self.areas.current.get_mut(self.main) -= size.main;
|
||||
}
|
||||
|
||||
fn finish_area(&mut self) {
|
||||
let full_size = {
|
||||
let expand = self.areas.expand;
|
||||
let full = self.areas.full;
|
||||
let current = self.areas.current;
|
||||
let used = self.used.switch(self.dirs).to_size();
|
||||
let Areas { current, full, expand, .. } = self.areas;
|
||||
let used = self.size.switch(self.main).to_size();
|
||||
|
||||
let mut size = Size::new(
|
||||
expand.horizontal.resolve(used.width, full.width),
|
||||
@ -113,21 +116,21 @@ impl StackLayouter {
|
||||
size = Size::new(width, width / aspect);
|
||||
}
|
||||
|
||||
size.switch(self.dirs)
|
||||
size.switch(self.main)
|
||||
};
|
||||
|
||||
let mut output = Frame::new(full_size.switch(self.dirs).to_size());
|
||||
let mut output = Frame::new(full_size.switch(self.main).to_size());
|
||||
|
||||
for (before, frame, aligns) in std::mem::take(&mut self.frames) {
|
||||
let child_size = frame.size.switch(self.dirs);
|
||||
let child_size = frame.size.switch(self.main);
|
||||
|
||||
// Align along the main axis.
|
||||
let main = aligns.main.resolve(if self.dirs.main.is_positive() {
|
||||
let after_with_self = self.used.main - before;
|
||||
let after_with_self = self.size.main - before;
|
||||
before .. full_size.main - after_with_self
|
||||
} else {
|
||||
let before_with_self = before + child_size.main;
|
||||
let after = self.used.main - (before + child_size.main);
|
||||
let after = self.size.main - (before + child_size.main);
|
||||
full_size.main - before_with_self .. after
|
||||
});
|
||||
|
||||
@ -138,15 +141,14 @@ impl StackLayouter {
|
||||
full_size.cross - child_size.cross .. Length::ZERO
|
||||
});
|
||||
|
||||
let pos = Gen::new(main, cross).switch(self.dirs).to_point();
|
||||
let pos = Gen::new(main, cross).switch(self.main).to_point();
|
||||
output.push_frame(pos, frame);
|
||||
}
|
||||
|
||||
self.finished.push(output);
|
||||
|
||||
self.areas.next();
|
||||
self.used = Gen::ZERO;
|
||||
self.ruler = Align::Start;
|
||||
self.size = Gen::ZERO;
|
||||
}
|
||||
|
||||
fn finish(mut self) -> Vec<Frame> {
|
||||
|
@ -1,36 +0,0 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use super::*;
|
||||
use crate::exec::FontProps;
|
||||
|
||||
/// A consecutive, styled run of text.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct TextNode {
|
||||
/// The text direction.
|
||||
pub dir: Dir,
|
||||
/// How to align this text node in its parent.
|
||||
pub aligns: LayoutAligns,
|
||||
/// The text.
|
||||
pub text: String,
|
||||
/// Properties used for font selection and layout.
|
||||
pub props: FontProps,
|
||||
}
|
||||
|
||||
impl Layout for TextNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Fragment {
|
||||
let frame = shape(&self.text, &mut ctx.env.fonts, &self.props);
|
||||
Fragment::Frame(frame, self.aligns)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for TextNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "Text({})", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TextNode> for Node {
|
||||
fn from(text: TextNode) -> Self {
|
||||
Self::Text(text)
|
||||
}
|
||||
}
|
@ -6,11 +6,6 @@ use super::*;
|
||||
/// - Alignments: variadic, of type `alignment`.
|
||||
/// - Body: optional, of type `template`.
|
||||
///
|
||||
/// Which axis an alignment should apply to (main or cross) is inferred from
|
||||
/// either the argument itself (for anything other than `center`) or from the
|
||||
/// second argument if present, defaulting to the cross axis for a single
|
||||
/// `center` alignment.
|
||||
///
|
||||
/// # Named parameters
|
||||
/// - Horizontal alignment: `horizontal`, of type `alignment`.
|
||||
/// - Vertical alignment: `vertical`, of type `alignment`.
|
||||
@ -21,32 +16,44 @@ use super::*;
|
||||
///
|
||||
/// # Relevant types and constants
|
||||
/// - Type `alignment`
|
||||
/// - `start`
|
||||
/// - `center`
|
||||
/// - `end`
|
||||
/// - `left`
|
||||
/// - `right`
|
||||
/// - `top`
|
||||
/// - `bottom`
|
||||
/// - `center`
|
||||
pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let first = args.find(ctx);
|
||||
let second = args.find(ctx);
|
||||
let hor = args.get(ctx, "horizontal");
|
||||
let ver = args.get(ctx, "vertical");
|
||||
let first = args.find::<AlignValue>(ctx);
|
||||
let second = args.find::<AlignValue>(ctx);
|
||||
let mut horizontal = args.get::<AlignValue>(ctx, "horizontal");
|
||||
let mut vertical = args.get::<AlignValue>(ctx, "vertical");
|
||||
let body = args.find::<TemplateValue>(ctx);
|
||||
|
||||
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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Value::template("align", move |ctx| {
|
||||
let snapshot = ctx.state.clone();
|
||||
|
||||
let values = first
|
||||
.into_iter()
|
||||
.chain(second.into_iter())
|
||||
.map(|arg: Spanned<AlignValue>| (arg.v.axis(), arg))
|
||||
.chain(hor.into_iter().map(|arg| (Some(SpecAxis::Horizontal), arg)))
|
||||
.chain(ver.into_iter().map(|arg| (Some(SpecAxis::Vertical), arg)));
|
||||
if let Some(horizontal) = horizontal {
|
||||
ctx.state.aligns.cross = horizontal.to_align(ctx.state.lang.dir);
|
||||
}
|
||||
|
||||
apply(ctx, values);
|
||||
|
||||
if ctx.state.aligns.main != snapshot.aligns.main {
|
||||
ctx.push_linebreak();
|
||||
if let Some(vertical) = vertical {
|
||||
ctx.state.aligns.main = vertical.to_align(Dir::TTB);
|
||||
if ctx.state.aligns.main != snapshot.aligns.main {
|
||||
ctx.push_linebreak();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(body) = &body {
|
||||
@ -56,109 +63,48 @@ pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
/// Deduplicate and apply the alignments.
|
||||
fn apply(
|
||||
ctx: &mut ExecContext,
|
||||
values: impl Iterator<Item = (Option<SpecAxis>, Spanned<AlignValue>)>,
|
||||
) {
|
||||
let mut had = Gen::uniform(false);
|
||||
let mut had_center = false;
|
||||
|
||||
for (axis, Spanned { v: arg, span }) in values {
|
||||
// Check whether we know which axis this alignment belongs to.
|
||||
if let Some(axis) = axis {
|
||||
// We know the axis.
|
||||
let gen_axis = axis.switch(ctx.state.dirs);
|
||||
let gen_align = arg.switch(ctx.state.dirs);
|
||||
|
||||
if arg.axis().map_or(false, |a| a != axis) {
|
||||
ctx.diag(error!(span, "invalid alignment for {} axis", axis));
|
||||
} else if had.get(gen_axis) {
|
||||
ctx.diag(error!(span, "duplicate alignment for {} axis", axis));
|
||||
} else {
|
||||
*ctx.state.aligns.get_mut(gen_axis) = gen_align;
|
||||
*had.get_mut(gen_axis) = true;
|
||||
}
|
||||
} else {
|
||||
// We don't know the axis: This has to be a `center` alignment for a
|
||||
// positional argument.
|
||||
debug_assert_eq!(arg, AlignValue::Center);
|
||||
|
||||
if had.main && had.cross {
|
||||
ctx.diag(error!(span, "duplicate alignment"));
|
||||
} else if had_center {
|
||||
// Both this and the previous one are unspecified `center`
|
||||
// alignments. Both axes should be centered.
|
||||
ctx.state.aligns.main = Align::Center;
|
||||
ctx.state.aligns.cross = Align::Center;
|
||||
had = Gen::uniform(true);
|
||||
} else {
|
||||
had_center = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we we know the other alignment, we can handle the unspecified
|
||||
// `center` alignment.
|
||||
if had_center && (had.main || had.cross) {
|
||||
if had.main {
|
||||
ctx.state.aligns.cross = Align::Center;
|
||||
} else {
|
||||
ctx.state.aligns.main = Align::Center;
|
||||
}
|
||||
had = Gen::uniform(true);
|
||||
had_center = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If `had_center` wasn't flushed by now, it's the only argument and
|
||||
// then we default to applying it to the cross axis.
|
||||
if had_center {
|
||||
ctx.state.aligns.cross = Align::Center;
|
||||
}
|
||||
}
|
||||
|
||||
/// An alignment value.
|
||||
/// An alignment specifier passed to `align`.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub(super) enum AlignValue {
|
||||
Left,
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl AlignValue {
|
||||
/// The specific axis this alignment refers to.
|
||||
fn axis(self) -> Option<SpecAxis> {
|
||||
match self {
|
||||
Self::Start => None,
|
||||
Self::Center => None,
|
||||
Self::End => None,
|
||||
Self::Left => Some(SpecAxis::Horizontal),
|
||||
Self::Right => Some(SpecAxis::Horizontal),
|
||||
Self::Top => Some(SpecAxis::Vertical),
|
||||
Self::Bottom => Some(SpecAxis::Vertical),
|
||||
Self::Center => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Switch for AlignValue {
|
||||
type Other = Align;
|
||||
|
||||
fn switch(self, dirs: LayoutDirs) -> Self::Other {
|
||||
let get = |dir: Dir, at_positive_start| {
|
||||
if dir.is_positive() == at_positive_start {
|
||||
fn to_align(self, dir: Dir) -> Align {
|
||||
let side = |is_at_positive_start| {
|
||||
if dir.is_positive() == is_at_positive_start {
|
||||
Align::Start
|
||||
} else {
|
||||
Align::End
|
||||
}
|
||||
};
|
||||
|
||||
let dirs = dirs.switch(dirs);
|
||||
match self {
|
||||
Self::Left => get(dirs.horizontal, true),
|
||||
Self::Right => get(dirs.horizontal, false),
|
||||
Self::Top => get(dirs.vertical, true),
|
||||
Self::Bottom => get(dirs.vertical, false),
|
||||
Self::Start => Align::Start,
|
||||
Self::Center => Align::Center,
|
||||
Self::End => Align::End,
|
||||
Self::Left => side(true),
|
||||
Self::Right => side(false),
|
||||
Self::Top => side(true),
|
||||
Self::Bottom => side(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,8 +112,10 @@ impl Switch for AlignValue {
|
||||
impl Display for AlignValue {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.pad(match self {
|
||||
Self::Left => "left",
|
||||
Self::Start => "start",
|
||||
Self::Center => "center",
|
||||
Self::End => "end",
|
||||
Self::Left => "left",
|
||||
Self::Right => "right",
|
||||
Self::Top => "top",
|
||||
Self::Bottom => "bottom",
|
||||
|
@ -2,9 +2,7 @@ use ::image::GenericImageView;
|
||||
|
||||
use super::*;
|
||||
use crate::env::{ImageResource, ResourceId};
|
||||
use crate::layout::{
|
||||
AnyNode, Areas, Element, Fragment, Frame, Image, Layout, LayoutContext,
|
||||
};
|
||||
use crate::layout::{AnyNode, Areas, Element, Frame, Image, Layout, LayoutContext};
|
||||
|
||||
/// `image`: An image.
|
||||
///
|
||||
@ -25,13 +23,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let loaded = ctx.env.resources.load(&path.v, ImageResource::parse);
|
||||
if let Some((res, img)) = loaded {
|
||||
let dimensions = img.buf.dimensions();
|
||||
ctx.push(ImageNode {
|
||||
res,
|
||||
dimensions,
|
||||
width,
|
||||
height,
|
||||
aligns: ctx.state.aligns,
|
||||
});
|
||||
ctx.push_into_par(ImageNode { res, dimensions, width, height });
|
||||
} else {
|
||||
ctx.diag(error!(path.span, "failed to load image"));
|
||||
}
|
||||
@ -42,8 +34,6 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
/// An image node.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct ImageNode {
|
||||
/// How to align this image node in its parent.
|
||||
aligns: LayoutAligns,
|
||||
/// The resource id of the image file.
|
||||
res: ResourceId,
|
||||
/// The pixel dimensions of the image.
|
||||
@ -55,7 +45,7 @@ struct ImageNode {
|
||||
}
|
||||
|
||||
impl Layout for ImageNode {
|
||||
fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
|
||||
let Areas { current, full, .. } = areas;
|
||||
|
||||
let pixel_width = self.dimensions.0 as f64;
|
||||
@ -86,7 +76,7 @@ impl Layout for ImageNode {
|
||||
let mut frame = Frame::new(size);
|
||||
frame.push(Point::ZERO, Element::Image(Image { res: self.res, size }));
|
||||
|
||||
Fragment::Frame(frame, self.aligns)
|
||||
vec![frame]
|
||||
}
|
||||
}
|
||||
|
||||
|
45
src/library/lang.rs
Normal file
45
src/library/lang.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use super::*;
|
||||
|
||||
/// `lang`: Configure the language.
|
||||
///
|
||||
/// # Positional parameters
|
||||
/// - Language: of type `string`. Has to be a valid ISO 639-1 code.
|
||||
///
|
||||
/// # Named parameters
|
||||
/// - Text direction: `dir`, of type `direction`, must be horizontal.
|
||||
///
|
||||
/// # Return value
|
||||
/// A template that configures language properties.
|
||||
///
|
||||
/// # Relevant types and constants
|
||||
/// - Type `direction`
|
||||
/// - `ltr`
|
||||
/// - `rtl`
|
||||
pub fn lang(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let iso = args.find::<String>(ctx).map(|s| s.to_ascii_lowercase());
|
||||
let dir = args.get::<Spanned<Dir>>(ctx, "dir");
|
||||
|
||||
Value::template("lang", move |ctx| {
|
||||
if let Some(iso) = &iso {
|
||||
ctx.state.lang.dir = lang_dir(iso);
|
||||
}
|
||||
|
||||
if let Some(dir) = dir {
|
||||
if dir.v.axis() == SpecAxis::Horizontal {
|
||||
ctx.state.lang.dir = dir.v;
|
||||
} else {
|
||||
ctx.diag(error!(dir.span, "must be horizontal"));
|
||||
}
|
||||
}
|
||||
|
||||
ctx.push_parbreak();
|
||||
})
|
||||
}
|
||||
|
||||
/// The default direction for the language identified by `iso`.
|
||||
fn lang_dir(iso: &str) -> Dir {
|
||||
match iso {
|
||||
"ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
|
||||
"en" | "fr" | "de" | _ => Dir::LTR,
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ mod align;
|
||||
mod base;
|
||||
mod font;
|
||||
mod image;
|
||||
mod lang;
|
||||
mod markup;
|
||||
mod pad;
|
||||
mod page;
|
||||
@ -18,6 +19,7 @@ pub use self::image::*;
|
||||
pub use align::*;
|
||||
pub use base::*;
|
||||
pub use font::*;
|
||||
pub use lang::*;
|
||||
pub use markup::*;
|
||||
pub use pad::*;
|
||||
pub use page::*;
|
||||
@ -31,7 +33,7 @@ use fontdock::{FontStyle, FontWeight};
|
||||
|
||||
use crate::eval::{AnyValue, FuncValue, Scope};
|
||||
use crate::eval::{EvalContext, FuncArgs, TemplateValue, Value};
|
||||
use crate::exec::{Exec, ExecContext, FontFamily};
|
||||
use crate::exec::{Exec, FontFamily};
|
||||
use crate::font::VerticalFontMetric;
|
||||
use crate::geom::*;
|
||||
use crate::syntax::{Node, Spanned};
|
||||
@ -67,6 +69,7 @@ pub fn _new() -> Scope {
|
||||
func!("font", font);
|
||||
func!("h", h);
|
||||
func!("image", image);
|
||||
func!("lang", lang);
|
||||
func!("pad", pad);
|
||||
func!("page", page);
|
||||
func!("pagebreak", pagebreak);
|
||||
@ -79,8 +82,10 @@ pub fn _new() -> Scope {
|
||||
func!("v", v);
|
||||
|
||||
// Constants.
|
||||
constant!("left", AlignValue::Left);
|
||||
constant!("start", AlignValue::Start);
|
||||
constant!("center", AlignValue::Center);
|
||||
constant!("end", AlignValue::End);
|
||||
constant!("left", AlignValue::Left);
|
||||
constant!("right", AlignValue::Right);
|
||||
constant!("top", AlignValue::Top);
|
||||
constant!("bottom", AlignValue::Bottom);
|
||||
|
@ -31,9 +31,7 @@ pub fn pad(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
);
|
||||
|
||||
Value::template("pad", move |ctx| {
|
||||
let snapshot = ctx.state.clone();
|
||||
let child = ctx.exec(&body).into();
|
||||
ctx.push(PadNode { padding, child });
|
||||
ctx.state = snapshot;
|
||||
let child = ctx.exec_group(&body).into();
|
||||
ctx.push_into_par(PadNode { padding, child });
|
||||
})
|
||||
}
|
||||
|
@ -17,19 +17,10 @@ use crate::paper::{Paper, PaperClass};
|
||||
/// - Top margin: `top`, of type `linear` relative to height.
|
||||
/// - Bottom margin: `bottom`, of type `linear` relative to height.
|
||||
/// - Flip width and height: `flip`, of type `bool`.
|
||||
/// - Main layouting direction: `main-dir`, of type `direction`.
|
||||
/// - Cross layouting direction: `cross-dir`, of type `direction`.
|
||||
///
|
||||
/// # Return value
|
||||
/// A template that configures page properties. The effect is scoped to the body
|
||||
/// if present.
|
||||
///
|
||||
/// # Relevant types and constants
|
||||
/// - Type `direction`
|
||||
/// - `ltr` (left to right)
|
||||
/// - `rtl` (right to left)
|
||||
/// - `ttb` (top to bottom)
|
||||
/// - `btt` (bottom to top)
|
||||
pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let paper = args.find::<Spanned<String>>(ctx).and_then(|name| {
|
||||
Paper::from_name(&name.v).or_else(|| {
|
||||
@ -46,8 +37,6 @@ pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let right = args.get(ctx, "right");
|
||||
let bottom = args.get(ctx, "bottom");
|
||||
let flip = args.get(ctx, "flip");
|
||||
let main = args.get(ctx, "main-dir");
|
||||
let cross = args.get(ctx, "cross-dir");
|
||||
let body = args.find::<TemplateValue>(ctx);
|
||||
let span = args.span;
|
||||
|
||||
@ -94,7 +83,6 @@ pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
std::mem::swap(&mut page.size.width, &mut page.size.height);
|
||||
}
|
||||
|
||||
ctx.set_dirs(Gen::new(main, cross));
|
||||
ctx.finish_page(false, true, span);
|
||||
|
||||
if let Some(body) = &body {
|
||||
|
@ -2,26 +2,19 @@ use super::*;
|
||||
|
||||
/// `par`: Configure paragraphs.
|
||||
///
|
||||
/// # Positional parameters
|
||||
/// - Body: optional, of type `template`.
|
||||
///
|
||||
/// # Named parameters
|
||||
/// - Paragraph spacing: `spacing`, of type `linear` relative to current font size.
|
||||
/// - Line leading: `leading`, of type `linear` relative to current font size.
|
||||
/// - Word spacing: `word-spacing`, of type `linear` relative to current font size.
|
||||
///
|
||||
/// # Return value
|
||||
/// A template that configures paragraph properties. The effect is scoped to the
|
||||
/// body if present.
|
||||
/// A template that configures paragraph properties.
|
||||
pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let spacing = args.get(ctx, "spacing");
|
||||
let leading = args.get(ctx, "leading");
|
||||
let word_spacing = args.get(ctx, "word-spacing");
|
||||
let body = args.find::<TemplateValue>(ctx);
|
||||
|
||||
Value::template("par", move |ctx| {
|
||||
let snapshot = ctx.state.clone();
|
||||
|
||||
if let Some(spacing) = spacing {
|
||||
ctx.state.par.spacing = spacing;
|
||||
}
|
||||
@ -35,10 +28,5 @@ pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
}
|
||||
|
||||
ctx.push_parbreak();
|
||||
|
||||
if let Some(body) = &body {
|
||||
body.exec(ctx);
|
||||
ctx.state = snapshot;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -59,21 +59,18 @@ fn rect_impl(
|
||||
body: TemplateValue,
|
||||
) -> Value {
|
||||
Value::template(name, move |ctx| {
|
||||
let snapshot = ctx.state.clone();
|
||||
let child = ctx.exec(&body).into();
|
||||
let child = ctx.exec_group(&body).into();
|
||||
let node = FixedNode { width, height, aspect, child };
|
||||
|
||||
if let Some(color) = fill {
|
||||
ctx.push(BackgroundNode {
|
||||
ctx.push_into_par(BackgroundNode {
|
||||
shape: BackgroundShape::Rect,
|
||||
fill: Fill::Color(color),
|
||||
child: node.into(),
|
||||
});
|
||||
} else {
|
||||
ctx.push(node);
|
||||
ctx.push_into_par(node);
|
||||
}
|
||||
|
||||
ctx.state = snapshot;
|
||||
})
|
||||
}
|
||||
|
||||
@ -136,8 +133,7 @@ fn ellipse_impl(
|
||||
// perfectly into the ellipse.
|
||||
const PAD: f64 = 0.5 - SQRT_2 / 4.0;
|
||||
|
||||
let snapshot = ctx.state.clone();
|
||||
let child = ctx.exec(&body).into();
|
||||
let child = ctx.exec_group(&body).into();
|
||||
let node = FixedNode {
|
||||
width,
|
||||
height,
|
||||
@ -150,15 +146,13 @@ fn ellipse_impl(
|
||||
};
|
||||
|
||||
if let Some(color) = fill {
|
||||
ctx.push(BackgroundNode {
|
||||
ctx.push_into_par(BackgroundNode {
|
||||
shape: BackgroundShape::Ellipse,
|
||||
fill: Fill::Color(color),
|
||||
child: node.into(),
|
||||
});
|
||||
} else {
|
||||
ctx.push(node);
|
||||
ctx.push_into_par(node);
|
||||
}
|
||||
|
||||
ctx.state = snapshot;
|
||||
})
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use crate::layout::SpacingNode;
|
||||
|
||||
/// `h`: Horizontal spacing.
|
||||
///
|
||||
@ -9,7 +8,7 @@ use crate::layout::SpacingNode;
|
||||
/// # Return value
|
||||
/// A template that inserts horizontal spacing.
|
||||
pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
spacing_impl(ctx, args, SpecAxis::Horizontal)
|
||||
spacing_impl("h", ctx, args, GenAxis::Cross)
|
||||
}
|
||||
|
||||
/// `v`: Vertical spacing.
|
||||
@ -20,20 +19,20 @@ pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
/// # Return value
|
||||
/// A template that inserts vertical spacing.
|
||||
pub fn v(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
spacing_impl(ctx, args, SpecAxis::Vertical)
|
||||
spacing_impl("v", ctx, args, GenAxis::Main)
|
||||
}
|
||||
|
||||
fn spacing_impl(ctx: &mut EvalContext, args: &mut FuncArgs, axis: SpecAxis) -> Value {
|
||||
fn spacing_impl(
|
||||
name: &str,
|
||||
ctx: &mut EvalContext,
|
||||
args: &mut FuncArgs,
|
||||
axis: GenAxis,
|
||||
) -> Value {
|
||||
let spacing: Option<Linear> = args.require(ctx, "spacing");
|
||||
Value::template("spacing", move |ctx| {
|
||||
Value::template(name, move |ctx| {
|
||||
if let Some(linear) = spacing {
|
||||
let amount = linear.resolve(ctx.state.font.resolve_size());
|
||||
let spacing = SpacingNode { amount, softness: 0 };
|
||||
if axis == ctx.state.dirs.main.axis() {
|
||||
ctx.push_into_stack(spacing);
|
||||
} else {
|
||||
ctx.push(spacing);
|
||||
}
|
||||
ctx.push_spacing(axis, amount, 0);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
BIN
tests/ref/library/lang.png
Normal file
BIN
tests/ref/library/lang.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 7.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.2 KiB |
16
tests/typ/library/lang.typ
Normal file
16
tests/typ/library/lang.typ
Normal file
@ -0,0 +1,16 @@
|
||||
// Test the `lang` function.
|
||||
|
||||
---
|
||||
Left to right.
|
||||
|
||||
#lang("ar")
|
||||
Right to left.
|
||||
|
||||
#lang(dir: ltr)
|
||||
Back again.
|
||||
|
||||
---
|
||||
// Ref: false
|
||||
|
||||
// Error: 12-15 must be horizontal
|
||||
#lang(dir: ttb)
|
@ -27,9 +27,6 @@
|
||||
// Error: 7-18 unknown variable
|
||||
#page(nonexistant)
|
||||
|
||||
// Error: 17-20 aligned axis
|
||||
#page(main-dir: ltr)
|
||||
|
||||
// Flipped predefined paper.
|
||||
#page("a11", flip: true)[Flipped A11]
|
||||
|
||||
@ -38,10 +35,6 @@
|
||||
#page(flip: true)
|
||||
Wide
|
||||
|
||||
// Test changing the layouting directions of pages.
|
||||
#page(height: 50pt, main-dir: btt, cross-dir: rtl)
|
||||
Right to left!
|
||||
|
||||
---
|
||||
// Test a combination of pages with bodies and normal content.
|
||||
|
||||
|
@ -16,10 +16,3 @@ Relative #h(100%) spacing
|
||||
// Missing spacing.
|
||||
// Error: 12 missing argument: spacing
|
||||
Totally #h() ignored
|
||||
|
||||
// Swapped axes.
|
||||
#page(main-dir: rtl, cross-dir: ttb, height: 80pt)[
|
||||
1 #h(1cm) 2
|
||||
|
||||
3 #v(1cm) 4 #v(-1cm) 5
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user