mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Make layout account for transforms (#2555)
This commit is contained in:
parent
f17208a5a2
commit
231b96e5cf
@ -50,7 +50,7 @@ impl Frame {
|
|||||||
|
|
||||||
/// Create a new, empty soft frame.
|
/// Create a new, empty soft frame.
|
||||||
///
|
///
|
||||||
/// Panics the size is not finite.
|
/// Panics if the size is not finite.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn soft(size: Size) -> Self {
|
pub fn soft(size: Size) -> Self {
|
||||||
Self::new(size, FrameKind::Soft)
|
Self::new(size, FrameKind::Soft)
|
||||||
|
@ -49,12 +49,20 @@ impl Point {
|
|||||||
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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
/// The distance between this point and the origin.
|
||||||
pub fn hypot(self) -> Abs {
|
pub fn hypot(self) -> Abs {
|
||||||
Abs::raw(self.x.to_raw().hypot(self.y.to_raw()))
|
Abs::raw(self.x.to_raw().hypot(self.y.to_raw()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transform the point with the given transformation.
|
/// 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 {
|
pub fn transform(self, ts: Transform) -> Self {
|
||||||
Self::new(
|
Self::new(
|
||||||
ts.sx.of(self.x) + ts.kx.of(self.y) + ts.tx,
|
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.
|
/// Convert to a size.
|
||||||
pub fn to_size(self) -> Size {
|
pub fn to_size(self) -> Size {
|
||||||
Size::new(self.x, self.y)
|
Size::new(self.x, self.y)
|
||||||
|
@ -2,13 +2,13 @@ use crate::diag::SourceResult;
|
|||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{elem, Content, Resolve, StyleChain};
|
use crate::foundations::{elem, Content, Resolve, StyleChain};
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
Abs, Align, Angle, Axes, FixedAlign, Fragment, HAlign, Layout, Length, Ratio,
|
Abs, Align, Angle, Axes, FixedAlign, Fragment, Frame, HAlign, Layout, Length, Point,
|
||||||
Regions, Rel, VAlign,
|
Ratio, Regions, Rel, Size, VAlign,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Moves content without affecting layout.
|
/// 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
|
/// it at the original positions. Containers will still be sized as if the
|
||||||
/// content was not moved.
|
/// content was not moved.
|
||||||
///
|
///
|
||||||
@ -57,7 +57,7 @@ impl Layout for MoveElem {
|
|||||||
/// Rotates content without affecting layout.
|
/// Rotates content without affecting layout.
|
||||||
///
|
///
|
||||||
/// Rotates an element by a given angle. The layout will act as if the element
|
/// 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
|
||||||
/// ```example
|
/// ```example
|
||||||
@ -98,6 +98,18 @@ pub struct RotateElem {
|
|||||||
#[default(HAlign::Center + VAlign::Horizon)]
|
#[default(HAlign::Center + VAlign::Horizon)]
|
||||||
pub origin: Align,
|
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.
|
/// The content to rotate.
|
||||||
#[required]
|
#[required]
|
||||||
pub body: Content,
|
pub body: Content,
|
||||||
@ -111,17 +123,27 @@ impl Layout for RotateElem {
|
|||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
regions: Regions,
|
regions: Regions,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
let angle = self.angle(styles);
|
||||||
let mut frame = self.body().layout(engine, styles, pod)?.into_frame();
|
let align = self.origin(styles).resolve(styles);
|
||||||
let Axes { x, y } = self
|
|
||||||
.origin(styles)
|
// Compute the new region's approximate size.
|
||||||
.resolve(styles)
|
let size = regions
|
||||||
.zip_map(frame.size(), FixedAlign::position);
|
.base()
|
||||||
let ts = Transform::translate(x, y)
|
.to_point()
|
||||||
.pre_concat(Transform::rotate(self.angle(styles)))
|
.transform_inf(Transform::rotate(angle))
|
||||||
.pre_concat(Transform::translate(-x, -y));
|
.map(Abs::abs)
|
||||||
frame.transform(ts);
|
.to_size();
|
||||||
Ok(Fragment::frame(frame))
|
|
||||||
|
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
|
/// ```example
|
||||||
/// #set align(center)
|
/// #set align(center)
|
||||||
/// #scale(x: -100%)[This is mirrored.]
|
/// #scale(x: -100%)[This is mirrored.]
|
||||||
|
/// #scale(x: -100%, reflow: true)[This is mirrored.]
|
||||||
/// ```
|
/// ```
|
||||||
#[elem(Layout)]
|
#[elem(Layout)]
|
||||||
pub struct ScaleElem {
|
pub struct ScaleElem {
|
||||||
@ -163,6 +186,18 @@ pub struct ScaleElem {
|
|||||||
#[default(HAlign::Center + VAlign::Horizon)]
|
#[default(HAlign::Center + VAlign::Horizon)]
|
||||||
pub origin: Align,
|
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.
|
/// The content to scale.
|
||||||
#[required]
|
#[required]
|
||||||
pub body: Content,
|
pub body: Content,
|
||||||
@ -176,17 +211,26 @@ impl Layout for ScaleElem {
|
|||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
regions: Regions,
|
regions: Regions,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
let sx = self.x(styles);
|
||||||
let mut frame = self.body().layout(engine, styles, pod)?.into_frame();
|
let sy = self.y(styles);
|
||||||
let Axes { x, y } = self
|
let align = self.origin(styles).resolve(styles);
|
||||||
.origin(styles)
|
|
||||||
.resolve(styles)
|
// Compute the new region's approximate size.
|
||||||
.zip_map(frame.size(), FixedAlign::position);
|
let size = regions
|
||||||
let transform = Transform::translate(x, y)
|
.base()
|
||||||
.pre_concat(Transform::scale(self.x(styles), self.y(styles)))
|
.zip_map(Axes::new(sx, sy), |r, s| s.of(r))
|
||||||
.pre_concat(Transform::translate(-x, -y));
|
.map(Abs::abs);
|
||||||
frame.transform(transform);
|
|
||||||
Ok(Fragment::frame(frame))
|
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()
|
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()))
|
||||||
|
}
|
||||||
|
BIN
tests/ref/layout/transform-layout.png
Normal file
BIN
tests/ref/layout/transform-layout.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
58
tests/typ/layout/transform-layout.typ
Normal file
58
tests/typ/layout/transform-layout.typ
Normal file
@ -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]!
|
Loading…
x
Reference in New Issue
Block a user