Refactor layouting base 🪁

This commit is contained in:
Laurenz 2020-10-11 22:38:30 +02:00
parent f04ad0ffa5
commit d3bc4ec073
14 changed files with 224 additions and 189 deletions

View File

@ -1,7 +1,5 @@
//! Simplifies argument parsing.
use std::mem;
use super::{Convert, EvalContext, RefKey, ValueDict};
use crate::syntax::{SpanWith, Spanned};
@ -67,7 +65,7 @@ impl Args {
{
for (&key, entry) in self.0.v.nums_mut() {
let span = entry.value.span;
match T::convert(mem::take(&mut entry.value)).0 {
match T::convert(std::mem::take(&mut entry.value)).0 {
Ok(t) => {
self.0.v.remove(key);
return Some(t);
@ -87,7 +85,7 @@ impl Args {
std::iter::from_fn(move || {
for (&key, entry) in self.0.v.nums_mut().skip(skip) {
let span = entry.value.span;
match T::convert(mem::take(&mut entry.value)).0 {
match T::convert(std::mem::take(&mut entry.value)).0 {
Ok(t) => {
self.0.v.remove(key);
return Some(t);
@ -109,7 +107,7 @@ impl Args {
std::iter::from_fn(move || {
for (key, entry) in self.0.v.strs_mut().skip(skip) {
let span = entry.value.span;
match T::convert(mem::take(&mut entry.value)).0 {
match T::convert(std::mem::take(&mut entry.value)).0 {
Ok(t) => {
let key = key.clone();
self.0.v.remove(&key);

View File

@ -16,16 +16,15 @@ pub use state::*;
pub use value::*;
use std::any::Any;
use std::mem;
use std::rc::Rc;
use fontdock::FontStyle;
use crate::diag::Diag;
use crate::diag::{Deco, Feedback, Pass};
use crate::geom::{Gen, Length, Relative, Spec, Switch};
use crate::geom::{Gen, Length, Relative};
use crate::layout::{
Document, LayoutNode, Pad, Pages, Par, Softness, Spacing, Stack, Text,
Document, Expansion, LayoutNode, Pad, Pages, Par, Softness, Spacing, Stack, Text,
};
use crate::syntax::*;
@ -35,11 +34,9 @@ use crate::syntax::*;
/// evaluation.
pub fn eval(tree: &SynTree, state: State) -> Pass<Document> {
let mut ctx = EvalContext::new(state);
ctx.start_page_group(false);
tree.eval(&mut ctx);
ctx.end_page_group();
ctx.finish()
}
@ -125,7 +122,7 @@ impl EvalContext {
/// [`push`]: #method.push
/// [`end_group`]: #method.end_group
pub fn start_group<T: 'static>(&mut self, meta: T) {
self.groups.push((Box::new(meta), mem::take(&mut self.inner)));
self.groups.push((Box::new(meta), std::mem::take(&mut self.inner)));
}
/// End a layouting group started with [`start_group`].
@ -134,9 +131,15 @@ impl EvalContext {
///
/// [`start_group`]: #method.start_group
pub fn end_group<T: 'static>(&mut self) -> (T, Vec<LayoutNode>) {
if let Some(&LayoutNode::Spacing(spacing)) = self.inner.last() {
if spacing.softness == Softness::Soft {
self.inner.pop();
}
}
let (any, outer) = self.groups.pop().expect("no pushed group");
let group = *any.downcast::<T>().expect("bad group type");
(group, mem::replace(&mut self.inner, outer))
(group, std::mem::replace(&mut self.inner, outer))
}
/// Start a page run group based on the active page state.
@ -167,9 +170,9 @@ impl EvalContext {
padding,
child: LayoutNode::dynamic(Stack {
dirs,
children,
aligns,
expand: Spec::new(true, true),
expansion: Gen::new(Expansion::Fill, Expansion::Fill),
children,
}),
}),
})
@ -191,13 +194,13 @@ impl EvalContext {
if !children.is_empty() {
// FIXME: This is a hack and should be superseded by constraints
// having min and max size.
let expand_cross = self.groups.len() <= 1;
let cross_expansion = Expansion::fill_if(self.groups.len() <= 1);
self.push(Par {
dirs,
aligns,
cross_expansion,
line_spacing,
children,
aligns,
expand: Gen::new(false, expand_cross).switch(dirs),
});
}
}
@ -222,7 +225,7 @@ impl EvalContext {
Text {
text,
dir: self.state.dirs.cross,
size: self.state.font.font_size(),
font_size: self.state.font.font_size(),
families: Rc::clone(&self.state.font.families),
variant,
aligns: self.state.aligns,
@ -284,26 +287,11 @@ impl Eval for SynNode {
ctx.start_par_group();
}
SynNode::Emph => {
ctx.state.font.emph ^= true;
}
SynNode::Strong => {
ctx.state.font.strong ^= true;
}
SynNode::Heading(heading) => {
heading.eval(ctx);
}
SynNode::Raw(raw) => {
raw.eval(ctx);
}
SynNode::Expr(expr) => {
let value = expr.eval(ctx);
value.eval(ctx);
}
SynNode::Emph => ctx.state.font.emph ^= true,
SynNode::Strong => ctx.state.font.strong ^= true,
SynNode::Heading(heading) => heading.eval(ctx),
SynNode::Raw(raw) => raw.eval(ctx),
SynNode::Expr(expr) => expr.eval(ctx).eval(ctx),
}
}
}
@ -339,9 +327,9 @@ impl Eval for NodeRaw {
ctx.push(Stack {
dirs: ctx.state.dirs,
children,
aligns: ctx.state.aligns,
expand: Spec::new(false, false),
expansion: Gen::new(Expansion::Fit, Expansion::Fit),
children,
});
ctx.state.font.families = prev;

View File

@ -142,9 +142,9 @@ impl<'a, W: Write> PdfExporter<'a, W> {
match element {
LayoutElement::Text(shaped) => {
// Check if we need to issue a font switching action.
if shaped.face != face || shaped.size != size {
if shaped.face != face || shaped.font_size != size {
face = shaped.face;
size = shaped.size;
size = shaped.font_size;
text.tf(
self.to_pdf[&shaped.face] as u32 + 1,
size.to_pt() as f32,

View File

@ -3,6 +3,7 @@ use super::*;
/// The top-level layout node.
#[derive(Debug, Clone, PartialEq)]
pub struct Document {
/// The runs of pages with same properties.
pub runs: Vec<Pages>,
}
@ -22,26 +23,17 @@ impl Document {
pub struct Pages {
/// The size of the pages.
pub size: Size,
/// The layout node that produces the actual pages.
/// The layout node that produces the actual pages (typically a [stack]).
///
/// [stack]: struct.Stack.html
pub child: LayoutNode,
}
impl Pages {
/// Layout the page run.
pub async fn layout(&self, ctx: &mut LayoutContext) -> Vec<BoxLayout> {
let constraints = LayoutConstraints {
spaces: vec![LayoutSpace { base: self.size, size: self.size }],
repeat: true,
};
self.child
.layout(ctx, constraints)
.await
.into_iter()
.filter_map(|item| match item {
Layouted::Spacing(_) => None,
Layouted::Box(layout, _) => Some(layout),
})
.collect()
let areas = Areas::repeat(self.size);
let layouted = self.child.layout(ctx, &areas).await;
layouted.into_iter().filter_map(Layouted::into_boxed).collect()
}
}

View File

@ -4,34 +4,25 @@ use crate::geom::Linear;
/// A node that can fix its child's width and height.
#[derive(Debug, Clone, PartialEq)]
pub struct Fixed {
/// The fixed width, if any.
pub width: Option<Linear>,
/// The fixed height, if any.
pub height: Option<Linear>,
/// The child node whose size to fix.
pub child: LayoutNode,
}
#[async_trait(?Send)]
impl Layout for Fixed {
async fn layout(
&self,
ctx: &mut LayoutContext,
constraints: LayoutConstraints,
) -> Vec<Layouted> {
let space = constraints.spaces[0];
async fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Layouted> {
let Area { rem, full } = areas.current;
let size = Size::new(
self.width
.map(|w| w.eval(space.base.width))
.unwrap_or(space.size.width),
self.height
.map(|h| h.eval(space.base.height))
.unwrap_or(space.size.height),
self.width.map(|w| w.eval(full.width)).unwrap_or(rem.width),
self.height.map(|h| h.eval(full.height)).unwrap_or(rem.height),
);
self.child
.layout(ctx, LayoutConstraints {
spaces: vec![LayoutSpace { base: size, size }],
repeat: false,
})
.await
let areas = Areas::once(size);
self.child.layout(ctx, &areas).await
}
}

View File

@ -51,11 +51,86 @@ pub trait Layout {
/// constraints: LayoutConstraints,
/// ) -> Vec<LayoutItem>;
/// ```
async fn layout(
&self,
ctx: &mut LayoutContext,
constraints: LayoutConstraints,
) -> Vec<Layouted>;
async fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Layouted>;
}
/// A sequence of areas to layout into.
#[derive(Debug, Clone, PartialEq)]
pub struct Areas {
/// The current area.
pub current: Area,
/// The backlog of followup areas.
///
/// _Note_: This works stack-like and not queue-like!
pub backlog: Vec<Size>,
/// The last area that is repeated when the backlog is empty.
pub last: Option<Size>,
}
impl Areas {
/// Create a new length-1 sequence of areas with just one `area`.
pub fn once(size: Size) -> Self {
Self {
current: Area::new(size),
backlog: vec![],
last: None,
}
}
/// Create a new sequence of areas that repeats `area` indefinitely.
pub fn repeat(size: Size) -> Self {
Self {
current: Area::new(size),
backlog: vec![],
last: Some(size),
}
}
/// Advance to the next area if there is any.
pub fn next(&mut self) {
if let Some(size) = self.backlog.pop().or(self.last) {
self.current = Area::new(size);
}
}
/// Whether `current` is a fully sized (untouched) copy of the last area.
///
/// If this is false calling `next()` will have no effect.
pub fn in_full_last(&self) -> bool {
self.backlog.is_empty() && self.last.map_or(true, |size| self.current.rem == size)
}
}
/// The area into which content can be laid out.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Area {
/// The remaining size of this area.
pub rem: Size,
/// The full size this area once had (used for relative sizing).
pub full: Size,
}
impl Area {
/// Create a new area.
pub fn new(size: Size) -> Self {
Self { rem: size, full: size }
}
}
/// How to determine a container's size along an axis.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Expansion {
/// Fit the content.
Fit,
/// Fill the available space.
Fill,
}
impl Expansion {
/// Returns `Fill` if the condition is true and `Fit` otherwise.
pub fn fill_if(condition: bool) -> Self {
if condition { Self::Fill } else { Self::Fit }
}
}
/// An item that is produced by [layouting] a node.
@ -65,27 +140,18 @@ pub trait Layout {
pub enum Layouted {
/// Spacing that should be added to the parent.
Spacing(Length),
/// A box that should be aligned in the parent.
Box(BoxLayout, Gen<Align>),
/// A box that should be added to and aligned in the parent.
Boxed(BoxLayout, Gen<Align>),
}
/// The constraints for layouting a single node.
#[derive(Debug, Clone)]
pub struct LayoutConstraints {
/// The spaces to layout into.
pub spaces: Vec<LayoutSpace>,
/// Whether to spill over into copies of the last space or finish layouting
/// when the last space is used up.
pub repeat: bool,
}
/// The space into which content is laid out.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct LayoutSpace {
/// The full size of this container (the base for relative sizes).
pub base: Size,
/// The maximum size of the rectangle to layout into.
pub size: Size,
impl Layouted {
/// Return the box if this if its a box variant.
pub fn into_boxed(self) -> Option<BoxLayout> {
match self {
Self::Spacing(_) => None,
Self::Boxed(boxed, _) => Some(boxed),
}
}
}
/// A finished box with content at fixed positions.

View File

@ -18,12 +18,23 @@ pub enum LayoutNode {
}
impl LayoutNode {
/// Create a new model node form a type implementing `DynNode`.
/// Create a new dynamic node.
pub fn dynamic<T: DynNode>(inner: T) -> Self {
Self::Dyn(Dynamic::new(inner))
}
}
#[async_trait(?Send)]
impl Layout for LayoutNode {
async fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Layouted> {
match self {
Self::Spacing(spacing) => spacing.layout(ctx, areas).await,
Self::Text(text) => text.layout(ctx, areas).await,
Self::Dyn(boxed) => boxed.layout(ctx, areas).await,
}
}
}
impl Debug for LayoutNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
@ -34,21 +45,6 @@ impl Debug for LayoutNode {
}
}
#[async_trait(?Send)]
impl Layout for LayoutNode {
async fn layout(
&self,
ctx: &mut LayoutContext,
constraints: LayoutConstraints,
) -> Vec<Layouted> {
match self {
Self::Spacing(spacing) => spacing.layout(ctx, constraints).await,
Self::Text(text) => text.layout(ctx, constraints).await,
Self::Dyn(boxed) => boxed.layout(ctx, constraints).await,
}
}
}
/// A wrapper around a boxed dynamic node.
///
/// _Note_: This is needed because the compiler can't `derive(PartialEq)` for
@ -80,6 +76,12 @@ impl Debug for Dynamic {
}
}
impl From<Dynamic> for LayoutNode {
fn from(dynamic: Dynamic) -> Self {
Self::Dyn(dynamic)
}
}
impl Clone for Dynamic {
fn clone(&self) -> Self {
Self(self.0.dyn_clone())
@ -92,12 +94,6 @@ impl PartialEq for Dynamic {
}
}
impl From<Dynamic> for LayoutNode {
fn from(dynamic: Dynamic) -> Self {
Self::Dyn(dynamic)
}
}
/// A dynamic node, which can implement custom layouting behaviour.
///
/// This trait just combines the requirements for types to qualify as dynamic

View File

@ -4,45 +4,40 @@ use crate::geom::Linear;
/// A node that pads its child at the sides.
#[derive(Debug, Clone, PartialEq)]
pub struct Pad {
/// The amount of padding.
pub padding: Sides<Linear>,
/// The child node whose sides to pad.
pub child: LayoutNode,
}
#[async_trait(?Send)]
impl Layout for Pad {
async fn layout(
&self,
ctx: &mut LayoutContext,
constraints: LayoutConstraints,
) -> Vec<Layouted> {
self.child
.layout(ctx, LayoutConstraints {
spaces: constraints
.spaces
.into_iter()
.map(|space| LayoutSpace {
base: space.base - self.padding.eval(space.base).size(),
size: space.size - self.padding.eval(space.size).size(),
})
.collect(),
repeat: constraints.repeat,
})
.await
.into_iter()
.map(|item| match item {
Layouted::Box(boxed, align) => {
let padding = self.padding.eval(boxed.size);
let padded = boxed.size + padding.size();
async fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Layouted> {
let shrink = |size| size - self.padding.eval(size).size();
let areas = Areas {
current: Area {
rem: shrink(areas.current.rem),
full: shrink(areas.current.full),
},
backlog: areas.backlog.iter().copied().map(shrink).collect(),
last: areas.last.map(shrink),
};
let mut outer = BoxLayout::new(padded);
let start = Point::new(padding.left, padding.top);
outer.push_layout(start, boxed);
let mut layouted = self.child.layout(ctx, &areas).await;
Layouted::Box(outer, align)
for item in &mut layouted {
if let Layouted::Boxed(boxed, _) = item {
let padding = self.padding.eval(boxed.size);
let origin = Point::new(padding.left, padding.top);
boxed.size += padding.size();
for (point, _) in &mut boxed.elements {
*point += origin;
}
item => item,
})
.collect()
}
}
layouted
}
}

View File

@ -2,16 +2,21 @@ use std::fmt::{self, Debug, Formatter};
use super::*;
/// A node that inserts spacing.
/// A spacing node.
#[derive(Copy, Clone, PartialEq)]
pub struct Spacing {
/// The amount of spacing to insert.
pub amount: Length,
/// Spacing interaction, see [Softness's] documentation for more
/// information.
///
/// [Softness's]: enum.Softness.html
pub softness: Softness,
}
#[async_trait(?Send)]
impl Layout for Spacing {
async fn layout(&self, _: &mut LayoutContext, _: LayoutConstraints) -> Vec<Layouted> {
async fn layout(&self, _: &mut LayoutContext, _: &Areas) -> Vec<Layouted> {
vec![Layouted::Spacing(self.amount)]
}
}

View File

@ -9,32 +9,36 @@ use crate::shaping;
/// A text node.
#[derive(Clone, PartialEq)]
pub struct Text {
/// The text.
pub text: String,
pub size: Length,
/// The font size.
pub font_size: Length,
/// The text direction.
pub dir: Dir,
/// The families used for font fallback.
pub families: Rc<FallbackTree>,
/// The font variant,
pub variant: FontVariant,
/// How to align this text node in its parent.
pub aligns: Gen<Align>,
}
#[async_trait(?Send)]
impl Layout for Text {
async fn layout(
&self,
ctx: &mut LayoutContext,
_constraints: LayoutConstraints,
) -> Vec<Layouted> {
async fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Vec<Layouted> {
let mut loader = ctx.loader.borrow_mut();
let boxed = shaping::shape(
&self.text,
self.size,
self.dir,
&mut loader,
&self.families,
self.variant,
)
.await;
vec![Layouted::Box(boxed, self.aligns)]
vec![Layouted::Boxed(
shaping::shape(
&mut loader,
&self.text,
self.font_size,
self.dir,
&self.families,
self.variant,
)
.await,
self.aligns,
)]
}
}

View File

@ -1,5 +1,5 @@
use crate::geom::Linear;
use crate::layout::{Fixed, Stack};
use crate::layout::{Expansion, Fixed, Stack};
use crate::prelude::*;
/// `box`: Layouts its contents into a box.
@ -20,9 +20,7 @@ pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value {
ctx.start_group(());
ctx.start_par_group();
body.eval(ctx);
ctx.end_par_group();
let ((), children) = ctx.end_group();
@ -33,7 +31,11 @@ pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value {
dirs,
children,
aligns,
expand: Spec::new(width.is_some(), height.is_some()),
expansion: Spec::new(
Expansion::fill_if(width.is_some()),
Expansion::fill_if(height.is_some()),
)
.switch(dirs),
}),
});

View File

@ -1,5 +1,3 @@
use std::mem;
use crate::geom::{Length, Linear};
use crate::paper::{Paper, PaperClass};
use crate::prelude::*;
@ -56,7 +54,7 @@ pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value {
if args.get::<_, bool>(ctx, "flip").unwrap_or(false) {
let size = &mut ctx.state.page.size;
mem::swap(&mut size.width, &mut size.height);
std::mem::swap(&mut size.width, &mut size.height);
}
args.done(ctx);

View File

@ -22,11 +22,11 @@ pub struct Shaped {
pub face: FaceId,
/// The shaped glyphs.
pub glyphs: Vec<GlyphId>,
/// The horizontal offsets of the glyphs. This is indexed parallel to `glyphs`.
/// Vertical offets are not yet supported.
/// The horizontal offsets of the glyphs. This is indexed parallel to
/// `glyphs`. Vertical offets are not yet supported.
pub offsets: Vec<Length>,
/// The font size.
pub size: Length,
pub font_size: Length,
}
impl Shaped {
@ -37,7 +37,7 @@ impl Shaped {
face,
glyphs: vec![],
offsets: vec![],
size,
font_size: size,
}
}
@ -62,15 +62,15 @@ impl Debug for Shaped {
///
/// [`Shaped`]: struct.Shaped.html
pub async fn shape(
text: &str,
size: Length,
dir: Dir,
loader: &mut FontLoader,
text: &str,
font_size: Length,
dir: Dir,
fallback: &FallbackTree,
variant: FontVariant,
) -> BoxLayout {
let mut layout = BoxLayout::new(Size::new(Length::ZERO, size));
let mut shaped = Shaped::new(FaceId::MAX, size);
let mut layout = BoxLayout::new(Size::new(Length::ZERO, font_size));
let mut shaped = Shaped::new(FaceId::MAX, font_size);
let mut offset = Length::ZERO;
// Create an iterator with conditional direction.
@ -86,7 +86,7 @@ pub async fn shape(
let query = FaceQuery { fallback: fallback.iter(), variant, c };
if let Some((id, owned_face)) = loader.query(query).await {
let face = owned_face.get();
let (glyph, width) = match lookup_glyph(face, c, size) {
let (glyph, width) = match lookup_glyph(face, c, font_size) {
Some(v) => v,
None => continue,
};
@ -96,7 +96,7 @@ pub async fn shape(
let pos = Point::new(layout.size.width, Length::ZERO);
layout.push(pos, LayoutElement::Text(shaped));
layout.size.width += offset;
shaped = Shaped::new(FaceId::MAX, size);
shaped = Shaped::new(FaceId::MAX, font_size);
offset = Length::ZERO;
}

View File

@ -198,9 +198,9 @@ fn render_shaped(
let path = builder.0.finish();
let units_per_em = face.units_per_em().unwrap_or(1000);
let s = scale * (shaped.size / units_per_em as f64);
let s = scale * (shaped.font_size / units_per_em as f64);
let x = pos.x + scale * offset;
let y = pos.y + scale * shaped.size;
let y = pos.y + scale * shaped.font_size;
let t = Transform::create_scale(s.to_pt() as f32, -s.to_pt() as f32)
.post_translate(Vector::new(x.to_pt() as f32, y.to_pt() as f32));