summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst/src/layout/frame.rs2
-rw-r--r--crates/typst/src/layout/point.rs17
-rw-r--r--crates/typst/src/layout/transform.rs165
-rw-r--r--tests/ref/layout/transform-layout.pngbin0 -> 42107 bytes
-rw-r--r--tests/typ/layout/transform-layout.typ58
5 files changed, 215 insertions, 27 deletions
diff --git a/crates/typst/src/layout/frame.rs b/crates/typst/src/layout/frame.rs
index 3abc8623..d06cd23d 100644
--- a/crates/typst/src/layout/frame.rs
+++ b/crates/typst/src/layout/frame.rs
@@ -50,7 +50,7 @@ impl Frame {
/// Create a new, empty soft frame.
///
- /// Panics the size is not finite.
+ /// Panics if the size is not finite.
#[track_caller]
pub fn soft(size: Size) -> Self {
Self::new(size, FrameKind::Soft)
diff --git a/crates/typst/src/layout/point.rs b/crates/typst/src/layout/point.rs
index 239a6fe7..bcc05c2b 100644
--- a/crates/typst/src/layout/point.rs
+++ b/crates/typst/src/layout/point.rs
@@ -49,12 +49,20 @@ impl Point {
Self { x: self.x.max(other.x), y: self.y.max(other.y) }
}
+ /// Maps the point with the given function.
+ pub fn map(self, f: impl Fn(Abs) -> Abs) -> Self {
+ Self { x: f(self.x), y: f(self.y) }
+ }
+
/// The distance between this point and the origin.
pub fn hypot(self) -> Abs {
Abs::raw(self.x.to_raw().hypot(self.y.to_raw()))
}
/// Transform the point with the given transformation.
+ ///
+ /// In the event that one of the coordinates is infinite, the result will
+ /// be zero.
pub fn transform(self, ts: Transform) -> Self {
Self::new(
ts.sx.of(self.x) + ts.kx.of(self.y) + ts.tx,
@@ -62,6 +70,15 @@ impl Point {
)
}
+ /// Transforms the point with the given transformation, without accounting
+ /// for infinite values.
+ pub fn transform_inf(self, ts: Transform) -> Self {
+ Self::new(
+ ts.sx.get() * self.x + ts.kx.get() * self.y + ts.tx,
+ ts.ky.get() * self.x + ts.sy.get() * self.y + ts.ty,
+ )
+ }
+
/// Convert to a size.
pub fn to_size(self) -> Size {
Size::new(self.x, self.y)
diff --git a/crates/typst/src/layout/transform.rs b/crates/typst/src/layout/transform.rs
index bfc35825..92cba120 100644
--- a/crates/typst/src/layout/transform.rs
+++ b/crates/typst/src/layout/transform.rs
@@ -2,13 +2,13 @@ use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Resolve, StyleChain};
use crate::layout::{
- Abs, Align, Angle, Axes, FixedAlign, Fragment, HAlign, Layout, Length, Ratio,
- Regions, Rel, VAlign,
+ Abs, Align, Angle, Axes, FixedAlign, Fragment, Frame, HAlign, Layout, Length, Point,
+ Ratio, Regions, Rel, Size, VAlign,
};
/// Moves content without affecting layout.
///
-/// The `move` function allows you to move content while the layout still 'sees'
+/// The `move` function allows you to move content while th layout still 'sees'
/// it at the original positions. Containers will still be sized as if the
/// content was not moved.
///
@@ -57,7 +57,7 @@ impl Layout for MoveElem {
/// Rotates content without affecting layout.
///
/// Rotates an element by a given angle. The layout will act as if the element
-/// was not rotated.
+/// was not rotated unless you specify `{reflow: true}`.
///
/// # Example
/// ```example
@@ -98,6 +98,18 @@ pub struct RotateElem {
#[default(HAlign::Center + VAlign::Horizon)]
pub origin: Align,
+ /// Whether the rotation impacts the layout.
+ ///
+ /// If set to `{false}`, the rotated content will retain the bounding box of
+ /// the original content. If set to `{true}`, the bounding box will take the
+ /// rotation of the content into account and adjust the layout accordingly.
+ ///
+ /// ```example
+ /// Hello #rotate(90deg, reflow: true)[World]!
+ /// ```
+ #[default(false)]
+ pub reflow: bool,
+
/// The content to rotate.
#[required]
pub body: Content,
@@ -111,17 +123,27 @@ impl Layout for RotateElem {
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
- let pod = Regions::one(regions.base(), Axes::splat(false));
- let mut frame = self.body().layout(engine, styles, pod)?.into_frame();
- let Axes { x, y } = self
- .origin(styles)
- .resolve(styles)
- .zip_map(frame.size(), FixedAlign::position);
- let ts = Transform::translate(x, y)
- .pre_concat(Transform::rotate(self.angle(styles)))
- .pre_concat(Transform::translate(-x, -y));
- frame.transform(ts);
- Ok(Fragment::frame(frame))
+ let angle = self.angle(styles);
+ let align = self.origin(styles).resolve(styles);
+
+ // Compute the new region's approximate size.
+ let size = regions
+ .base()
+ .to_point()
+ .transform_inf(Transform::rotate(angle))
+ .map(Abs::abs)
+ .to_size();
+
+ measure_and_layout(
+ engine,
+ regions.base(),
+ size,
+ styles,
+ self.body(),
+ Transform::rotate(angle),
+ align,
+ self.reflow(styles),
+ )
}
}
@@ -133,6 +155,7 @@ impl Layout for RotateElem {
/// ```example
/// #set align(center)
/// #scale(x: -100%)[This is mirrored.]
+/// #scale(x: -100%, reflow: true)[This is mirrored.]
/// ```
#[elem(Layout)]
pub struct ScaleElem {
@@ -163,6 +186,18 @@ pub struct ScaleElem {
#[default(HAlign::Center + VAlign::Horizon)]
pub origin: Align,
+ /// Whether the scaling impacts the layout.
+ ///
+ /// If set to `{false}`, the scaled content will be allowed to overlap
+ /// other content. If set to `{true}`, it will compute the new size of
+ /// the scaled content and adjust the layout accordingly.
+ ///
+ /// ```example
+ /// Hello #scale(x: 20%, y: 40%, reflow: true)[World]!
+ /// ```
+ #[default(false)]
+ pub reflow: bool,
+
/// The content to scale.
#[required]
pub body: Content,
@@ -176,17 +211,26 @@ impl Layout for ScaleElem {
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
- let pod = Regions::one(regions.base(), Axes::splat(false));
- let mut frame = self.body().layout(engine, styles, pod)?.into_frame();
- let Axes { x, y } = self
- .origin(styles)
- .resolve(styles)
- .zip_map(frame.size(), FixedAlign::position);
- let transform = Transform::translate(x, y)
- .pre_concat(Transform::scale(self.x(styles), self.y(styles)))
- .pre_concat(Transform::translate(-x, -y));
- frame.transform(transform);
- Ok(Fragment::frame(frame))
+ let sx = self.x(styles);
+ let sy = self.y(styles);
+ let align = self.origin(styles).resolve(styles);
+
+ // Compute the new region's approximate size.
+ let size = regions
+ .base()
+ .zip_map(Axes::new(sx, sy), |r, s| s.of(r))
+ .map(Abs::abs);
+
+ measure_and_layout(
+ engine,
+ regions.base(),
+ size,
+ styles,
+ self.body(),
+ Transform::scale(sx, sy),
+ align,
+ self.reflow(styles),
+ )
}
}
@@ -314,3 +358,72 @@ impl Default for Transform {
Self::identity()
}
}
+
+/// Applies a transformation to a frame, reflowing the layout if necessary.
+#[allow(clippy::too_many_arguments)]
+fn measure_and_layout(
+ engine: &mut Engine,
+ base_size: Size,
+ size: Size,
+ styles: StyleChain,
+ body: &Content,
+ transform: Transform,
+ align: Axes<FixedAlign>,
+ reflow: bool,
+) -> SourceResult<Fragment> {
+ if !reflow {
+ // Layout the body.
+ let pod = Regions::one(base_size, Axes::splat(false));
+ let mut frame = body.layout(engine, styles, pod)?.into_frame();
+ let Axes { x, y } = align.zip_map(frame.size(), FixedAlign::position);
+
+ // Apply the transform.
+ let ts = Transform::translate(x, y)
+ .pre_concat(transform)
+ .pre_concat(Transform::translate(-x, -y));
+ frame.transform(ts);
+
+ return Ok(Fragment::frame(frame));
+ }
+
+ // Measure the size of the body.
+ let pod = Regions::one(size, Axes::splat(false));
+ let frame = body.measure(engine, styles, pod)?.into_frame();
+
+ // Actually perform the layout.
+ let pod = Regions::one(frame.size(), Axes::splat(true));
+ let mut frame = body.layout(engine, styles, pod)?.into_frame();
+ let Axes { x, y } = align.zip_map(frame.size(), FixedAlign::position);
+
+ // Apply the transform.
+ let ts = Transform::translate(x, y)
+ .pre_concat(transform)
+ .pre_concat(Transform::translate(-x, -y));
+
+ // Compute the bounding box and offset and wrap in a new frame.
+ let (offset, size) = compute_bounding_box(&frame, ts);
+ frame.transform(ts);
+ frame.translate(offset);
+ frame.set_size(size);
+ Ok(Fragment::frame(frame))
+}
+
+/// Computes the bounding box and offset of a transformed frame.
+fn compute_bounding_box(frame: &Frame, ts: Transform) -> (Point, Size) {
+ let top_left = Point::zero().transform_inf(ts);
+ let top_right = Point::new(frame.width(), Abs::zero()).transform_inf(ts);
+ let bottom_left = Point::new(Abs::zero(), frame.height()).transform_inf(ts);
+ let bottom_right = Point::new(frame.width(), frame.height()).transform_inf(ts);
+
+ // We first compute the new bounding box of the rotated frame.
+ let min_x = top_left.x.min(top_right.x).min(bottom_left.x).min(bottom_right.x);
+ let min_y = top_left.y.min(top_right.y).min(bottom_left.y).min(bottom_right.y);
+ let max_x = top_left.x.max(top_right.x).max(bottom_left.x).max(bottom_right.x);
+ let max_y = top_left.y.max(top_right.y).max(bottom_left.y).max(bottom_right.y);
+
+ // Then we compute the new size of the frame.
+ let width = max_x - min_x;
+ let height = max_y - min_y;
+
+ (Point::new(-min_x, -min_y), Size::new(width.abs(), height.abs()))
+}
diff --git a/tests/ref/layout/transform-layout.png b/tests/ref/layout/transform-layout.png
new file mode 100644
index 00000000..576824f0
--- /dev/null
+++ b/tests/ref/layout/transform-layout.png
Binary files differ
diff --git a/tests/typ/layout/transform-layout.typ b/tests/typ/layout/transform-layout.typ
new file mode 100644
index 00000000..ce6dc930
--- /dev/null
+++ b/tests/typ/layout/transform-layout.typ
@@ -0,0 +1,58 @@
+// Test layout transformations
+
+---
+// Test that rotation impact layout.
+#set page(width: 200pt)
+#set rotate(reflow: true)
+
+#let one(angle) = box(fill: aqua, rotate(angle)[Test Text])
+#for angle in range(0, 360, step: 15) {
+ one(angle * 1deg)
+}
+
+---
+// Test relative sizing in rotated boxes.
+#set page(width: 200pt, height: 200pt)
+#set text(size: 32pt)
+#let rotated(body) = box(rotate(
+ 90deg,
+ box(stroke: 0.5pt, height: 20%, clip: true, body)
+))
+
+#set rotate(reflow: false)
+Hello #rotated[World]!\
+
+#set rotate(reflow: true)
+Hello #rotated[World]!
+
+---
+// Test that scaling impact layout.
+#set page(width: 200pt)
+#set text(size: 32pt)
+#let scaled(body) = box(scale(
+ x: 20%,
+ y: 40%,
+ body
+))
+
+#set scale(reflow: false)
+Hello #scaled[World]!
+
+#set scale(reflow: true)
+Hello #scaled[World]!
+
+---
+// Test relative sizing in scaled boxes.
+#set page(width: 200pt, height: 200pt)
+#set text(size: 32pt)
+#let scaled(body) = box(scale(
+ x: 60%,
+ y: 40%,
+ box(stroke: 0.5pt, width: 30%, clip: true, body)
+))
+
+#set scale(reflow: false)
+Hello #scaled[World]!\
+
+#set scale(reflow: true)
+Hello #scaled[World]!