In quirks mode, the body element expands to fill its parent (the html
element) when height is auto, per the quirks spec section 3.7.
This quirk applies when:
- The document is in quirks mode
- The body element has height: auto
- The body is not absolutely/fixed positioned
- The body is not floated
- The body is not inline-level
The quirks mode percentage height calculation quirk was incorrectly
applied to anonymous boxes (like the internal flex wrapper inside
buttons), causing buttons to collapse to zero height.
Per the quirks spec, the percentage height quirk:
- Only applies to DOM elements, not anonymous boxes
- Does not apply to flex/grid items (they resolve against their
container)
- Does not apply to table-related display types
This patch:
1. Excludes anonymous boxes and flex/grid items from the quirk in
should_treat_height_as_auto()
2. Adds quirks mode percentage height walk-up in
calculate_inner_height() for inline-level boxes
3. Removes the incorrect flex/grid container exclusion from
BlockFormattingContext (the quirk applies to containers, not items)
For `float: left` boxes, we would take the available space into account
and push boxes down as required. This applies that same logic to `float:
right` boxes, where we would previously only compare their offset from
the edge using `>= 0`, which was almost always true.
Fixes#4750.
Previously, buttons with `display: block` and `width: auto` would take
an early return path in compute_width() that set the content width to
fit-content but skipped all margin resolution. This meant `margin: auto`
would not center the button horizontally.
Per CSS 2.1 Section 10.5, percentage heights should only resolve when
the containing block's height is "specified explicitly". This means a
containing block with `height: auto` and `min-height: 50px` does NOT
provide a definite height for percentage resolution - the child's
`height: 100%` should be treated as `auto`.
Previously, we checked `available_space.height.is_indefinite()` to
determine if percentage heights should become auto. However, this
conflated "available layout space" with "containing block height for
percentage resolution" - these are distinct concepts.
Now we check the containing block's `has_definite_height()` flag, which
correctly reflects whether the containing block has an explicit height
property. This handles:
- Anonymous wrapper blocks (skip them to find real containing block)
- Quirks mode (has special percentage height handling)
- Absolutely positioned elements (excluded, different rules apply)
Also update `calculate_inner_height()` to use the containing block's
actual used height when resolving percentages with indefinite available
space, which fixes inline-block and similar cases.
When userSpaceOnUse clips are nested inside objectBoundingBox masks,
the viewport_size was compounding incorrectly because it was calculated
after content scaling by m_parent_viewbox_transform. For userSpaceOnUse
clips (which have no viewBox), the fallback to content_width() returned
the already-scaled value, causing sizes to explode with each nesting
level.
Fix by calculating viewport dimensions before the scaling block. This
ensures m_viewport_size represents the coordinate system dimensions,
not the final pixel dimensions.
Compute inline-block baselines by traversing into nested block children
to find the last in-flow line box, using correct offsets relative to the
margin box edge.
Also ensure inline-flex and inline-grid containers always derive their
baseline from content (per CSS Align), and add special handling for
<input> elements which have `overflow: clip` in the UA stylesheet but
should still align adjacent text with their internal content.
We had two issues with ::backdrop which this commit fixes:
::backdrop is unique in that it's the previous sibling to its
originating element, instead of a child of it. This means when that
element's layout node is thrown away, the ::backdrop's is not.
A second issue is that if we do a partial layout rebuild, the
originating element's layout node replaces its previous one, but we
would still append a new layout node for ::backdrop to the root, so it
would appear in front of the originating element.
A related issue is that clear_pseudo_element_nodes() got called on the
element after its ::backdrop had been assigned, so it would immediately
lose track of it again.
To solve this, we now always remove the ::backdrop's layout node. If we
need to create a new one, we insert it before the element's layout node
if it has one, otherwise we append as before. This ensures we only ever
have up to one layout node for the ::backdrop, and it appears behind
its originating element.
To support this, create_pseudo_element_if_needed() has a couple of
changes:
- It returns the node that was created.
- The caller can ask it not to insert the node, so that the caller can
do so (which we use so that we can insert it in a specific place)
Reuse existing paintables during relayout to reduce GC allocation
pressure. Each paintable subclass implements reset_for_relayout()
to clear state before reuse.
This ensures that we are explicitly declaring the allocator to use when
allocating a cell(-inheriting) type, instead of silently falling back
to size-based allocation.
Since this is done in allocate_cell, this will only be detected for
types that are actively being allocated. However, since that means
they're _not_ being allocated, that means it's safe to not declare
an allocator to use for those. For example, the base TypedArray<T>,
which is never directly allocated and only the defined specializations
are ever allocated.
CSS allows inline elements with `position: relative` (or other
containing-block-establishing properties) to serve as the containing
block for their absolutely positioned descendants. However, our layout
system stores containing blocks as `Box*`, which cannot represent
inline elements (they are `InlineNode`, not `Box`).
This patch adds a workaround: when computing containing blocks, we
also check if there's an inline element between the abspos element
and its Box containing block that should actually be the CSS
containing block. If found, we store it in a new member called
`m_inline_containing_block_if_applicable` and use it during abspos
layout to:
1. Compute the inline's fragment bounding box as the containing
block rectangle (including padding, per CSS spec)
2. Resolve percentage-based insets against the inline's dimensions
3. Position the abspos element relative to the inline's location
Some details to be aware of:
- The inline containing block search happens in the function
`recompute_containing_block()` by walking DOM ancestors (not layout
tree ancestors, since the layout tree restructures blocks inside
inlines as siblings)
- For pseudo-elements like `::after`, we start the search from the
generating element itself, since it may be the inline containing
block
- Fragment offsets are relative to their block container, so we
translate the computed rect to the abspos element's containing
block coordinate system by accumulating offsets up the ancestor
chain
- When the abspos element uses static position (auto insets), we
don't apply the inline rect translation since static position is
already computed in the correct coordinate system
Long term, we want to refactor our "containing block" concept to
map more cleanly to the spec concept. That means turning it into
a rectangle instead of the box this rectangle was derived from.
That's an invasive change for another day though.
When calculating the width to use for intrinsic height determination of
flex items in a column layout, we were unconditionally ignoring
percentage min-width and max-width values.
This was overly conservative - when the containing block has a definite
width, percentages can be resolved. We now check if the available width
is definite and resolve percentages in that case.
This fixes the hero layout on https://slack.com/
When computing the width of a box with `width: auto` in an intrinsic
sizing context (min-content or max-content), we previously called
calculate_min/max_content_width() for every such box.
This was wasteful for boxes that don't establish their own formatting
context (e.g. plain divs in a BFC), since their intrinsic width is
simply the intrinsic width of their contents, which we're already
computing as part of the current layout pass.
By only computing intrinsic widths separately for boxes that actually
create a new formatting context (BFC, flex, grid, table, etc.), we
avoid creating redundant throwaway LayoutState objects and formatting
contexts.
For deeply nested structures with `width: max-content` on an ancestor,
this reduces the number of formatting contexts created from O(n) to
O(1), where n is the nesting depth.
Boxes with no layout children have no content, so their intrinsic
content size is zero by definition. This lets us skip creating a
throwaway layout state and running a formatting context just to
arrive at the same answer.
Instead of generating items lazily during iteration (and using a
lookahead queue for peeking), we now pre-generate all items upfront
in the constructor.
This allows next() and next_non_whitespace_sequence_width() to be
simple O(1) array accesses instead of doing generation work.
To handle tabs correctly (which need to know the accumulated width of
content before them on the line), we track m_accumulated_width_for_tabs
during pre-generation and reset it on forced breaks.
For ASCII characters, the bidirectional class is trivially known:
letters (A-Z, a-z) are Left-to-Right, and everything else (digits,
spaces, punctuation) is neutral.
This avoids expensive Unicode::bidirectional_class() lookups via ICU
for the common case of ASCII-only text.
For ASCII text, every character is its own grapheme - there are no
combining characters or emoji sequences. This means grapheme boundary
detection is trivial: next_boundary(i) is simply i+1.
This commit adds AsciiGraphemeSegmenter, a simple Segmenter subclass
that performs O(1) boundary lookups without any ICU overhead.
TextNode::grapheme_segmenter() now checks if the text is ASCII and uses
this fast path, avoiding expensive ICU BreakIterator cloning and
boundary detection for the common case of ASCII-only text.
Instead of lazily generating text chunks via ChunkIterator with a peek
queue that used O(n) take_first() operations, we now pre-generate all
chunks upfront when entering a text node.
This makes chunk access O(1) via simple array indexing, which speeds up
both next() calls and forward lookups for bidi direction resolution.
Add optional tracing that prints a tree visualization of formatting
context `run()` invocations. This is useful for debugging layout issues
where you need to understand the nesting and order of layout passes,
or why a box receives unexpected available space.
Example output:
```
├─ BFC <Viewport<#document>> run(definite(800) x definite(600))
│ ├─ BFC <BlockContainer<HTML>> run(definite(800) x indefinite)
│ │ ├─ IFC <BlockContainer(anonymous)> run(definite(800) x indefinite)
│ │ ├─ GFC <Box<DIV.grid>> run(definite(800) x indefinite)
│ │ │ ├─ BFC <BlockContainer<DIV.item>> run(definite(400) x indefinite)
```
For grid items with auto preferred size, stretch/normal alignment, and
no auto margins, the final size is simply the containing block size
minus margin box sizes. We can compute this directly without calling
`calculate_fit_content_width/height`, which triggers expensive intrinsic
sizing layout passes.
This adds visit_edges(Cell::Visitor&) methods to various helper structs
that contain GC pointers, and makes sure they are called from owning
GC-heap-allocated objects as needed.
These were found by our Clang plugin after expanding its capabilities.
The added rules will be enforced by CI going forward.
Previously we only allowed the viewport itself to be the containing
block for fixed-position elements, but the specs give us a few other
situations. Many of these are the same as for absolute-positioned ones.
When content changes inside a layout node, we now reset intrinsic size
caches only up to the nearest absolutely positioned ancestor, rather
than all the way to the document root.
This optimization is safe because absolutely positioned elements don't
contribute to their ancestors' intrinsic sizes - they are skipped in
min/max content width calculations.
The needs_layout_update flag still propagates to all ancestors so the
document knows layout is needed. Only the cache reset is bounded.
Previously, GridFormattingContext handled its own min-height constraints
internally, which caused infinite recursion when min-height was an
intrinsic sizing keyword (e.g., min-height: max-content).
This change moves the responsibility to the parent formatting context
(BFC). When any box establishing an independent formatting context has
auto height but non-auto min-height:
1. BFC measures content height using a throwaway LayoutState
2. If content height < min-height, BFC passes min-height as definite
available height to the child formatting context
3. Child FC runs once with correct constraints, unaware of min-height
Fixes https://github.com/LadybirdBrowser/ladybird/issues/4261 which
is corresponding to stack overflow on https://claude.ai/
When an item is placed with an unpositioned start and a positioned end
(e.g., `grid-row: auto / 1`), allow the start position to be negative.
This correctly creates implicit tracks before the explicit grid.
When distributing item contributions among flexible tracks with
intrinsic min sizing functions, we were skipping distribution entirely
when total_flex == 0. However, tracks like `0fr` (equivalent to
minmax(auto, 0fr)) should still receive the item's contribution
distributed equally among them.
Now we count intrinsic flexible tracks separately and distribute
equally when all flex factors are zero.
This commit includes two interdependent changes that must be applied
together:
- Treat `fit-content()` tracks as having an intrinsic min sizing
function when the limit resolves to zero.
- When clamping growth limit to fit-content limit, use `max(base_size,
fit_content_limit)` instead of just `fit_content_limit` to preserve
intrinsic sizing contributions.
These changes are coupled because the first change causes
`fit-content(0)` tracks to participate in intrinsic sizing, while the
second ensures the base size from that sizing is not discarded during
clamping.
Rewrite
`increase_sizes_to_accommodate_spanning_items_crossing_flexible_tracks`
to follow spec steps more closely:
1. Process all items spanning flexible tracks together (not grouped by
span size), taking the maximum contribution per track
2. Distribute item contributions proportionally to flex factors
3. Subtract non-flexible tracks existing sizes from item contribution
before distributing to flexible tracks
When resolving grid placement like `grid-column: 1 / a`, the start
position from the line number was being incorrectly overwritten when
processing the end identifier. Now we only set start from end if the
start placement doesn't already have a line number.
No tests are affected by the change itself, but combined with the
upcoming commits it's going to result in the progress for
`grid-flex-track-intrinsic-sizes-002.html`.
This patch fixes an issue where grid items using `grid-area: <name>`
with no matching named grid area or lines would be incorrectly placed
at position 0.
According to https://www.w3.org/TR/css-grid-1/#line-placement:
1. When a `<custom-ident>` doesn't match any named line, placement
should fall back to the first implicit grid line
2. When the same `<custom-ident>` is given for both `grid-*-start` and
`grid-*-end` (which happens with `grid-area: name`), and both fall
back to the same implicit line, the resulting span is 1
Previously, the fallback values were hardcoded to 0 and 1, which placed
items in track 0. The fix changes the fallback to use
`m_explicit_line_count` (the first implicit line index) for both start
and end identifiers. When both reference the same line (start == end),
the existing "both positioned" logic now handles this by setting span=1
and adjusting end accordingly.