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 000000000..9b50396d6 Binary files /dev/null and b/tests/ref/transform-scale-abs-and-auto.png differ 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)