Skip to content

Don't let a deferred float inflate its block formatting context#2828

Merged
liZe merged 2 commits into
Kozea:mainfrom
mvanherwijnen:float-defer-excluded-shapes
Jul 20, 2026
Merged

Don't let a deferred float inflate its block formatting context#2828
liZe merged 2 commits into
Kozea:mainfrom
mvanherwijnen:float-defer-excluded-shapes

Conversation

@mvanherwijnen

@mvanherwijnen mvanherwijnen commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What's wrong

When a block that establishes a block formatting context (display: flow-root, overflow other than visible, etc.) contains floats, a float that doesn't fit on the current page can push the entire block to the next page

This is very visible in print documents full of figures that use float + wrapping text: blocks jump to a fresh page instead of partially filling the current one.

Minimal example

<style>
  @page { size: 100px; margin: 0 }
  body { margin: 0 }
  div.bfc { display: flow-root }
  p { height: 20px; margin: 0 }
  div.float { float: right; width: 30px; height: 40px }
</style>
<div style="height: 70px"></div>
<div class="bfc">
  <p>a</p>
  <div class="float"></div>
  <p>b</p>
</div>
  • Expected: page 1 = spacer + a; page 2 = float + b.
  • Actual: page 1 = spacer only; the whole .bfc (both paragraphs and the float) is pushed to page 2, wasting ~30px of page 1.

Cause

float_layout appends every laid-out float to context.excluded_shapes. When the float overflows and _out_of_flow_layout breaks before it (deferring it to the next page via resume_at), the float is left in excluded_shapes.

finish_block_formatting_context then grows the BFC box's height to max(shape bottoms) including the deferred float, which floated up beside the content that stays on the page. The fragment's height therefore exceeds the page, the caller rejects it, and the whole block is moved to the next page instead of keeping the part that fits.

Fix

When breaking before an overflowing float, drop it from the current formatting context's excluded shapes so it no longer inflates the fragment that stays on the page:

context.excluded_shapes = [
    shape for shape in context.excluded_shapes
    if shape is not new_child]

Test

Adds test_float_overflow_splits_formatting_context in tests/layout/test_float.py, reproducing the example above and asserting the block is split (paragraph a stays on page 1). It fails before the change and passes after. The full tests/layout/ suite passes with the change.

When a float overflows the current page it is deferred to the next page,
but it was left in the block formatting context's excluded shapes. As a
result finish_block_formatting_context grew the fragment's height to
contain that deferred float -- which has floated up beside the content
that stays on the page -- so the fragment overflowed and the whole block
was pushed to the next page instead of being split across the page break.

Drop the deferred float from the excluded shapes when breaking before it,
so the fragment keeps the height of the content that actually stays.
@mvanherwijnen
mvanherwijnen force-pushed the float-defer-excluded-shapes branch from 41c8a7b to 64a34ed Compare July 7, 2026 13:55
@liZe

liZe commented Jul 20, 2026

Copy link
Copy Markdown
Member

Hi!

Thanks for the pull request.

The problem is actually larger than what your patch fix. I’ll add a commit on top of yours to fix that!

@liZe
liZe merged commit 8975612 into Kozea:main Jul 20, 2026
8 checks passed
@liZe liZe added the bug Existing features not working as expected label Jul 20, 2026
@liZe liZe added this to the 70.0 milestone Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Existing features not working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants