Make all nodes block-level

This commit is contained in:
Laurenz 2023-02-12 18:58:39 +01:00
parent f4856c18b9
commit 3ffa7393f0
54 changed files with 378 additions and 272 deletions

View File

@ -38,7 +38,7 @@ use crate::prelude::*;
/// ## Category /// ## Category
/// layout /// layout
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct BoxNode { pub struct BoxNode {
/// How to size the content horizontally and vertically. /// How to size the content horizontally and vertically.
@ -99,8 +99,6 @@ impl Layout for BoxNode {
} }
} }
impl Inline for BoxNode {}
/// # Block /// # Block
/// A block-level container that places content into a separate flow. /// A block-level container that places content into a separate flow.
/// ///

View File

@ -2,6 +2,7 @@ use typst::model::Style;
use super::{AlignNode, BlockNode, ColbreakNode, ParNode, PlaceNode, Spacing, VNode}; use super::{AlignNode, BlockNode, ColbreakNode, ParNode, PlaceNode, Spacing, VNode};
use crate::prelude::*; use crate::prelude::*;
use crate::visualize::{CircleNode, EllipseNode, ImageNode, RectNode, SquareNode};
/// Arrange spacing, paragraphs and block-level nodes into a flow. /// Arrange spacing, paragraphs and block-level nodes into a flow.
/// ///
@ -31,8 +32,17 @@ impl Layout for FlowNode {
let barrier = Style::Barrier(child.id()); let barrier = Style::Barrier(child.id());
let styles = styles.chain_one(&barrier); let styles = styles.chain_one(&barrier);
layouter.layout_par(vt, node, styles)?; layouter.layout_par(vt, node, styles)?;
} else if child.is::<RectNode>()
|| child.is::<SquareNode>()
|| child.is::<EllipseNode>()
|| child.is::<CircleNode>()
|| child.is::<ImageNode>()
{
let barrier = Style::Barrier(child.id());
let styles = styles.chain_one(&barrier);
layouter.layout_single(vt, child, styles)?;
} else if child.has::<dyn Layout>() { } else if child.has::<dyn Layout>() {
layouter.layout_block(vt, child, styles)?; layouter.layout_multiple(vt, child, styles)?;
} else if child.is::<ColbreakNode>() { } else if child.is::<ColbreakNode>() {
layouter.finish_region(); layouter.finish_region();
} else { } else {
@ -157,8 +167,25 @@ impl<'a> FlowLayouter<'a> {
Ok(()) Ok(())
} }
/// Layout a block. /// Layout into a single region.
fn layout_block( fn layout_single(
&mut self,
vt: &mut Vt,
content: &Content,
styles: StyleChain,
) -> SourceResult<()> {
let aligns = styles.get(AlignNode::ALIGNS).resolve(styles);
let sticky = styles.get(BlockNode::STICKY);
let pod = Regions::one(self.regions.base(), Axes::splat(false));
let layoutable = content.with::<dyn Layout>().unwrap();
let frame = layoutable.layout(vt, styles, pod)?.into_frame();
self.layout_item(FlowItem::Frame(frame, aligns, sticky));
self.last_was_par = false;
Ok(())
}
/// Layout into multiple regions.
fn layout_multiple(
&mut self, &mut self,
vt: &mut Vt, vt: &mut Vt,
block: &Content, block: &Content,

View File

@ -21,7 +21,7 @@ use crate::prelude::*;
/// ## Category /// ## Category
/// layout /// layout
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Show)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct HideNode(pub Content); pub struct HideNode(pub Content);
@ -39,19 +39,8 @@ impl HideNode {
} }
} }
impl Layout for HideNode { impl Show for HideNode {
fn layout( fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> {
&self, Ok(self.0.clone().styled(Meta::DATA, vec![Meta::Hidden]))
vt: &mut Vt,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let mut fragment = self.0.layout(vt, styles, regions)?;
for frame in &mut fragment {
frame.clear();
}
Ok(fragment)
} }
} }
impl Inline for HideNode {}

View File

@ -57,6 +57,7 @@ use crate::meta::DocumentNode;
use crate::prelude::*; use crate::prelude::*;
use crate::shared::BehavedBuilder; use crate::shared::BehavedBuilder;
use crate::text::{LinebreakNode, SmartQuoteNode, SpaceNode, TextNode}; use crate::text::{LinebreakNode, SmartQuoteNode, SpaceNode, TextNode};
use crate::visualize::{CircleNode, EllipseNode, ImageNode, RectNode, SquareNode};
/// Root-level layout. /// Root-level layout.
#[capability] #[capability]
@ -144,10 +145,6 @@ impl Layout for Content {
} }
} }
/// Inline-level layout.
#[capability]
pub trait Inline: Layout {}
/// Realize into a node that is capable of root-level layout. /// Realize into a node that is capable of root-level layout.
fn realize_root<'a>( fn realize_root<'a>(
vt: &mut Vt, vt: &mut Vt,
@ -173,7 +170,14 @@ fn realize_block<'a>(
content: &'a Content, content: &'a Content,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<(Content, StyleChain<'a>)> { ) -> SourceResult<(Content, StyleChain<'a>)> {
if content.has::<dyn Layout>() && !applicable(content, styles) { if content.has::<dyn Layout>()
&& !content.is::<RectNode>()
&& !content.is::<SquareNode>()
&& !content.is::<EllipseNode>()
&& !content.is::<CircleNode>()
&& !content.is::<ImageNode>()
&& !applicable(content, styles)
{
return Ok((content.clone(), styles)); return Ok((content.clone(), styles));
} }
@ -464,18 +468,19 @@ struct ParBuilder<'a>(BehavedBuilder<'a>);
impl<'a> ParBuilder<'a> { impl<'a> ParBuilder<'a> {
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
if content.is::<SpaceNode>() if content.is::<SpaceNode>()
|| content.is::<LinebreakNode>() || content.is::<TextNode>()
|| content.is::<HNode>() || content.is::<HNode>()
|| content.is::<SmartQuoteNode>() || content.is::<SmartQuoteNode>()
|| content.is::<TextNode>() || content.is::<LinebreakNode>()
|| content.is::<FormulaNode>() || content.is::<BoxNode>()
|| content.has::<dyn Inline>() || content.is::<RepeatNode>()
|| content.to::<FormulaNode>().map_or(false, |node| !node.block)
{ {
self.0.push(content.clone(), styles); self.0.push(content.clone(), styles);
return true; return true;
} }
if content.has::<dyn LayoutMath>() { if !content.is::<FormulaNode>() && content.has::<dyn LayoutMath>() {
let formula = FormulaNode { body: content.clone(), block: false }.pack(); let formula = FormulaNode { body: content.clone(), block: false }.pack();
self.0.push(formula, styles); self.0.push(formula, styles);
return true; return true;

View File

@ -500,12 +500,14 @@ fn collect<'a>(
.0 .0
.items() .items()
.find_map(|child| { .find_map(|child| {
if child.is::<TextNode>() || child.is::<SmartQuoteNode>() { if child.with::<dyn Behave>().map_or(false, |behaved| {
Some(true) behaved.behaviour() == Behaviour::Ignorant
} else if child.has::<dyn Inline>() { }) {
Some(false)
} else {
None None
} else if child.is::<TextNode>() || child.is::<SmartQuoteNode>() {
Some(true)
} else {
Some(false)
} }
}) })
.unwrap_or_default() .unwrap_or_default()
@ -558,11 +560,9 @@ fn collect<'a>(
} else if let Some(&node) = child.to::<HNode>() { } else if let Some(&node) = child.to::<HNode>() {
full.push(SPACING_REPLACE); full.push(SPACING_REPLACE);
Segment::Spacing(node.amount) Segment::Spacing(node.amount)
} else if child.has::<dyn Inline>() { } else {
full.push(NODE_REPLACE); full.push(NODE_REPLACE);
Segment::Inline(child) Segment::Inline(child)
} else {
panic!("unexpected par child: {child:?}");
}; };
if let Some(last) = full.chars().last() { if let Some(last) = full.chars().last() {

View File

@ -26,7 +26,7 @@ use crate::prelude::*;
/// ## Category /// ## Category
/// layout /// layout
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct RepeatNode(pub Content); pub struct RepeatNode(pub Content);
@ -54,5 +54,3 @@ impl Layout for RepeatNode {
self.0.layout(vt, styles, regions) self.0.layout(vt, styles, regions)
} }
} }
impl Inline for RepeatNode {}

View File

@ -169,7 +169,7 @@ enum StackItem {
/// Fractional spacing between other items. /// Fractional spacing between other items.
Fractional(Fr), Fractional(Fr),
/// A frame for a layouted block. /// A frame for a layouted block.
Frame(Frame, Align), Frame(Frame, Axes<Align>),
} }
impl<'a> StackLayouter<'a> { impl<'a> StackLayouter<'a> {
@ -239,7 +239,7 @@ impl<'a> StackLayouter<'a> {
styles.get(AlignNode::ALIGNS) styles.get(AlignNode::ALIGNS)
}; };
let align = aligns.get(self.axis).resolve(styles); let aligns = aligns.resolve(styles);
let fragment = block.layout(vt, styles, self.regions)?; let fragment = block.layout(vt, styles, self.regions)?;
let len = fragment.len(); let len = fragment.len();
for (i, frame) in fragment.into_iter().enumerate() { for (i, frame) in fragment.into_iter().enumerate() {
@ -257,7 +257,7 @@ impl<'a> StackLayouter<'a> {
self.used.main += gen.main; self.used.main += gen.main;
self.used.cross.set_max(gen.cross); self.used.cross.set_max(gen.cross);
self.items.push(StackItem::Frame(frame, align)); self.items.push(StackItem::Frame(frame, aligns));
if i + 1 < len { if i + 1 < len {
self.finish_region(); self.finish_region();
@ -291,24 +291,30 @@ impl<'a> StackLayouter<'a> {
match item { match item {
StackItem::Absolute(v) => cursor += v, StackItem::Absolute(v) => cursor += v,
StackItem::Fractional(v) => cursor += v.share(self.fr, remaining), StackItem::Fractional(v) => cursor += v.share(self.fr, remaining),
StackItem::Frame(frame, align) => { StackItem::Frame(frame, aligns) => {
if self.dir.is_positive() { if self.dir.is_positive() {
ruler = ruler.max(align); ruler = ruler.max(aligns.get(self.axis));
} else { } else {
ruler = ruler.min(align); ruler = ruler.min(aligns.get(self.axis));
} }
// Align along the block axis. // Align along the main axis.
let parent = size.get(self.axis); let parent = size.get(self.axis);
let child = frame.size().get(self.axis); let child = frame.size().get(self.axis);
let block = ruler.position(parent - self.used.main) let main = ruler.position(parent - self.used.main)
+ if self.dir.is_positive() { + if self.dir.is_positive() {
cursor cursor
} else { } else {
self.used.main - child - cursor self.used.main - child - cursor
}; };
let pos = Gen::new(Abs::zero(), block).to_point(self.axis); // Align along the cross axis.
let other = self.axis.other();
let cross = aligns
.get(other)
.position(size.get(other) - frame.size().get(other));
let pos = Gen::new(cross, main).to_point(self.axis);
cursor += child; cursor += child;
output.push_frame(pos, frame); output.push_frame(pos, frame);
} }

View File

@ -39,7 +39,7 @@ use crate::prelude::*;
/// ## Category /// ## Category
/// layout /// layout
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct MoveNode { pub struct MoveNode {
/// The offset by which to move the content. /// The offset by which to move the content.
@ -75,18 +75,15 @@ impl Layout for MoveNode {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let mut fragment = self.body.layout(vt, styles, regions)?; let pod = Regions::one(regions.base(), Axes::splat(false));
for frame in &mut fragment { let mut frame = self.body.layout(vt, styles, pod)?.into_frame();
let delta = self.delta.resolve(styles); let delta = self.delta.resolve(styles);
let delta = delta.zip(regions.base()).map(|(d, s)| d.relative_to(s)); let delta = delta.zip(regions.base()).map(|(d, s)| d.relative_to(s));
frame.translate(delta.to_point()); frame.translate(delta.to_point());
} Ok(Fragment::frame(frame))
Ok(fragment)
} }
} }
impl Inline for MoveNode {}
/// # Rotate /// # Rotate
/// Rotate content with affecting layout. /// Rotate content with affecting layout.
/// ///
@ -116,7 +113,7 @@ impl Inline for MoveNode {}
/// ## Category /// ## Category
/// layout /// layout
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct RotateNode { pub struct RotateNode {
/// The angle by which to rotate the node. /// The angle by which to rotate the node.
@ -169,21 +166,18 @@ impl Layout for RotateNode {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let mut fragment = self.body.layout(vt, styles, regions)?; let pod = Regions::one(regions.base(), Axes::splat(false));
for frame in &mut fragment { let mut frame = self.body.layout(vt, styles, pod)?.into_frame();
let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON);
let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s));
let transform = Transform::translate(x, y) let ts = Transform::translate(x, y)
.pre_concat(Transform::rotate(self.angle)) .pre_concat(Transform::rotate(self.angle))
.pre_concat(Transform::translate(-x, -y)); .pre_concat(Transform::translate(-x, -y));
frame.transform(transform); frame.transform(ts);
} Ok(Fragment::frame(frame))
Ok(fragment)
} }
} }
impl Inline for RotateNode {}
/// # Scale /// # Scale
/// Scale content without affecting layout. /// Scale content without affecting layout.
/// ///
@ -214,7 +208,7 @@ impl Inline for RotateNode {}
/// ## Category /// ## Category
/// layout /// layout
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct ScaleNode { pub struct ScaleNode {
/// Scaling factor. /// Scaling factor.
@ -262,17 +256,14 @@ impl Layout for ScaleNode {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let mut fragment = self.body.layout(vt, styles, regions)?; let pod = Regions::one(regions.base(), Axes::splat(false));
for frame in &mut fragment { let mut frame = self.body.layout(vt, styles, pod)?.into_frame();
let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON);
let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s));
let transform = Transform::translate(x, y) let transform = Transform::translate(x, y)
.pre_concat(Transform::scale(self.factor.x, self.factor.y)) .pre_concat(Transform::scale(self.factor.x, self.factor.y))
.pre_concat(Transform::translate(-x, -y)); .pre_concat(Transform::translate(-x, -y));
frame.transform(transform); frame.transform(transform);
} Ok(Fragment::frame(frame))
Ok(fragment)
} }
} }
impl Inline for ScaleNode {}

View File

@ -141,7 +141,7 @@ pub fn module() -> Module {
/// ## Category /// ## Category
/// math /// math
#[func] #[func]
#[capable(Show, Finalize, Layout, Inline, LayoutMath)] #[capable(Show, Finalize, Layout, LayoutMath)]
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct FormulaNode { pub struct FormulaNode {
/// Whether the formula is displayed as a separate block. /// Whether the formula is displayed as a separate block.
@ -229,8 +229,6 @@ impl Layout for FormulaNode {
} }
} }
impl Inline for FormulaNode {}
#[capability] #[capability]
pub trait LayoutMath { pub trait LayoutMath {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>; fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>;

View File

@ -28,6 +28,6 @@ pub use typst::util::{format_eco, EcoString};
pub use typst::World; pub use typst::World;
#[doc(no_inline)] #[doc(no_inline)]
pub use crate::layout::{Fragment, Inline, Layout, Regions}; pub use crate::layout::{Fragment, Layout, Regions};
#[doc(no_inline)] #[doc(no_inline)]
pub use crate::shared::{Behave, Behaviour, ContentExt, StyleMapExt}; pub use crate::shared::{Behave, Behaviour, ContentExt, StyleMapExt};

View File

@ -32,9 +32,13 @@ use crate::prelude::*;
/// ## Category /// ## Category
/// visualize /// visualize
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct ImageNode(pub Image); pub struct ImageNode {
pub image: Image,
pub width: Smart<Rel<Length>>,
pub height: Smart<Rel<Length>>,
}
#[node] #[node]
impl ImageNode { impl ImageNode {
@ -57,10 +61,9 @@ impl ImageNode {
}; };
let image = Image::new(buffer, format).at(span)?; let image = Image::new(buffer, format).at(span)?;
let width = args.named("width")?; let width = args.named("width")?.unwrap_or_default();
let height = args.named("height")?; let height = args.named("height")?.unwrap_or_default();
Ok(ImageNode { image, width, height }.pack())
Ok(ImageNode(image).pack().boxed(Axes::new(width, height)))
} }
} }
@ -71,22 +74,28 @@ impl Layout for ImageNode {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let pxw = self.0.width() as f64; let sizing = Axes::new(self.width, self.height);
let pxh = self.0.height() as f64; let region = sizing
let px_ratio = pxw / pxh; .zip(regions.base())
.map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r)))
.unwrap_or(regions.base());
let expand = sizing.as_ref().map(Smart::is_custom) | regions.expand;
let region_ratio = region.x / region.y;
// Find out whether the image is wider or taller than the target size. // Find out whether the image is wider or taller than the target size.
let Regions { size: first, expand, .. } = regions; let pxw = self.image.width() as f64;
let region_ratio = first.x / first.y; let pxh = self.image.height() as f64;
let px_ratio = pxw / pxh;
let wide = px_ratio > region_ratio; let wide = px_ratio > region_ratio;
// The space into which the image will be placed according to its fit. // The space into which the image will be placed according to its fit.
let target = if expand.x && expand.y { let target = if expand.x && expand.y {
first region
} else if expand.x || (!expand.y && wide && first.x.is_finite()) { } else if expand.x || (!expand.y && wide && region.x.is_finite()) {
Size::new(first.x, first.y.min(first.x.safe_div(px_ratio))) Size::new(region.x, region.y.min(region.x.safe_div(px_ratio)))
} else if first.y.is_finite() { } else if region.y.is_finite() {
Size::new(first.x.min(first.y * px_ratio), first.y) Size::new(region.x.min(region.y * px_ratio), region.y)
} else { } else {
Size::new(Abs::pt(pxw), Abs::pt(pxh)) Size::new(Abs::pt(pxw), Abs::pt(pxh))
}; };
@ -108,7 +117,7 @@ impl Layout for ImageNode {
// the frame to the target size, center aligning the image in the // the frame to the target size, center aligning the image in the
// process. // process.
let mut frame = Frame::new(fitted); let mut frame = Frame::new(fitted);
frame.push(Point::zero(), Element::Image(self.0.clone(), fitted)); frame.push(Point::zero(), Element::Image(self.image.clone(), fitted));
frame.resize(target, Align::CENTER_HORIZON); frame.resize(target, Align::CENTER_HORIZON);
// Create a clipping group if only part of the image should be visible. // Create a clipping group if only part of the image should be visible.
@ -123,8 +132,6 @@ impl Layout for ImageNode {
} }
} }
impl Inline for ImageNode {}
/// How an image should adjust itself to a given area. /// How an image should adjust itself to a given area.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ImageFit { pub enum ImageFit {

View File

@ -28,7 +28,7 @@ use crate::prelude::*;
/// ## Category /// ## Category
/// visualize /// visualize
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct LineNode { pub struct LineNode {
/// Where the line starts. /// Where the line starts.
@ -61,7 +61,7 @@ impl LineNode {
Some(end) => end.zip(start).map(|(to, from)| to - from), Some(end) => end.zip(start).map(|(to, from)| to - from),
None => { None => {
let length = let length =
args.named::<Rel<Length>>("length")?.unwrap_or(Abs::cm(1.0).into()); args.named::<Rel<Length>>("length")?.unwrap_or(Abs::pt(30.0).into());
let angle = args.named::<Angle>("angle")?.unwrap_or_default(); let angle = args.named::<Angle>("angle")?.unwrap_or_default();
let x = angle.cos() * length; let x = angle.cos() * length;
@ -106,5 +106,3 @@ impl Layout for LineNode {
Ok(Fragment::frame(frame)) Ok(Fragment::frame(frame))
} }
} }
impl Inline for LineNode {}

View File

@ -33,9 +33,13 @@ use crate::prelude::*;
/// ## Category /// ## Category
/// visualize /// visualize
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct RectNode(pub Option<Content>); pub struct RectNode {
pub body: Option<Content>,
pub width: Smart<Rel<Length>>,
pub height: Smart<Rel<Length>>,
}
#[node] #[node]
impl RectNode { impl RectNode {
@ -155,14 +159,15 @@ impl RectNode {
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let width = args.named("width")?; let width = args.named("width")?.unwrap_or_default();
let height = args.named("height")?; let height = args.named("height")?.unwrap_or_default();
Ok(Self(args.eat()?).pack().boxed(Axes::new(width, height))) let body = args.eat()?;
Ok(Self { body, width, height }.pack())
} }
fn field(&self, name: &str) -> Option<Value> { fn field(&self, name: &str) -> Option<Value> {
match name { match name {
"body" => match &self.0 { "body" => match &self.body {
Some(body) => Some(Value::Content(body.clone())), Some(body) => Some(Value::Content(body.clone())),
None => Some(Value::None), None => Some(Value::None),
}, },
@ -181,7 +186,8 @@ impl Layout for RectNode {
layout( layout(
vt, vt,
ShapeKind::Rect, ShapeKind::Rect,
&self.0, &self.body,
Axes::new(self.width, self.height),
styles.get(Self::FILL), styles.get(Self::FILL),
styles.get(Self::STROKE), styles.get(Self::STROKE),
styles.get(Self::INSET), styles.get(Self::INSET),
@ -193,8 +199,6 @@ impl Layout for RectNode {
} }
} }
impl Inline for RectNode {}
/// # Square /// # Square
/// A square with optional content. /// A square with optional content.
/// ///
@ -237,9 +241,13 @@ impl Inline for RectNode {}
/// ## Category /// ## Category
/// visualize /// visualize
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct SquareNode(pub Option<Content>); pub struct SquareNode {
pub body: Option<Content>,
pub width: Smart<Rel<Length>>,
pub height: Smart<Rel<Length>>,
}
#[node] #[node]
impl SquareNode { impl SquareNode {
@ -270,22 +278,24 @@ impl SquareNode {
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let size = args.named::<Length>("size")?.map(Rel::from); let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
let width = match size { let width = match size {
None => args.named("width")?, None => args.named("width")?,
size => size, size => size,
}; }
.unwrap_or_default();
let height = match size { let height = match size {
None => args.named("height")?, None => args.named("height")?,
size => size, size => size,
}; }
Ok(Self(args.eat()?).pack().boxed(Axes::new(width, height))) .unwrap_or_default();
let body = args.eat()?;
Ok(Self { body, width, height }.pack())
} }
fn field(&self, name: &str) -> Option<Value> { fn field(&self, name: &str) -> Option<Value> {
match name { match name {
"body" => match &self.0 { "body" => match &self.body {
Some(body) => Some(Value::Content(body.clone())), Some(body) => Some(Value::Content(body.clone())),
None => Some(Value::None), None => Some(Value::None),
}, },
@ -304,7 +314,8 @@ impl Layout for SquareNode {
layout( layout(
vt, vt,
ShapeKind::Square, ShapeKind::Square,
&self.0, &self.body,
Axes::new(self.width, self.height),
styles.get(Self::FILL), styles.get(Self::FILL),
styles.get(Self::STROKE), styles.get(Self::STROKE),
styles.get(Self::INSET), styles.get(Self::INSET),
@ -316,8 +327,6 @@ impl Layout for SquareNode {
} }
} }
impl Inline for SquareNode {}
/// # Ellipse /// # Ellipse
/// An ellipse with optional content. /// An ellipse with optional content.
/// ///
@ -350,9 +359,13 @@ impl Inline for SquareNode {}
/// ## Category /// ## Category
/// visualize /// visualize
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct EllipseNode(pub Option<Content>); pub struct EllipseNode {
pub body: Option<Content>,
pub width: Smart<Rel<Length>>,
pub height: Smart<Rel<Length>>,
}
#[node] #[node]
impl EllipseNode { impl EllipseNode {
@ -378,14 +391,15 @@ impl EllipseNode {
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let width = args.named("width")?; let width = args.named("width")?.unwrap_or_default();
let height = args.named("height")?; let height = args.named("height")?.unwrap_or_default();
Ok(Self(args.eat()?).pack().boxed(Axes::new(width, height))) let body = args.eat()?;
Ok(Self { body, width, height }.pack())
} }
fn field(&self, name: &str) -> Option<Value> { fn field(&self, name: &str) -> Option<Value> {
match name { match name {
"body" => match &self.0 { "body" => match &self.body {
Some(body) => Some(Value::Content(body.clone())), Some(body) => Some(Value::Content(body.clone())),
None => Some(Value::None), None => Some(Value::None),
}, },
@ -404,7 +418,8 @@ impl Layout for EllipseNode {
layout( layout(
vt, vt,
ShapeKind::Ellipse, ShapeKind::Ellipse,
&self.0, &self.body,
Axes::new(self.width, self.height),
styles.get(Self::FILL), styles.get(Self::FILL),
styles.get(Self::STROKE).map(Sides::splat), styles.get(Self::STROKE).map(Sides::splat),
styles.get(Self::INSET), styles.get(Self::INSET),
@ -416,8 +431,6 @@ impl Layout for EllipseNode {
} }
} }
impl Inline for EllipseNode {}
/// # Circle /// # Circle
/// A circle with optional content. /// A circle with optional content.
/// ///
@ -458,9 +471,13 @@ impl Inline for EllipseNode {}
/// ## Category /// ## Category
/// visualize /// visualize
#[func] #[func]
#[capable(Layout, Inline)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct CircleNode(pub Option<Content>); pub struct CircleNode {
pub body: Option<Content>,
pub width: Smart<Rel<Length>>,
pub height: Smart<Rel<Length>>,
}
#[node] #[node]
impl CircleNode { impl CircleNode {
@ -486,22 +503,26 @@ impl CircleNode {
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let size = args.named::<Length>("radius")?.map(|r| 2.0 * Rel::from(r)); let size = args
.named::<Smart<Length>>("radius")?
.map(|s| s.map(|r| 2.0 * Rel::from(r)));
let width = match size { let width = match size {
None => args.named("width")?, None => args.named("width")?,
size => size, size => size,
}; }
.unwrap_or_default();
let height = match size { let height = match size {
None => args.named("height")?, None => args.named("height")?,
size => size, size => size,
}; }
Ok(Self(args.eat()?).pack().boxed(Axes::new(width, height))) .unwrap_or_default();
let body = args.eat()?;
Ok(Self { body, width, height }.pack())
} }
fn field(&self, name: &str) -> Option<Value> { fn field(&self, name: &str) -> Option<Value> {
match name { match name {
"body" => match &self.0 { "body" => match &self.body {
Some(body) => Some(Value::Content(body.clone())), Some(body) => Some(Value::Content(body.clone())),
None => Some(Value::None), None => Some(Value::None),
}, },
@ -520,7 +541,8 @@ impl Layout for CircleNode {
layout( layout(
vt, vt,
ShapeKind::Circle, ShapeKind::Circle,
&self.0, &self.body,
Axes::new(self.width, self.height),
styles.get(Self::FILL), styles.get(Self::FILL),
styles.get(Self::STROKE).map(Sides::splat), styles.get(Self::STROKE).map(Sides::splat),
styles.get(Self::INSET), styles.get(Self::INSET),
@ -532,13 +554,12 @@ impl Layout for CircleNode {
} }
} }
impl Inline for CircleNode {}
/// Layout a shape. /// Layout a shape.
fn layout( fn layout(
vt: &mut Vt, vt: &mut Vt,
kind: ShapeKind, kind: ShapeKind,
body: &Option<Content>, body: &Option<Content>,
sizing: Axes<Smart<Rel<Length>>>,
fill: Option<Paint>, fill: Option<Paint>,
stroke: Smart<Sides<Option<PartialStroke<Abs>>>>, stroke: Smart<Sides<Option<PartialStroke<Abs>>>>,
mut inset: Sides<Rel<Abs>>, mut inset: Sides<Rel<Abs>>,
@ -547,29 +568,28 @@ fn layout(
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let resolved = sizing
.zip(regions.base())
.map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r)));
let mut frame; let mut frame;
if let Some(child) = body { if let Some(child) = body {
let region = resolved.unwrap_or(regions.base());
if kind.is_round() { if kind.is_round() {
inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0)); inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
} }
// Pad the child. // Pad the child.
let child = child.clone().padded(inset.map(|side| side.map(Length::from))); let child = child.clone().padded(inset.map(|side| side.map(Length::from)));
let pod = Regions::one(regions.size, regions.expand); let expand = sizing.as_ref().map(Smart::is_custom);
let pod = Regions::one(region, expand);
frame = child.layout(vt, styles, pod)?.into_frame(); frame = child.layout(vt, styles, pod)?.into_frame();
// Relayout with full expansion into square region to make sure // Relayout with full expansion into square region to make sure
// the result is really a square or circle. // the result is really a square or circle.
if kind.is_quadratic() { if kind.is_quadratic() {
let length = if regions.expand.x || regions.expand.y { let length = frame.size().max_by_side().min(region.min_by_side());
let target = regions.expand.select(regions.size, Size::zero());
target.x.max(target.y)
} else {
let size = frame.size();
let desired = size.x.max(size.y);
desired.min(regions.size.x).min(regions.size.y)
};
let size = Size::splat(length); let size = Size::splat(length);
let pod = Regions::one(size, Axes::splat(true)); let pod = Regions::one(size, Axes::splat(true));
frame = child.layout(vt, styles, pod)?.into_frame(); frame = child.layout(vt, styles, pod)?.into_frame();
@ -577,20 +597,11 @@ fn layout(
} else { } else {
// The default size that a shape takes on if it has no child and // The default size that a shape takes on if it has no child and
// enough space. // enough space.
let mut size = Size::new(Abs::pt(45.0), Abs::pt(30.0)).min(regions.size); let default = Size::new(Abs::pt(45.0), Abs::pt(30.0));
let mut size = resolved.unwrap_or(default.min(regions.base()));
if kind.is_quadratic() { if kind.is_quadratic() {
let length = if regions.expand.x || regions.expand.y { size = Size::splat(size.min_by_side());
let target = regions.expand.select(regions.size, Size::zero());
target.x.max(target.y)
} else {
size.x.min(size.y)
};
size = Size::splat(length);
} else {
size = regions.expand.select(regions.size, size);
} }
frame = Frame::new(size); frame = Frame::new(size);
} }

View File

@ -267,6 +267,10 @@ impl Frame {
/// Attach the metadata from this style chain to the frame. /// Attach the metadata from this style chain to the frame.
pub fn meta(&mut self, styles: StyleChain) { pub fn meta(&mut self, styles: StyleChain) {
for meta in styles.get(Meta::DATA) { for meta in styles.get(Meta::DATA) {
if matches!(meta, Meta::Hidden) {
self.clear();
break;
}
self.push(Point::zero(), Element::Meta(meta, self.size)); self.push(Point::zero(), Element::Meta(meta, self.size));
} }
} }
@ -533,6 +537,8 @@ pub enum Meta {
/// An identifiable piece of content that produces something within the /// An identifiable piece of content that produces something within the
/// area this metadata is attached to. /// area this metadata is attached to.
Node(StableId, Content), Node(StableId, Content),
/// Indicates that the content is hidden.
Hidden,
} }
#[node] #[node]

View File

@ -288,6 +288,7 @@ fn write_frame(ctx: &mut PageContext, frame: &Frame) {
Element::Meta(meta, size) => match meta { Element::Meta(meta, size) => match meta {
Meta::Link(dest) => write_link(ctx, pos, dest, *size), Meta::Link(dest) => write_link(ctx, pos, dest, *size),
Meta::Node(_, _) => {} Meta::Node(_, _) => {}
Meta::Hidden => {}
}, },
} }
} }

View File

@ -61,6 +61,7 @@ fn render_frame(
Element::Meta(meta, _) => match meta { Element::Meta(meta, _) => match meta {
Meta::Link(_) => {} Meta::Link(_) => {}
Meta::Node(_, _) => {} Meta::Node(_, _) => {}
Meta::Hidden => {}
}, },
} }
} }

View File

@ -104,6 +104,16 @@ impl<T: Ord> Axes<T> {
pub fn max(self, other: Self) -> Self { pub fn max(self, other: Self) -> Self {
Self { x: self.x.max(other.x), y: self.y.max(other.y) } Self { x: self.x.max(other.x), y: self.y.max(other.y) }
} }
/// The minimum of width and height.
pub fn min_by_side(self) -> T {
self.x.min(self.y)
}
/// The minimum of width and height.
pub fn max_by_side(self) -> T {
self.x.max(self.y)
}
} }
impl<T> Get<Axis> for Axes<T> { impl<T> Get<Axis> for Axes<T> {
@ -189,6 +199,16 @@ impl<T> Axes<Option<T>> {
} }
} }
impl<T> Axes<Smart<T>> {
/// Unwrap the individual fields.
pub fn unwrap_or(self, other: Axes<T>) -> Axes<T> {
Axes {
x: self.x.unwrap_or(other.x),
y: self.y.unwrap_or(other.y),
}
}
}
impl Axes<bool> { impl Axes<bool> {
/// Select `t.x` if `self.x` is true and `f.x` otherwise and same for `y`. /// Select `t.x` if `self.x` is true and `f.x` otherwise and same for `y`.
pub fn select<T>(self, t: Axes<T>, f: Axes<T>) -> Axes<T> { pub fn select<T>(self, t: Axes<T>, f: Axes<T>) -> Axes<T> {

View File

@ -35,6 +35,16 @@ impl Point {
Self { x: Abs::zero(), y } Self { x: Abs::zero(), y }
} }
/// The component-wise minimum of this and another point.
pub fn min(self, other: Self) -> Self {
Self { x: self.x.min(other.x), y: self.y.min(other.y) }
}
/// The component-wise minimum of this and another point.
pub fn max(self, other: Self) -> Self {
Self { x: self.x.max(other.x), y: self.y.max(other.y) }
}
/// Transform the point with the given transformation. /// Transform the point with the given transformation.
pub fn transform(self, ts: Transform) -> Self { pub fn transform(self, ts: Transform) -> Self {
Self::new( Self::new(
@ -42,6 +52,11 @@ impl Point {
ts.ky.of(self.x) + ts.sy.of(self.y) + ts.ty, ts.ky.of(self.x) + ts.sy.of(self.y) + ts.ty,
) )
} }
/// Convert to a size.
pub fn to_size(self) -> Size {
Size::new(self.x, self.y)
}
} }
impl Numeric for Point { impl Numeric for Point {

View File

@ -10,6 +10,16 @@ pub enum Smart<T> {
} }
impl<T> Smart<T> { impl<T> Smart<T> {
/// Whether the value is `Auto`.
pub fn is_auto(&self) -> bool {
matches!(self, Self::Auto)
}
/// Whether this holds a custom value.
pub fn is_custom(&self) -> bool {
matches!(self, Self::Custom(_))
}
/// Map the contained custom value with `f`. /// Map the contained custom value with `f`.
pub fn map<F, U>(self, f: F) -> Smart<U> pub fn map<F, U>(self, f: F) -> Smart<U>
where where

Binary file not shown.

Before

Width:  |  Height:  |  Size: 506 B

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 769 B

After

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,22 +1,26 @@
// Test color modification methods. // Test color modification methods.
---
// Test CMYK color conversion.
#let c = cmyk(50%, 64%, 16%, 17%)
#stack(
dir: ltr,
spacing: 1fr,
rect(width: 1cm, fill: cmyk(69%, 11%, 69%, 41%)),
rect(width: 1cm, fill: c),
rect(width: 1cm, fill: c.negate()),
)
#for x in range(0, 11) {
box(square(size: 9pt, fill: c.lighten(x * 10%)))
}
#for x in range(0, 11) {
box(square(size: 9pt, fill: c.darken(x * 10%)))
}
--- ---
// Test gray color modification. // Test gray color modification.
// Ref: false
#test(luma(20%).lighten(50%), luma(60%)) #test(luma(20%).lighten(50%), luma(60%))
#test(luma(80%).darken(20%), luma(63.9%)) #test(luma(80%).darken(20%), luma(63.9%))
#test(luma(80%).negate(), luma(20%)) #test(luma(80%).negate(), luma(20%))
---
// Test CMYK color conversion.
// Ref: true
#let c = cmyk(50%, 64%, 16%, 17%)
#rect(width: 1cm, fill: cmyk(69%, 11%, 69%, 41%))
#rect(width: 1cm, fill: c)
#rect(width: 1cm, fill: c.negate())
#for x in range(0, 11) {
square(size: 9pt, fill: c.lighten(x * 10%))
}
#for x in range(0, 11) {
square(size: 9pt, fill: c.darken(x * 10%))
}

View File

@ -22,7 +22,7 @@
--- ---
// The inner rectangle should not be yellow here. // The inner rectangle should not be yellow here.
A #rect(fill: yellow, inset: 5pt, rect()) B A #box(rect(fill: yellow, inset: 5pt, rect())) B
--- ---
// The constructor property should still work // The constructor property should still work

View File

@ -28,7 +28,7 @@ my heading?
// Test integrated example. // Test integrated example.
#show heading: it => block({ #show heading: it => block({
set text(10pt) set text(10pt)
move(dy: -1pt)[📖] box(move(dy: -1pt)[📖])
h(5pt) h(5pt)
if it.level == 1 { if it.level == 1 {
underline(text(1.25em, blue, it.title)) underline(text(1.25em, blue, it.title))

View File

@ -2,12 +2,13 @@
--- ---
// Inline code. // Inline code.
#show raw.where(block: false): rect.with( #show raw.where(block: false): it => box(rect(
radius: 2pt, radius: 2pt,
outset: (y: 3pt), outset: (y: 3pt),
inset: (x: 3pt, y: 0pt), inset: (x: 3pt, y: 0pt),
fill: luma(230), fill: luma(230),
) it,
))
// Code blocks. // Code blocks.
#show raw.where(block: true): rect.with( #show raw.where(block: true): rect.with(

View File

@ -8,7 +8,7 @@ Die Zeitung Der Spiegel existiert.
--- ---
// Another classic example. // Another classic example.
#show "TeX": [T#h(-0.145em)#move(dy: 0.233em)[E]#h(-0.135em)X] #show "TeX": [T#h(-0.145em)#box(move(dy: 0.233em)[E])#h(-0.135em)X]
#show regex("(Lua)?(La)?TeX"): name => box(text("Latin Modern Roman")[#name]) #show regex("(Lua)?(La)?TeX"): name => box(text("Latin Modern Roman")[#name])
TeX, LaTeX, LuaTeX and LuaLaTeX! TeX, LaTeX, LuaTeX and LuaLaTeX!
@ -28,7 +28,7 @@ Treeworld, the World of worlds, is a world.
--- ---
// This is a fun one. // This is a fun one.
#set par(justify: true) #set par(justify: true)
#show regex("\S"): letter => rect(inset: 2pt)[#upper(letter)] #show regex("\S"): letter => box(rect(inset: 2pt, upper(letter)))
#lorem(5) #lorem(5)
--- ---

View File

@ -17,8 +17,7 @@
--- ---
// Test gray color conversion. // Test gray color conversion.
// Ref: true // Ref: true
#rect(fill: luma(0)) #stack(dir: ltr, rect(fill: luma(0)), rect(fill: luma(80%)))
#rect(fill: luma(80%))
--- ---
// Error for values that are out of range. // Error for values that are out of range.

View File

@ -6,10 +6,10 @@
#set text(lang: "ar", "Noto Sans Arabic", "IBM Plex Serif") #set text(lang: "ar", "Noto Sans Arabic", "IBM Plex Serif")
#set columns(gutter: 30pt) #set columns(gutter: 30pt)
#rect(fill: conifer, height: 8pt, width: 6pt) وتحفيز #box(rect(fill: conifer, height: 8pt, width: 6pt)) وتحفيز
العديد من التفاعلات الكيميائية. (DNA) من أهم الأحماض النووية التي تُشكِّل العديد من التفاعلات الكيميائية. (DNA) من أهم الأحماض النووية التي تُشكِّل
إلى جانب كل من البروتينات والليبيدات والسكريات المتعددة إلى جانب كل من البروتينات والليبيدات والسكريات المتعددة
#rect(fill: eastern, height: 8pt, width: 6pt) #box(rect(fill: eastern, height: 8pt, width: 6pt))
الجزيئات الضخمة الأربعة الضرورية للحياة. الجزيئات الضخمة الأربعة الضرورية للحياة.
--- ---

View File

@ -43,7 +43,7 @@ Lריווח #h(1cm) R
--- ---
// Test inline object. // Test inline object.
#set text(lang: "he", "IBM Plex Serif") #set text(lang: "he", "IBM Plex Serif")
קרנפיםRh#image("/rhino.png", height: 11pt)inoחיים קרנפיםRh#box(image("/rhino.png", height: 11pt))inoחיים
--- ---
// Test whether L1 whitespace resetting destroys stuff. // Test whether L1 whitespace resetting destroys stuff.

View File

@ -9,7 +9,7 @@ The first paragraph has no indent.
But the second one does. But the second one does.
#image("/tiger.jpg", height: 6pt) #box(image("/tiger.jpg", height: 6pt))
starts a paragraph without indent. starts a paragraph without indent.
#align(center, image("/rhino.png", width: 1cm)) #align(center, image("/rhino.png", width: 1cm))

View File

@ -6,7 +6,7 @@
#let tex = { #let tex = {
[T] [T]
h(-0.14 * size) h(-0.14 * size)
move(dy: 0.22 * size)[E] box(move(dy: 0.22 * size)[E])
h(-0.12 * size) h(-0.12 * size)
[X] [X]
} }
@ -14,11 +14,11 @@
#let xetex = { #let xetex = {
[X] [X]
h(-0.14 * size) h(-0.14 * size)
scale(x: -100%, move(dy: 0.26 * size)[E]) box(scale(x: -100%, move(dy: 0.26 * size)[E]))
h(-0.14 * size) h(-0.14 * size)
[T] [T]
h(-0.14 * size) h(-0.14 * size)
move(dy: 0.26 * size)[E] box(move(dy: 0.26 * size)[E])
h(-0.12 * size) h(-0.12 * size)
[X] [X]
} }
@ -44,6 +44,6 @@ nor #xetex!
// Test setting scaling origin. // Test setting scaling origin.
#let r = rect(width: 100pt, height: 10pt, fill: forest) #let r = rect(width: 100pt, height: 10pt, fill: forest)
#set page(height: 65pt) #set page(height: 65pt)
#scale(r, x: 50%, y: 200%, origin: left + top) #box(scale(r, x: 50%, y: 200%, origin: left + top))
#scale(r, x: 50%, origin: center) #box(scale(r, x: 50%, origin: center))
#scale(r, x: 50%, y: 200%, origin: right + bottom) #box(scale(r, x: 50%, y: 200%, origin: right + bottom))

View File

@ -32,13 +32,13 @@ You could also make the
// Transformed link. // Transformed link.
#set page(height: 60pt) #set page(height: 60pt)
#let mylink = link("https://typst.org/")[LINK] #let mylink = link("https://typst.org/")[LINK]
My cool #move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink))) My cool #box(move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink))))
--- ---
// Link containing a block. // Link containing a block.
#link("https://example.com/", block[ #link("https://example.com/", block[
My cool rhino My cool rhino
#move(dx: 10pt, image("/rhino.png", width: 1cm)) #box(move(dx: 10pt, image("/rhino.png", width: 1cm)))
]) ])
--- ---

View File

@ -4,6 +4,6 @@
Hi #text(1.5em)[You], #text(0.75em)[how are you?] Hi #text(1.5em)[You], #text(0.75em)[how are you?]
Our cockatoo was one of the Our cockatoo was one of the
#text(baseline: -0.2em)[#circle(radius: 2pt) first] #text(baseline: -0.2em)[#box(circle(radius: 2pt)) first]
#text(baseline: 0.2em)[birds #circle(radius: 2pt)] #text(baseline: 0.2em)[birds #box(circle(radius: 2pt))]
that ever learned to mimic a human voice. that ever learned to mimic a human voice.

View File

@ -30,5 +30,4 @@ G // 5pt
size - 3pt size - 3pt
} }
#square(size: size) #stack(dir: ltr, spacing: 1fr, square(size: size), square(size: 25pt))
#square(size: 25pt)

View File

@ -4,8 +4,8 @@
#table( #table(
columns: 3, columns: 3,
[Typo.], [Fallb.], [Synth], [Typo.], [Fallb.], [Synth],
[x#super[1]], [x#super[5n]], [x#super[2 #square(size: 6pt)]], [x#super[1]], [x#super[5n]], [x#super[2 #box(square(size: 6pt))]],
[x#sub[1]], [x#sub[5n]], [x#sub[2 #square(size: 6pt)]], [x#sub[1]], [x#sub[5n]], [x#sub[2 #box(square(size: 6pt))]],
) )
--- ---

View File

@ -14,8 +14,8 @@
// Test configuring the size and fitting behaviour of images. // Test configuring the size and fitting behaviour of images.
// Set width and height explicitly. // Set width and height explicitly.
#image("/rhino.png", width: 30pt) #box(image("/rhino.png", width: 30pt))
#image("/rhino.png", height: 30pt) #box(image("/rhino.png", height: 30pt))
// Set width and height explicitly and force stretching. // Set width and height explicitly and force stretching.
#image("/monkey.svg", width: 100%, height: 20pt, fit: "stretch") #image("/monkey.svg", width: 100%, height: 20pt, fit: "stretch")
@ -38,13 +38,12 @@
--- ---
// Does not fit to remaining height of page. // Does not fit to remaining height of page.
#set page(height: 60pt) #set page(height: 60pt)
Stuff #parbreak()
Stuff Stuff
#image("/rhino.png") #image("/rhino.png")
--- ---
// Test baseline. // Test baseline.
A #image("/tiger.jpg", height: 1cm, width: 80%) B A #box(image("/tiger.jpg", height: 1cm, width: 80%)) B
--- ---
// Test advanced SVG features. // Test advanced SVG features.

View File

@ -7,8 +7,7 @@
place(line(end: (0.4em, 0pt))) place(line(end: (0.4em, 0pt)))
place(line(start: (0pt, 0.4em), end: (0pt, 0pt))) place(line(start: (0pt, 0.4em), end: (0pt, 0pt)))
line(end: (0.6em, 0.6em)) line(end: (0.6em, 0.6em))
}) }) Hello #box(line(length: 1cm))!
Hello #line()!
#line(end: (70%, 50%)) #line(end: (70%, 50%))

View File

@ -4,26 +4,35 @@
// Test relative width and height and size that is smaller // Test relative width and height and size that is smaller
// than default size. // than default size.
#set page(width: 120pt, height: 70pt) #set page(width: 120pt, height: 70pt)
#set align(center + horizon) #set align(bottom)
#square(width: 50%, [A]) #let centered = align.with(center + horizon)
#square(height: 50%) #stack(
#box(stack( dir: ltr,
square(size: 10pt), spacing: 1fr,
square(size: 20pt, [B]) square(width: 50%, centered[A]),
)) square(height: 50%),
stack(
square(size: 10pt),
square(size: 20pt, centered[B])
),
)
--- ---
// Test alignment in automatically sized square and circle. // Test alignment in automatically sized square and circle.
#set text(8pt) #set text(8pt)
#square(inset: 4pt)[ #box(square(inset: 4pt)[
Hey there, #align(center + bottom, rotate(180deg, [you!])) Hey there, #align(center + bottom, rotate(180deg, [you!]))
] ])
#circle(align(center + horizon, [Hey.])) #box(circle(align(center + horizon, [Hey.])))
--- ---
// Test that maximum wins if both width and height are given. // Test that minimum wins if both width and height are given.
#square(width: 10pt, height: 20pt) #stack(
#circle(width: 20%, height: 10pt) dir: ltr,
spacing: 2pt,
square(width: 20pt, height: 40pt),
circle(width: 20%, height: 100pt),
)
--- ---
// Test square that is limited by region size. // Test square that is limited by region size.
@ -33,18 +42,22 @@
--- ---
// Test different ways of sizing. // Test different ways of sizing.
#set page(width: 120pt, height: 40pt) #set page(width: 120pt, height: 40pt)
#circle(radius: 5pt) #stack(
#circle(width: 10%) dir: ltr,
#circle(height: 50%) spacing: 2pt,
circle(radius: 5pt),
circle(width: 10%),
circle(height: 50%),
)
--- ---
// Test square that is overflowing due to its aspect ratio. // Test that square doesn't overflow due to its aspect ratio.
#set page(width: 40pt, height: 20pt, margin: 5pt) #set page(width: 40pt, height: 25pt, margin: 5pt)
#square(width: 100%) #parbreak() #square(width: 100%)
#square(width: 100%)[Hey] #square(width: 100%)[Hello there]
--- ---
// Size cannot be relative because we wouldn't know // Size cannot be relative because we wouldn't know
// relative to which axis. // relative to which axis.
// Error: 15-18 expected length, found ratio // Error: 15-18 expected length or auto, found ratio
#square(size: 50%) #square(size: 50%)

View File

@ -2,14 +2,14 @@
--- ---
// Default circle. // Default circle.
#circle() #box(circle())
#circle[Hey] #box(circle[Hey])
--- ---
// Test auto sizing. // Test auto sizing.
#set circle(inset: 0pt) #set circle(inset: 0pt)
Auto-sized circle. \ Auto-sized circle.
#circle(fill: rgb("eb5278"), stroke: 2pt + black, #circle(fill: rgb("eb5278"), stroke: 2pt + black,
align(center + horizon)[But, soft!] align(center + horizon)[But, soft!]
) )
@ -21,7 +21,7 @@ Center-aligned rect in auto-sized circle.
) )
) )
Rect in auto-sized circle. \ Rect in auto-sized circle.
#circle(fill: forest, #circle(fill: forest,
rect(fill: conifer, stroke: white, inset: 4pt)[ rect(fill: conifer, stroke: white, inset: 4pt)[
#set text(8pt) #set text(8pt)
@ -39,13 +39,18 @@ Expanded by height.
--- ---
// Test relative sizing. // Test relative sizing.
#let centered(body) = align(center + horizon, body)
#set text(fill: white) #set text(fill: white)
#rect(width: 100pt, height: 50pt, inset: 0pt, fill: rgb("aaa"), centered[ #show rect.with(width: 100pt, height: 50pt, inset: 0pt, fill: rgb("aaa"))
#circle(radius: 10pt, fill: eastern, centered[A]) // D=20pt #set align(center + horizon)
#circle(height: 60%, fill: eastern, centered[B]) // D=30pt #stack(
#circle(width: 20% + 20pt, fill: eastern, centered[C]) // D=40pt dir: ltr,
]) spacing: 1fr,
1fr,
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
1fr,
)
--- ---
// Radius wins over width and height. // Radius wins over width and height.

View File

@ -8,7 +8,7 @@
#set rect(inset: 0pt) #set rect(inset: 0pt)
#set ellipse(inset: 0pt) #set ellipse(inset: 0pt)
Rect in ellipse in fixed rect. \ Rect in ellipse in fixed rect.
#rect(width: 3cm, height: 2cm, fill: rgb("2a631a"), #rect(width: 3cm, height: 2cm, fill: rgb("2a631a"),
ellipse(fill: forest, width: 100%, height: 100%, ellipse(fill: forest, width: 100%, height: 100%,
rect(fill: conifer, width: 100%, height: 100%, rect(fill: conifer, width: 100%, height: 100%,
@ -19,11 +19,13 @@ Rect in ellipse in fixed rect. \
) )
) )
Auto-sized ellipse. \ Auto-sized ellipse.
#ellipse(fill: conifer, stroke: 3pt + forest, inset: 3pt)[ #ellipse(fill: conifer, stroke: 3pt + forest, inset: 3pt)[
#set text(8pt) #set text(8pt)
But, soft! what light through yonder window breaks? But, soft! what light through yonder window breaks?
] ]
An inline #ellipse(width: 8pt, height: 6pt, outset: (top: 3pt, rest: 5.5pt)) ellipse. An inline
#box(ellipse(width: 8pt, height: 6pt, outset: (top: 3pt, rest: 5.5pt)))
ellipse.

View File

@ -27,7 +27,7 @@
--- ---
// Test stroke folding. // Test stroke folding.
#let sq = square.with(size: 10pt) #let sq(..args) = box(square(size: 10pt, ..args))
#set square(stroke: none) #set square(stroke: none)
#sq() #sq()

View File

@ -24,19 +24,23 @@
#rect(height: 1cm, width: 100%, fill: rgb("734ced"))[Topleft] #rect(height: 1cm, width: 100%, fill: rgb("734ced"))[Topleft]
// These are inline with text. // These are inline with text.
{#rect(width: 0.5in, height: 7pt, fill: rgb("d6cd67")) {#box(rect(width: 0.5in, height: 7pt, fill: rgb("d6cd67")))
#rect(width: 0.5in, height: 7pt, fill: rgb("edd466")) #box(rect(width: 0.5in, height: 7pt, fill: rgb("edd466")))
#rect(width: 0.5in, height: 7pt, fill: rgb("e3be62"))} #box(rect(width: 0.5in, height: 7pt, fill: rgb("e3be62")))}
// Rounded corners. // Rounded corners.
#rect(width: 2cm, radius: 60%) #stack(
#rect(width: 1cm, radius: (left: 10pt, right: 5pt)) dir: ltr,
#rect(width: 1.25cm, radius: ( spacing: 1fr,
top-left: 2pt, rect(width: 2cm, radius: 60%),
top-right: 5pt, rect(width: 1cm, radius: (left: 10pt, right: 5pt)),
bottom-right: 8pt, rect(width: 1.25cm, radius: (
bottom-left: 11pt top-left: 2pt,
)) top-right: 5pt,
bottom-right: 8pt,
bottom-left: 11pt
)),
)
// Different strokes. // Different strokes.
#set rect(stroke: (right: red)) #set rect(stroke: (right: red))

View File

@ -2,8 +2,8 @@
--- ---
// Default square. // Default square.
#square() #box(square())
#square[hey!] #box(square[hey!])
--- ---
// Test auto-sized square. // Test auto-sized square.
@ -15,7 +15,7 @@
--- ---
// Test relative-sized child. // Test relative-sized child.
#square(fill: eastern)[ #square(fill: eastern)[
#rect(width: 10pt, height: 5pt, fill: conifer) \ #rect(width: 10pt, height: 5pt, fill: conifer)
#rect(width: 40%, height: 5pt, stroke: conifer) #rect(width: 40%, height: 5pt, stroke: conifer)
] ]