New ShapeNode

Replaces `BackgroundNode` and `FixedNode`
This commit is contained in:
Laurenz 2021-10-13 18:33:10 +02:00
parent 5f4dde0a6b
commit 1e74f7c407
18 changed files with 254 additions and 295 deletions

View File

@ -1,55 +0,0 @@
use super::*;
/// A node that places a rectangular filled background behind its child.
#[derive(Debug)]
#[cfg_attr(feature = "layout-cache", derive(Hash))]
pub struct BackgroundNode {
/// The kind of shape to use as a background.
pub shape: BackgroundShape,
/// Background color / texture.
pub fill: Paint,
/// The child node to be filled.
pub child: LayoutNode,
}
/// The kind of shape to use as a background.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum BackgroundShape {
Rect,
Ellipse,
}
impl Layout for BackgroundNode {
fn layout(
&self,
ctx: &mut LayoutContext,
regions: &Regions,
) -> Vec<Constrained<Rc<Frame>>> {
let mut frames = self.child.layout(ctx, regions);
for Constrained { item: frame, .. } in &mut frames {
let (point, geometry) = match self.shape {
BackgroundShape::Rect => (Point::zero(), Geometry::Rect(frame.size)),
BackgroundShape::Ellipse => {
(frame.size.to_point() / 2.0, Geometry::Ellipse(frame.size))
}
};
// Create a new frame with the background geometry and the child's
// frame.
let empty = Frame::new(frame.size, frame.baseline);
let prev = std::mem::replace(frame, Rc::new(empty));
let new = Rc::make_mut(frame);
new.push(point, Element::Geometry(geometry, self.fill));
new.push_frame(Point::zero(), prev);
}
frames
}
}
impl From<BackgroundNode> for LayoutNode {
fn from(background: BackgroundNode) -> Self {
Self::new(background)
}
}

View File

@ -1,99 +0,0 @@
use decorum::N64;
use super::*;
/// A node that can fix its child's width and height.
#[derive(Debug)]
#[cfg_attr(feature = "layout-cache", derive(Hash))]
pub struct FixedNode {
/// The fixed width, if any.
pub width: Option<Linear>,
/// The fixed height, if any.
pub height: Option<Linear>,
/// The fixed aspect ratio between width and height.
///
/// The resulting frame will satisfy `width = aspect * height`.
pub aspect: Option<N64>,
/// The child node whose size to fix.
pub child: LayoutNode,
}
impl Layout for FixedNode {
fn layout(
&self,
ctx: &mut LayoutContext,
regions: &Regions,
) -> Vec<Constrained<Rc<Frame>>> {
// Fill in width or height if aspect ratio and the other is given.
let aspect = self.aspect.map(N64::into_inner);
let width = self.width.or(self.height.zip(aspect).map(|(h, a)| a * h));
let height = self.height.or(self.width.zip(aspect).map(|(w, a)| w / a));
// Resolve the linears based on the current width and height.
let mut child_size = Size::new(
width.map_or(regions.current.w, |w| w.resolve(regions.base.w)),
height.map_or(regions.current.h, |h| h.resolve(regions.base.h)),
);
// If width or height aren't set for an axis, the base should be
// inherited from the parent for that axis.
let child_base = Size::new(
if width.is_some() { child_size.w } else { regions.base.w },
if height.is_some() { child_size.h } else { regions.base.h },
);
// Prepare constraints.
let mut constraints = Constraints::new(regions.expand);
constraints.set_base_if_linear(regions.base, Spec::new(width, height));
// If the size for one axis isn't specified, the `current` size along
// that axis needs to remain the same for the result to be reusable.
if width.is_none() {
constraints.exact.x = Some(regions.current.w);
}
if height.is_none() {
constraints.exact.y = Some(regions.current.h);
}
// Apply the aspect ratio.
if let Some(aspect) = aspect {
let width = child_size.w.min(aspect * child_size.h);
child_size = Size::new(width, width / aspect);
constraints.exact = regions.current.to_spec().map(Some);
constraints.min = Spec::splat(None);
constraints.max = Spec::splat(None);
}
// If width or height are fixed, the child should fill the available
// space along that axis.
let child_expand = Spec::new(width.is_some(), height.is_some());
// Layout the child.
let mut regions = Regions::one(child_size, child_base, child_expand);
let mut frames = self.child.layout(ctx, &regions);
// If we have an aspect ratio and the child is content-sized, we need to
// relayout with expansion.
if let Some(aspect) = aspect {
if width.is_none() && height.is_none() {
let needed = frames[0].item.size.cap(child_size);
let width = needed.w.max(aspect * needed.h);
regions.current = Size::new(width, width / aspect);
regions.expand = Spec::splat(true);
frames = self.child.layout(ctx, &regions);
}
}
// Overwrite the child's constraints with ours.
assert_eq!(frames.len(), 1);
frames[0].constraints = constraints;
frames
}
}
impl From<FixedNode> for LayoutNode {
fn from(fixed: FixedNode) -> Self {
Self::new(fixed)
}
}

View File

@ -1,8 +1,6 @@
//! Layouting.
mod background;
mod constraints;
mod fixed;
mod frame;
mod grid;
mod image;
@ -11,14 +9,13 @@ mod incremental;
mod pad;
mod par;
mod regions;
mod shape;
mod shaping;
mod stack;
mod tree;
pub use self::image::*;
pub use background::*;
pub use constraints::*;
pub use fixed::*;
pub use frame::*;
pub use grid::*;
#[cfg(feature = "layout-cache")]
@ -26,6 +23,7 @@ pub use incremental::*;
pub use pad::*;
pub use par::*;
pub use regions::*;
pub use shape::*;
pub use shaping::*;
pub use stack::*;
pub use tree::*;

141
src/layout/shape.rs Normal file
View File

@ -0,0 +1,141 @@
use std::f64::consts::SQRT_2;
use super::*;
/// Places its child into a sizable and fillable shape.
#[derive(Debug)]
#[cfg_attr(feature = "layout-cache", derive(Hash))]
pub struct ShapeNode {
/// Which shape to place the child into.
pub shape: ShapeKind,
/// The width, if any.
pub width: Option<Linear>,
/// The height, if any.
pub height: Option<Linear>,
/// How to fill the shape, if at all.
pub fill: Option<Paint>,
/// The child node to place into the shape, if any.
pub child: Option<LayoutNode>,
}
/// The type of a shape.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ShapeKind {
/// A rectangle with equal side lengths.
Square,
/// A quadrilateral with four right angles.
Rect,
/// An ellipse with coinciding foci.
Circle,
/// A curve around two focal points.
Ellipse,
}
impl Layout for ShapeNode {
fn layout(
&self,
ctx: &mut LayoutContext,
regions: &Regions,
) -> Vec<Constrained<Rc<Frame>>> {
// Resolve width and height relative to the region's base.
let width = self.width.map(|w| w.resolve(regions.base.w));
let height = self.height.map(|h| h.resolve(regions.base.h));
// Generate constraints.
let constraints = {
let mut cts = Constraints::new(regions.expand);
cts.set_base_if_linear(regions.base, Spec::new(self.width, self.height));
// Set exact and base constraint if child is automatically sized.
if self.width.is_none() {
cts.exact.x = Some(regions.current.w);
cts.base.x = Some(regions.base.w);
}
// Same here.
if self.height.is_none() {
cts.exact.y = Some(regions.current.h);
cts.base.y = Some(regions.base.h);
}
cts
};
// Layout.
let mut frames = if let Some(child) = &self.child {
let mut node: &dyn Layout = child;
let padded;
if matches!(self.shape, ShapeKind::Circle | ShapeKind::Ellipse) {
// Padding with this ratio ensures that a rectangular child fits
// perfectly into a circle / an ellipse.
padded = PadNode {
padding: Sides::splat(Relative::new(0.5 - SQRT_2 / 4.0).into()),
child: child.clone(),
};
node = &padded;
}
// The "pod" is the region into which the child will be layouted.
let mut pod = {
let size = Size::new(
width.unwrap_or(regions.current.w),
height.unwrap_or(regions.current.h),
);
let base = Size::new(
if width.is_some() { size.w } else { regions.base.w },
if height.is_some() { size.h } else { regions.base.h },
);
let expand = Spec::new(width.is_some(), height.is_some());
Regions::one(size, base, expand)
};
// Now, layout the child.
let mut frames = node.layout(ctx, &pod);
if matches!(self.shape, ShapeKind::Square | ShapeKind::Circle) {
// Relayout with full expansion into square region to make sure
// the result is really a square or circle.
let size = frames[0].item.size;
pod.current.w = size.w.max(size.h).min(pod.current.w);
pod.current.h = pod.current.w;
pod.expand = Spec::splat(true);
frames = node.layout(ctx, &pod);
}
// Validate and set constraints.
assert_eq!(frames.len(), 1);
frames[0].constraints = constraints;
frames
} else {
// Resolve shape size.
let size = Size::new(width.unwrap_or_default(), height.unwrap_or_default());
vec![Frame::new(size, size.h).constrain(constraints)]
};
// Add background shape if desired.
if let Some(fill) = self.fill {
let frame = Rc::make_mut(&mut frames[0].item);
let (pos, geometry) = match self.shape {
ShapeKind::Square | ShapeKind::Rect => {
(Point::zero(), Geometry::Rect(frame.size))
}
ShapeKind::Circle | ShapeKind::Ellipse => {
(frame.size.to_point() / 2.0, Geometry::Ellipse(frame.size))
}
};
frame.prepend(pos, Element::Geometry(geometry, fill));
}
frames
}
}
impl From<ShapeNode> for LayoutNode {
fn from(shape: ShapeNode) -> Self {
Self::new(shape)
}
}

View File

@ -50,8 +50,9 @@ impl PageRun {
}
/// A dynamic layouting node.
#[derive(Clone)]
pub struct LayoutNode {
node: Box<dyn Layout>,
node: Rc<dyn Layout>,
#[cfg(feature = "layout-cache")]
hash: u64,
}
@ -63,7 +64,7 @@ impl LayoutNode {
where
T: Layout + 'static,
{
Self { node: Box::new(node) }
Self { node: Rc::new(node) }
}
/// Create a new instance from any node that satisifies the required bounds.
@ -79,7 +80,7 @@ impl LayoutNode {
state.finish()
};
Self { node: Box::new(node), hash }
Self { node: Rc::new(node), hash }
}
}
@ -99,10 +100,16 @@ impl Layout for LayoutNode {
ctx.level -= 1;
let entry = FramesEntry::new(frames.clone(), ctx.level);
debug_assert!(
entry.check(regions),
"constraints did not match regions they were created for",
);
#[cfg(debug_assertions)]
if !entry.check(regions) {
eprintln!("regions: {:#?}", regions);
eprintln!(
"constraints: {:#?}",
frames.iter().map(|c| c.constraints).collect::<Vec<_>>()
);
panic!("constraints did not match regions they were created for");
}
ctx.layouts.insert(self.hash, entry);
frames

View File

@ -1,11 +1,8 @@
use std::f64::consts::SQRT_2;
use std::io;
use decorum::N64;
use super::*;
use crate::diag::Error;
use crate::layout::{BackgroundNode, BackgroundShape, FixedNode, ImageNode, PadNode};
use crate::layout::{ImageNode, ShapeKind, ShapeNode};
/// `image`: An image.
pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
@ -33,52 +30,24 @@ 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.eat().unwrap_or_default();
Ok(rect_impl(width, height, None, fill, body))
let body = args.eat();
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 {
Some(size) => Some(size),
None => args.named("width")?,
size => size,
};
let height = match width {
Some(_) => None,
let height = match size {
None => args.named("height")?,
size => size,
};
let aspect = Some(N64::from(1.0));
let fill = args.named("fill")?;
let body = args.eat().unwrap_or_default();
Ok(rect_impl(width, height, aspect, fill, body))
}
fn rect_impl(
width: Option<Linear>,
height: Option<Linear>,
aspect: Option<N64>,
fill: Option<Color>,
body: Template,
) -> Value {
Value::Template(Template::from_inline(move |style| {
let mut node = LayoutNode::new(FixedNode {
width,
height,
aspect,
child: body.to_stack(style).into(),
});
if let Some(fill) = fill {
node = LayoutNode::new(BackgroundNode {
shape: BackgroundShape::Rect,
fill: Paint::Color(fill),
child: node,
});
}
node
}))
let body = args.eat();
Ok(shape_impl(ShapeKind::Square, width, height, fill, body))
}
/// `ellipse`: An ellipse with optional content.
@ -86,8 +55,8 @@ 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.eat().unwrap_or_default();
Ok(ellipse_impl(width, height, None, fill, body))
let body = args.eat();
Ok(shape_impl(ShapeKind::Ellipse, width, height, fill, body))
}
/// `circle`: A circle with optional content.
@ -97,46 +66,39 @@ pub fn circle(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
None => args.named("width")?,
diameter => diameter,
};
let height = match width {
let height = match diameter {
None => args.named("height")?,
width => width,
diameter => diameter,
};
let aspect = Some(N64::from(1.0));
let fill = args.named("fill")?;
let body = args.eat().unwrap_or_default();
Ok(ellipse_impl(width, height, aspect, fill, body))
let body = args.eat();
Ok(shape_impl(ShapeKind::Circle, width, height, fill, body))
}
fn ellipse_impl(
width: Option<Linear>,
height: Option<Linear>,
aspect: Option<N64>,
fn shape_impl(
shape: ShapeKind,
mut width: Option<Linear>,
mut height: Option<Linear>,
fill: Option<Color>,
body: Template,
body: Option<Template>,
) -> Value {
Value::Template(Template::from_inline(move |style| {
// This padding ratio ensures that the rectangular padded region fits
// perfectly into the ellipse.
const PAD: f64 = 0.5 - SQRT_2 / 4.0;
let mut node = LayoutNode::new(FixedNode {
width,
height,
aspect,
child: LayoutNode::new(PadNode {
padding: Sides::splat(Relative::new(PAD).into()),
child: body.to_stack(style).into(),
}),
// 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,
});
}
if let Some(fill) = fill {
node = LayoutNode::new(BackgroundNode {
shape: BackgroundShape::Ellipse,
fill: Paint::Color(fill),
child: node,
});
}
node
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).into()),
}))
}

View File

@ -1,5 +1,7 @@
use super::*;
use crate::layout::{FixedNode, GridNode, PadNode, StackChild, StackNode, TrackSizing};
use crate::layout::{
GridNode, PadNode, ShapeKind, ShapeNode, StackChild, StackNode, TrackSizing,
};
use crate::style::{Paper, PaperClass};
/// `page`: Configure pages.
@ -146,13 +148,15 @@ pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
pub fn boxed(_: &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.eat().unwrap_or_default();
Ok(Value::Template(Template::from_inline(move |style| {
FixedNode {
ShapeNode {
shape: ShapeKind::Rect,
width,
height,
aspect: None,
child: body.to_stack(style).into(),
fill: fill.map(Paint::Color),
child: Some(body.to_stack(style).into()),
}
})))
}

View File

@ -20,7 +20,6 @@ use crate::diag::{At, TypResult};
use crate::eval::{Args, Array, EvalContext, Scope, Str, Template, Value};
use crate::font::{FontFamily, FontStretch, FontStyle, FontWeight, VerticalFontMetric};
use crate::geom::*;
use crate::layout::LayoutNode;
use crate::style::Style;
use crate::syntax::{Span, Spanned};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -10,18 +10,12 @@
#let university = [*Technische Universität {city}*]
#let faculty = [*Fakultät II, Institut for Mathematik*]
// The `box` function just places content into a rectangular container. When
// the only argument to a function is a template, the parentheses can be omitted
// (i.e. `f[a]` is the same as `f([a])`).
#box[
// Backslash adds a forced line break.
#university \
#faculty \
Sekretariat MA \
Dr. Max Mustermann \
Ola Nordmann, John Doe
]
#align(right, box[*WiSe 2019/2020* \ Woche 3])
// Backslashs add forced line breaks.
#university #align(right)[*WiSe 2019/2020*] \
#faculty #align(right)[Woche 3] \
Sekretariat MA \
Dr. Max Mustermann \
Ola Nordmann, John Doe
// Adds vertical spacing.
#v(6mm)

View File

@ -1,5 +1,9 @@
// Test the `circle` function.
---
// Default circle.
#circle()
---
// Test auto sizing.
@ -29,20 +33,15 @@ Expanded by height.
---
// Test relative sizing.
#rect(width: 100%, height: 50pt, fill: rgb("aaa"))[
#rect(width: 100pt, height: 50pt, fill: rgb("aaa"))[
#align(center, center)
#font(fill: white)
#circle(radius: 10pt, fill: eastern)[A]
#circle(height: 60%, fill: eastern)[B]
#circle(width: 20% + 20pt, fill: eastern)[C]
#circle(radius: 10pt, fill: eastern)[A] // D=20pt
#circle(height: 60%, fill: eastern)[B] // D=30pt
#circle(width: 20% + 20pt, fill: eastern)[C] // D=40pt
]
---
// Radius wins over width and height.
// Error: 23-34 unexpected argument
#circle(radius: 10pt, width: 50pt, height: 100pt, fill: eastern)
---
// Width wins over height.
// Error: 9-21 unexpected argument
#circle(height: 50pt, width: 20pt, fill: eastern)

View File

@ -1,5 +1,9 @@
// Test the `ellipse` function.
---
// Default ellipse.
#ellipse()
---
100% rect in 100% ellipse in fixed rect. \
#rect(width: 3cm, height: 2cm, fill: rgb("2a631a"),

View File

@ -1,27 +1,25 @@
// Test shapes.
---
// Test the `rect` function.
---
// Default rectangle.
#rect()
---
#page(width: 150pt)
// Fit to text.
#rect(fill: conifer)[Textbox]
// Empty with fixed width and height.
#rect(width: 3cm, height: 12pt, fill: rgb("CB4CED"))
#rect(width: 3cm, height: 12pt, fill: rgb("ed8a4c"))
// Fixed width, text height.
#rect(width: 2cm, fill: rgb("9650D6"), pad(5pt)[Fixed and padded])
#rect(width: 2cm, fill: rgb("9650d6"), pad(5pt)[Fixed and padded])
// Page width, fixed height.
#rect(height: 1cm, width: 100%, fill: rgb("734CED"))[Topleft]
#rect(height: 1cm, width: 100%, fill: rgb("734ced"))[Topleft]
// Not visible, but creates a gap between the boxes above and below
// due to line spacing.
#rect(width: 1in, fill: rgb("ff0000"))
// These are in a row!
#rect(width: 0.5in, height: 10pt, fill: rgb("D6CD67"))
#rect(width: 0.5in, height: 10pt, fill: rgb("EDD466"))
#rect(width: 0.5in, height: 10pt, fill: rgb("E3BE62"))
// These are inline with text.
\{#rect(width: 0.5in, height: 7pt, fill: rgb("d6cd67"))
#rect(width: 0.5in, height: 7pt, fill: rgb("edd466"))
#rect(width: 0.5in, height: 7pt, fill: rgb("e3be62"))\}

View File

@ -1,5 +1,10 @@
// Test the `square` function.
---
// Default square.
#square()
#square[hey!]
---
// Test auto-sized square.
#square(fill: eastern)[
@ -7,6 +12,7 @@
#align(center)
#pad(5pt)[Typst]
]
---
// Test relative-sized child.
#square(fill: eastern)[
@ -15,14 +21,14 @@
]
---
// Test height overflow.
// Test text overflowing height.
#page(width: 75pt, height: 100pt)
#square(fill: conifer)[
But, soft! what light through yonder window breaks?
]
---
// Test width overflow.
// Test required height overflowing page.
#page(width: 100pt, height: 75pt)
#square(fill: conifer)[
But, soft! what light through yonder window breaks?

View File

@ -1,23 +1,23 @@
// Test grid layouts.
---
#let rect(width, fill) = rect(width: width, height: 2cm, fill: fill)
#let cell(width, color) = rect(width: width, height: 2cm, fill: color)
#page(width: 100pt, height: 140pt)
#grid(
columns: (auto, 1fr, 3fr, 0.25cm, 3%, 2mm + 10%),
rect(0.5cm, rgb("2a631a")),
rect(100%, forest),
rect(100%, conifer),
rect(100%, rgb("ff0000")),
rect(100%, rgb("00ff00")),
rect(80%, rgb("00faf0")),
rect(1cm, rgb("00ff00")),
rect(0.5cm, rgb("2a631a")),
rect(100%, forest),
rect(100%, conifer),
rect(100%, rgb("ff0000")),
rect(100%, rgb("00ff00")),
cell(0.5cm, rgb("2a631a")),
cell(100%, forest),
cell(100%, conifer),
cell(100%, rgb("ff0000")),
cell(100%, rgb("00ff00")),
cell(80%, rgb("00faf0")),
cell(1cm, rgb("00ff00")),
cell(0.5cm, rgb("2a631a")),
cell(100%, forest),
cell(100%, conifer),
cell(100%, rgb("ff0000")),
cell(100%, rgb("00ff00")),
)
#grid()
@ -51,6 +51,7 @@
#grid(
columns: (1fr,),
rows: (1fr, auto, 2fr),
[], rect(width: 100%)[A bit more to the top], [],
[],
box(width: 100%)[A bit more to the top],
[],
)