diff --git a/crates/typst-layout/src/flow/distribute.rs b/crates/typst-layout/src/flow/distribute.rs
index 7a1cf4264..f504d22e7 100644
--- a/crates/typst-layout/src/flow/distribute.rs
+++ b/crates/typst-layout/src/flow/distribute.rs
@@ -17,7 +17,7 @@ pub fn distribute(composer: &mut Composer, regions: Regions) -> FlowResult {
/// A snapshot which can be restored to migrate a suffix of sticky blocks to
/// the next region.
sticky: Option>,
- /// Whether there was at least one proper block. Otherwise, sticky blocks
- /// are disabled (or else they'd keep being migrated).
- stickable: bool,
+ /// Whether the current group of consecutive sticky blocks are still sticky
+ /// and may migrate with the attached frame. This is `None` while we aren't
+ /// processing sticky blocks. On the first sticky block, this will become
+ /// `Some(true)` if migrating sticky blocks as usual would make a
+ /// difference - this is given by `regions.may_progress()`. Otherwise, it
+ /// is set to `Some(false)`, which is usually the case when the first
+ /// sticky block in the group is at the very top of the page (then,
+ /// migrating it would just lead us back to the top of the page, leading
+ /// to an infinite loop). In that case, all sticky blocks of the group are
+ /// also disabled, until this is reset to `None` on the first non-sticky
+ /// frame we find.
+ ///
+ /// While this behavior of disabling stickiness of sticky blocks at the
+ /// very top of the page may seem non-ideal, it is only problematic (that
+ /// is, may lead to orphaned sticky blocks / headings) if the combination
+ /// of 'sticky blocks + attached frame' doesn't fit in one page, in which
+ /// case there is nothing Typst can do to improve the situation, as sticky
+ /// blocks are supposed to always be in the same page as the subsequent
+ /// frame, but that is impossible in that case, which is thus pathological.
+ stickable: Option,
}
/// A snapshot of the distribution state.
@@ -314,13 +331,31 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
// If the frame is sticky and we haven't remembered a preceding
// sticky element, make a checkpoint which we can restore should we
// end on this sticky element.
- if self.stickable && self.sticky.is_none() {
+ //
+ // The first sticky block within consecutive sticky blocks
+ // determines whether this group of sticky blocks has stickiness
+ // disabled or not.
+ //
+ // The criteria used here is: if migrating this group of sticky
+ // blocks together with the "attached" block can't improve the lack
+ // of space, since we're at the start of the region, then we don't
+ // do so, and stickiness is disabled (at least, for this region).
+ // Otherwise, migration is allowed.
+ //
+ // Note that, since the whole region is checked, this ensures sticky
+ // blocks at the top of a block - but not necessarily of the page -
+ // can still be migrated.
+ if self.sticky.is_none()
+ && *self.stickable.get_or_insert_with(|| self.regions.may_progress())
+ {
self.sticky = Some(self.snapshot());
}
} else if !frame.is_empty() {
- // If the frame isn't sticky, we can forget a previous snapshot.
- self.stickable = true;
+ // If the frame isn't sticky, we can forget a previous snapshot. We
+ // interrupt a group of sticky blocks, if there was one, so we reset
+ // the saved stickable check for the next group of sticky blocks.
self.sticky = None;
+ self.stickable = None;
}
// Handle footnotes.
diff --git a/tests/ref/issue-5296-block-sticky-in-block-at-top.png b/tests/ref/issue-5296-block-sticky-in-block-at-top.png
new file mode 100644
index 000000000..ad0ace76f
Binary files /dev/null and b/tests/ref/issue-5296-block-sticky-in-block-at-top.png differ
diff --git a/tests/ref/issue-5296-block-sticky-spaced-from-top-of-page.png b/tests/ref/issue-5296-block-sticky-spaced-from-top-of-page.png
new file mode 100644
index 000000000..ad0ace76f
Binary files /dev/null and b/tests/ref/issue-5296-block-sticky-spaced-from-top-of-page.png differ
diff --git a/tests/ref/issue-5296-block-sticky-weakly-spaced-from-top-of-page.png b/tests/ref/issue-5296-block-sticky-weakly-spaced-from-top-of-page.png
new file mode 100644
index 000000000..9bcdbe569
Binary files /dev/null and b/tests/ref/issue-5296-block-sticky-weakly-spaced-from-top-of-page.png differ
diff --git a/tests/suite/layout/container.typ b/tests/suite/layout/container.typ
index 799300f0d..9948a00e3 100644
--- a/tests/suite/layout/container.typ
+++ b/tests/suite/layout/container.typ
@@ -279,3 +279,28 @@ First!
// Test box in 100% width block.
#block(width: 100%, fill: red, box("a box"))
#block(width: 100%, fill: red, [#box("a box") #box()])
+
+--- issue-5296-block-sticky-in-block-at-top ---
+#set page(height: 3cm)
+#v(1.6cm)
+#block(height: 2cm, breakable: true)[
+ #block(sticky: true)[*A*]
+
+ b
+]
+
+--- issue-5296-block-sticky-spaced-from-top-of-page ---
+#set page(height: 3cm)
+#v(2cm)
+
+#block(sticky: true)[*A*]
+
+b
+
+--- issue-5296-block-sticky-weakly-spaced-from-top-of-page ---
+#set page(height: 3cm)
+#v(2cm, weak: true)
+
+#block(sticky: true)[*A*]
+
+b