From df56a2d20ddae5f4ea9aa39a4a1a8b7b11cf7c85 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Wed, 17 Jul 2024 04:27:46 -0400 Subject: [PATCH] Allow absolute lengths in `scale` (#4271) Co-authored-by: Laurenz --- crates/typst/src/layout/ratio.rs | 2 +- crates/typst/src/layout/transform.rs | 99 ++++++++++++++++++--- tests/ref/transform-scale-abs-and-auto.png | Bin 0 -> 3719 bytes tests/suite/layout/transform.typ | 13 ++- 4 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 tests/ref/transform-scale-abs-and-auto.png diff --git a/crates/typst/src/layout/ratio.rs b/crates/typst/src/layout/ratio.rs index 2d791f2d4..020d689aa 100644 --- a/crates/typst/src/layout/ratio.rs +++ b/crates/typst/src/layout/ratio.rs @@ -70,7 +70,7 @@ impl Ratio { impl Debug for Ratio { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{:?}%", self.get()) + write!(f, "{:?}%", self.get() * 100.0) } } diff --git a/crates/typst/src/layout/transform.rs b/crates/typst/src/layout/transform.rs index 7172466f3..ad3668d10 100644 --- a/crates/typst/src/layout/transform.rs +++ b/crates/typst/src/layout/transform.rs @@ -1,7 +1,11 @@ -use crate::diag::SourceResult; +use std::ops::Div; + +use once_cell::unsync::Lazy; + +use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, + cast, elem, Content, NativeElement, Packed, Resolve, Show, Smart, StyleChain, }; use crate::introspection::Locator; use crate::layout::{ @@ -188,15 +192,15 @@ pub struct ScaleElem { let all = args.find()?; args.named("x")?.or(all) )] - #[default(Ratio::one())] - pub x: Ratio, + #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))] + pub x: Smart, /// The vertical scaling factor. /// /// The body will be mirrored vertically if the parameter is negative. #[parse(args.named("y")?.or(all))] - #[default(Ratio::one())] - pub y: Ratio, + #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))] + pub y: Smart, /// The origin of the transformation. /// @@ -242,12 +246,9 @@ fn layout_scale( styles: StyleChain, region: Region, ) -> SourceResult { - let sx = elem.x(styles); - let sy = elem.y(styles); - let align = elem.origin(styles).resolve(styles); - // Compute the new region's approximate size. - let size = region.size.zip_map(Axes::new(sx, sy), |r, s| s.of(r)).map(Abs::abs); + let scale = elem.resolve_scale(engine, locator.relayout(), region.size, styles)?; + let size = region.size.zip_map(scale, |r, s| s.of(r)).map(Abs::abs); measure_and_layout( engine, @@ -256,12 +257,84 @@ fn layout_scale( size, styles, elem.body(), - Transform::scale(sx, sy), - align, + Transform::scale(scale.x, scale.y), + elem.origin(styles).resolve(styles), elem.reflow(styles), ) } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum ScaleAmount { + Ratio(Ratio), + Length(Length), +} + +impl Packed { + /// Resolves scale parameters, preserving aspect ratio if one of the scales is set to `auto`. + fn resolve_scale( + &self, + engine: &mut Engine, + locator: Locator, + container: Size, + styles: StyleChain, + ) -> SourceResult> { + fn resolve_axis( + axis: Smart, + body: impl Fn() -> SourceResult, + styles: StyleChain, + ) -> SourceResult> { + Ok(match axis { + Smart::Auto => Smart::Auto, + Smart::Custom(amt) => Smart::Custom(match amt { + ScaleAmount::Ratio(ratio) => ratio, + ScaleAmount::Length(length) => { + let length = length.resolve(styles); + Ratio::new(length.div(body()?)) + } + }), + }) + } + + let size = Lazy::new(|| { + let pod = Regions::one(container, Axes::splat(false)); + let frame = self.body().layout(engine, locator, styles, pod)?.into_frame(); + SourceResult::Ok(frame.size()) + }); + + let x = resolve_axis( + self.x(styles), + || size.as_ref().map(|size| size.x).map_err(Clone::clone), + styles, + )?; + + let y = resolve_axis( + self.y(styles), + || size.as_ref().map(|size| size.y).map_err(Clone::clone), + styles, + )?; + + match (x, y) { + (Smart::Auto, Smart::Auto) => { + bail!(self.span(), "x and y cannot both be auto") + } + (Smart::Custom(x), Smart::Custom(y)) => Ok(Axes::new(x, y)), + (Smart::Auto, Smart::Custom(v)) | (Smart::Custom(v), Smart::Auto) => { + Ok(Axes::splat(v)) + } + } + } +} + +cast! { + ScaleAmount, + self => match self { + ScaleAmount::Ratio(ratio) => ratio.into_value(), + ScaleAmount::Length(length) => length.into_value(), + }, + ratio: Ratio => ScaleAmount::Ratio(ratio), + length: Length => ScaleAmount::Length(length), +} + /// A scale-skew-translate transformation. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Transform { diff --git a/tests/ref/transform-scale-abs-and-auto.png b/tests/ref/transform-scale-abs-and-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..9b50396d62149a3f5d6472736b74f914dc302078 GIT binary patch literal 3719 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 literal 0 HcmV?d00001 diff --git a/tests/suite/layout/transform.typ b/tests/suite/layout/transform.typ index 50a6d417f..3604b72f7 100644 --- a/tests/suite/layout/transform.typ +++ b/tests/suite/layout/transform.typ @@ -74,7 +74,7 @@ Hello #rotated[World]!\ Hello #rotated[World]! --- transform-scale --- -// Test that scaling impact layout. +// Test that scaling impacts layout. #set page(width: 200pt) #set text(size: 32pt) #let scaled(body) = box(scale( @@ -104,3 +104,14 @@ Hello #scaled[World]!\ #set scale(reflow: true) Hello #scaled[World]! + +--- transform-scale-abs-and-auto --- +// Test scaling by absolute lengths and auto. +#set page(width: 200pt, height: 200pt) +#let cylinder = image("/assets/images/cylinder.svg") + +#cylinder +#scale(x: 100pt, y: 50pt, reflow: true, cylinder) +#scale(x: auto, y: 50pt, reflow: true, cylinder) +#scale(x: 100pt, y: auto, reflow: true, cylinder) +#scale(x: 150%, y: auto, reflow: true, cylinder)