From 23746ee18901e08852306f35639298ad234d3481 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Thu, 30 May 2024 11:40:01 -0400 Subject: [PATCH] Add flush element (#4141) --- crates/typst/src/layout/flow.rs | 20 ++++++++++-- crates/typst/src/layout/mod.rs | 1 + crates/typst/src/layout/place.rs | 51 +++++++++++++++++++++++++++++-- crates/typst/src/realize/mod.rs | 5 +-- tests/ref/place-figure-flush.png | Bin 0 -> 5197 bytes tests/ref/place-float-flush.png | Bin 0 -> 2644 bytes tests/suite/layout/place.typ | 27 ++++++++++++++++ 7 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 tests/ref/place-figure-flush.png create mode 100644 tests/ref/place-float-flush.png diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index 2f985f285..84a395cc3 100644 --- a/crates/typst/src/layout/flow.rs +++ b/crates/typst/src/layout/flow.rs @@ -13,9 +13,9 @@ use crate::foundations::{ }; use crate::introspection::TagElem; use crate::layout::{ - Abs, AlignElem, Axes, BlockElem, ColbreakElem, ColumnsElem, FixedAlignment, Fr, - Fragment, Frame, FrameItem, LayoutMultiple, LayoutSingle, PlaceElem, Point, Regions, - Rel, Size, Spacing, VElem, + Abs, AlignElem, Axes, BlockElem, ColbreakElem, ColumnsElem, FixedAlignment, + FlushElem, Fr, Fragment, Frame, FrameItem, LayoutMultiple, LayoutSingle, PlaceElem, + Point, Regions, Rel, Size, Spacing, VElem, }; use crate::model::{FootnoteElem, FootnoteEntry, ParElem}; use crate::utils::Numeric; @@ -73,6 +73,8 @@ impl LayoutMultiple for Packed { if let Some(elem) = child.to_packed::() { layouter.layout_tag(elem); + } else if child.is::() { + layouter.flush(engine)?; } else if let Some(elem) = child.to_packed::() { layouter.layout_spacing(engine, elem, styles)?; } else if let Some(placed) = child.to_packed::() { @@ -683,6 +685,18 @@ impl<'a> FlowLayouter<'a> { Ok(()) } + /// Lays out all floating elements before continuing with other content. + fn flush(&mut self, engine: &mut Engine) -> SourceResult<()> { + for item in std::mem::take(&mut self.pending_floats) { + self.layout_item(engine, item)?; + } + while !self.pending_floats.is_empty() { + self.finish_region(engine, false)?; + } + + Ok(()) + } + /// Finish layouting and return the resulting fragment. fn finish(mut self, engine: &mut Engine) -> SourceResult { if self.expand.y { diff --git a/crates/typst/src/layout/mod.rs b/crates/typst/src/layout/mod.rs index 0fd8e6b10..444d51621 100644 --- a/crates/typst/src/layout/mod.rs +++ b/crates/typst/src/layout/mod.rs @@ -107,6 +107,7 @@ pub fn define(global: &mut Scope) { global.define_elem::(); global.define_elem::(); global.define_elem::(); + global.define_elem::(); global.define_elem::(); global.define_elem::(); global.define_elem::(); diff --git a/crates/typst/src/layout/place.rs b/crates/typst/src/layout/place.rs index 7d41b0376..f81af2b80 100644 --- a/crates/typst/src/layout/place.rs +++ b/crates/typst/src/layout/place.rs @@ -1,6 +1,6 @@ use crate::diag::{bail, At, Hint, SourceResult}; use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Smart, StyleChain}; +use crate::foundations::{elem, scope, Content, Packed, Smart, StyleChain, Unlabellable}; use crate::layout::{ Alignment, Axes, Em, Fragment, LayoutMultiple, Length, Regions, Rel, Size, VAlignment, }; @@ -26,7 +26,7 @@ use crate::realize::{Behave, Behaviour}; /// ), /// ) /// ``` -#[elem(Behave)] +#[elem(scope, Behave)] pub struct PlaceElem { /// Relative to which position in the parent container to place the content. /// @@ -43,7 +43,9 @@ pub struct PlaceElem { /// Whether the placed element has floating layout. /// /// Floating elements are positioned at the top or bottom of the page, - /// displacing in-flow content. + /// displacing in-flow content. They are always placed in the in-flow + /// order relative to each other, as well as before any content following + /// a later [`flush`] element. /// /// ```example /// #set page(height: 150pt) @@ -95,6 +97,12 @@ pub struct PlaceElem { pub body: Content, } +#[scope] +impl PlaceElem { + #[elem] + type FlushElem; +} + impl Packed { #[typst_macros::time(name = "place", span = self.span())] pub fn layout( @@ -136,3 +144,40 @@ impl Behave for Packed { Behaviour::Ignorant } } + +/// Asks the layout algorithm to place pending floating elements before +/// continuing with the content. +/// +/// This is useful for preventing floating figures from spilling +/// into the next section. +/// +/// ```example +/// #set page(height: 165pt, width: 150pt) +/// +/// Some introductory text: #lorem(15) +/// +/// #figure( +/// rect( +/// width: 100%, +/// height: 64pt, +/// [I float with a caption!], +/// ), +/// placement: auto, +/// caption: [A self-describing figure], +/// ) +/// +/// #place.flush() +/// +/// Some conclusive text that must occur +/// after the figure. +/// ``` +#[elem(Behave, Unlabellable)] +pub struct FlushElem {} + +impl Behave for Packed { + fn behaviour(&self) -> Behaviour { + Behaviour::Invisible + } +} + +impl Unlabellable for Packed {} diff --git a/crates/typst/src/realize/mod.rs b/crates/typst/src/realize/mod.rs index 4679a61cb..600df1bb7 100644 --- a/crates/typst/src/realize/mod.rs +++ b/crates/typst/src/realize/mod.rs @@ -23,8 +23,8 @@ use crate::foundations::{ }; use crate::introspection::TagElem; use crate::layout::{ - AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, HElem, LayoutMultiple, - LayoutSingle, PageElem, PagebreakElem, Parity, PlaceElem, VElem, + AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, FlushElem, HElem, + LayoutMultiple, LayoutSingle, PageElem, PagebreakElem, Parity, PlaceElem, VElem, }; use crate::math::{EquationElem, LayoutMath}; use crate::model::{ @@ -381,6 +381,7 @@ impl<'a> FlowBuilder<'a> { || content.is::() || content.is::() || content.is::() + || content.is::() { self.0.push(content, styles); return true; diff --git a/tests/ref/place-figure-flush.png b/tests/ref/place-figure-flush.png new file mode 100644 index 0000000000000000000000000000000000000000..2e6e03105438a5bd7f4afc840a6fe1b96174778b GIT binary patch literal 5197 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