css: flex-shrink handling

Builds ontop of recent width improvements (1) and adds flex-shrink handling.
Horizontal margins are now also considered in width calculation and flex free
space.

Also, all flex rows now go through the flex path, not just growers. And the
flex path considers things like clipped content and absolutely positioned
children.

(1) https://github.com/lightpanda-io/browser/pull/4078
    https://github.com/lightpanda-io/browser/pull/4189
    https://github.com/lightpanda-io/browser/pull/4221
This commit is contained in:
Karl Seguin committed 2026-10-28 06:41:47 +08:00
1 parent 7b12874e03
commit 6676867fe4
3 files changed
+502 -33

No files matched your search

+156 -8
View File
@@ -885,6 +885,49 @@ pub fn widthSizing(self: *StyleManager, el: *Element) WidthSizing {
return if (self.geometryProps(el).flex_grow) .grow else .shrink;
}
/// An in-flow child of a row flex container: one of the row's flex items.
pub fn isFlexRowItem(self: *StyleManager, el: *Element) bool {
self.assertOwns(el);
self.rebuildIfDirty() catch return false;
if (self.flowProps(el).position.outOfFlow()) {
return false;
}
const parent = self.boxParent(el) orelse return false;
const parent_vis = self.visibilityProps(parent);
return parent_vis.display == .flex and parent_vis.flex_column == false;
}
/// A flex container that wraps its items onto new lines instead of
/// shrinking them. The lines aren't modeled.
pub fn flexWraps(self: *StyleManager, el: *Element) bool {
self.assertOwns(el);
self.rebuildIfDirty() catch return false;
return self.visibilityProps(el).flex_wrap;
}
pub fn flexShrinks(self: *StyleManager, el: *Element) bool {
self.assertOwns(el);
self.rebuildIfDirty() catch return false;
return self.geometryProps(el).flex_no_shrink == false;
}
/// The left and right margins together. A percentage resolves against the
/// containing block's width.
pub fn inlineMargins(self: *StyleManager, el: *Element, containing_width: ?f64) f64 {
self.assertOwns(el);
self.rebuildIfDirty() catch return 0;
const geometry = self.geometryProps(el);
const viewport = self.frame.page.getViewport();
var total: f64 = 0;
for ([_]Length{ geometry.margin_left, geometry.margin_right }) |margin| {
total += margin.resolve(viewport) orelse switch (margin.unit) {
.percent => if (containing_width) |width| @as(f64, margin.value) / 100.0 * width else 0,
else => 0,
};
}
return total;
}
/// The nearest element to el that generates a box, or `el` itself it it does
pub fn boxElement(self: *StyleManager, el: *Element) ?*Element {
self.assertOwns(el);
@@ -1013,7 +1056,8 @@ fn widthLimit(self: *StyleManager, length: Length) ?WidthLimit {
};
}
pub const WidthKind = enum { box, content };
// row_shrink is a flex row's shrink ratio, not a width
pub const WidthKind = enum { box, content, full_content, row_shrink };
/// Element's computed widths (see Element.boxWidth), valid for one style
/// version and viewport. Each read otherwise walks up to the root and down
@@ -1027,6 +1071,8 @@ const WidthMemo = struct {
const Entry = struct {
box: ?f64 = null,
content: ?f64 = null,
full_content: ?f64 = null,
row_shrink: ?f64 = null,
};
};
@@ -1942,6 +1988,19 @@ pub const Length = packed struct(u35) {
};
}
// A margin may be negative and has no intrinsic sizes; auto is 0.
fn parseMargin(text: []const u8) Length {
const parsed = units.parse(text) catch return .{};
return switch (parsed.unit) {
inline .vw, .vh => |unit| .{ .value = @floatCast(parsed.value), .unit = @field(Unit, @tagName(unit)) },
.percentage => .{ .value = @floatCast(parsed.value), .unit = .percent },
else => .{
.value = @floatCast(parsed.value * (units.absoluteLengthFactor(parsed.unit) orelse return .{})),
.unit = .px,
},
};
}
fn isIntrinsic(text: []const u8) bool {
inline for (.{ "min-content", "max-content", "fit-content" }) |keyword| {
if (std.ascii.eqlIgnoreCase(text, keyword)) {
@@ -1964,11 +2023,13 @@ pub const Length = packed struct(u35) {
/// Whether an element is rendered and takes pointer input.
const Visibility = struct {
const Declared = struct {
const names = [_][]const u8{ "display", "visibility", "opacity", "pointer-events", "flex-direction", "flex-flow" };
// A shorthand comes before its longhands, so a later longhand wins
const names = [_][]const u8{ "display", "visibility", "opacity", "pointer-events", "flex-flow", "flex-direction", "flex-wrap" };
display: ?Display = null,
box: ?Box = null,
flex_column: ?bool = null,
flex_wrap: ?bool = null,
// Inner null: an explicit inherit, which still wins the cascade
visibility_hidden: ??bool = null,
opacity_zero: ?bool = null,
@@ -1978,9 +2039,23 @@ const Visibility = struct {
if (std.ascii.eqlIgnoreCase(name, "display")) {
self.display = Display.parse(value);
self.box = Box.parse(value);
} else if (std.ascii.eqlIgnoreCase(name, "flex-direction") or std.ascii.eqlIgnoreCase(name, "flex-flow")) {
// flex-flow's direction keyword comes first
self.flex_column = std.ascii.startsWithIgnoreCase(std.mem.trimStart(u8, value, &std.ascii.whitespace), "column");
} else if (std.ascii.eqlIgnoreCase(name, "flex-direction")) {
self.flex_column = isColumnDirection(value);
} else if (std.ascii.eqlIgnoreCase(name, "flex-wrap")) {
self.flex_wrap = isWrap(value);
} else if (std.ascii.eqlIgnoreCase(name, "flex-flow")) {
self.flex_column = false;
self.flex_wrap = false;
// Either order; an omitted one resets to row or nowrap
var it = std.mem.tokenizeAny(u8, value, &std.ascii.whitespace);
while (it.next()) |token| {
if (isColumnDirection(token)) {
self.flex_column = true;
}
if (isWrap(token)) {
self.flex_wrap = true;
}
}
} else if (std.ascii.eqlIgnoreCase(name, "visibility")) {
self.visibility_hidden = parseInheritedFlag(value, &.{ "hidden", "collapse" }, &.{ "visible", "initial" });
} else if (std.ascii.eqlIgnoreCase(name, "opacity")) {
@@ -1993,6 +2068,16 @@ const Visibility = struct {
});
}
}
fn isColumnDirection(value: []const u8) bool {
const trimmed = std.mem.trim(u8, value, &std.ascii.whitespace);
return std.ascii.eqlIgnoreCase(trimmed, "column") or std.ascii.eqlIgnoreCase(trimmed, "column-reverse");
}
fn isWrap(value: []const u8) bool {
const trimmed = std.mem.trim(u8, value, &std.ascii.whitespace);
return std.ascii.eqlIgnoreCase(trimmed, "wrap") or std.ascii.eqlIgnoreCase(trimmed, "wrap-reverse");
}
};
// The element's own values, from the cascade.
@@ -2003,6 +2088,7 @@ const Visibility = struct {
// Without an author `display` it's the tag's UA default, see finish
box: Box = .inline_level,
flex_column: bool = false,
flex_wrap: bool = false,
author_display: bool = false,
// Null takes the parent's value
visibility_hidden: ?bool = null,
@@ -2012,10 +2098,11 @@ const Visibility = struct {
outside_flat_tree: bool = false,
};
const Computed = packed struct(u11) {
const Computed = packed struct(u12) {
display: Display = .other,
box: Box = .inline_level,
flex_column: bool = false,
flex_wrap: bool = false,
author_display: bool = false,
visibility_hidden: bool = false, // inherited
pointer_events_none: bool = false, // inherited
@@ -2027,6 +2114,7 @@ const Visibility = struct {
.display = own.display,
.box = own.box,
.flex_column = own.flex_column,
.flex_wrap = own.flex_wrap,
.author_display = own.author_display,
.visibility_hidden = own.visibility_hidden orelse parent.visibility_hidden,
.pointer_events_none = own.pointer_events_none orelse parent.pointer_events_none,
@@ -2062,11 +2150,12 @@ const Visibility = struct {
/// An element's box: its size and how it scrolls.
const Geometry = struct {
const Declared = struct {
const names = [_][]const u8{ "overflow-x", "overflow-y", "overscroll-behavior-x", "overscroll-behavior-y", "width", "height", "min-width", "max-width", "flex", "flex-grow" };
const names = [_][]const u8{ "overflow-x", "overflow-y", "overscroll-behavior-x", "overscroll-behavior-y", "width", "height", "min-width", "max-width", "flex", "flex-grow", "flex-shrink", "margin", "margin-inline", "margin-left", "margin-right", "margin-inline-start", "margin-inline-end" };
overflow_x_scrolls: ?bool = null,
overflow_x_clips: ?bool = null,
flex_grow: ?bool = null,
flex_no_shrink: ?bool = null,
overflow_y_scrolls: ?bool = null,
overscroll_x_contains: ?bool = null,
overscroll_y_contains: ?bool = null,
@@ -2074,6 +2163,8 @@ const Geometry = struct {
height: ?Length = null,
min_width: ?Length = null,
max_width: ?Length = null,
margin_left: ?Length = null,
margin_right: ?Length = null,
fn apply(self: *Declared, name: []const u8, value: []const u8) void {
if (std.ascii.eqlIgnoreCase(name, "overflow-x")) {
@@ -2081,8 +2172,40 @@ const Geometry = struct {
self.overflow_x_clips = overflowClips(value);
} else if (std.ascii.eqlIgnoreCase(name, "flex-grow")) {
self.flex_grow = flexGrows(value);
} else if (std.ascii.eqlIgnoreCase(name, "flex-shrink")) {
if (flexFactor(value)) |factor| {
self.flex_no_shrink = factor == 0;
}
} else if (std.ascii.eqlIgnoreCase(name, "flex")) {
self.flex_grow = flexShorthandGrows(value);
self.flex_no_shrink = flexShorthandNoShrink(value);
} else if (std.ascii.eqlIgnoreCase(name, "margin")) {
var count: usize = 0;
// margin: top [right [bottom [left]]]
var tokens: [4][]const u8 = undefined;
var it = std.mem.tokenizeAny(u8, value, &std.ascii.whitespace);
while (it.next()) |token| : (count += 1) {
if (count == tokens.len) {
return;
}
tokens[count] = token;
}
if (count == 0) {
return;
}
const right = tokens[@min(count - 1, 1)];
self.margin_right = Length.parseMargin(right);
self.margin_left = Length.parseMargin(if (count == 4) tokens[3] else right);
} else if (std.ascii.eqlIgnoreCase(name, "margin-inline")) {
// margin-inline: start [end], in a left-to-right flow
var it = std.mem.tokenizeAny(u8, value, &std.ascii.whitespace);
const start = it.next() orelse return;
self.margin_left = Length.parseMargin(start);
self.margin_right = Length.parseMargin(it.next() orelse start);
} else if (std.ascii.eqlIgnoreCase(name, "margin-left") or std.ascii.eqlIgnoreCase(name, "margin-inline-start")) {
self.margin_left = Length.parseMargin(value);
} else if (std.ascii.eqlIgnoreCase(name, "margin-right") or std.ascii.eqlIgnoreCase(name, "margin-inline-end")) {
self.margin_right = Length.parseMargin(value);
} else if (std.ascii.eqlIgnoreCase(name, "overflow-y")) {
self.overflow_y_scrolls = overflowScrolls(value);
} else if (std.ascii.eqlIgnoreCase(name, "overscroll-behavior-x")) {
@@ -2119,6 +2242,12 @@ const Geometry = struct {
return number > 0;
}
// A non-negative <number>, null for anything else (a basis, a keyword)
fn flexFactor(value: []const u8) ?f64 {
const number = std.fmt.parseFloat(f64, std.mem.trim(u8, value, &std.ascii.whitespace)) catch return null;
return if (number >= 0) number else null;
}
// `flex: <grow> [<shrink>] [<basis>]`; `auto` grows, `none` and
// `initial` don't, and a lone width (`flex: 200px`) is a basis.
fn flexShorthandGrows(value: []const u8) bool {
@@ -2128,6 +2257,21 @@ const Geometry = struct {
return flexGrows(first);
}
// Only `none` and an explicit 0 shrink factor stop the shrinking; an
// omitted shrink is 1. The shrink is the number after the grow.
fn flexShorthandNoShrink(value: []const u8) bool {
var it = std.mem.tokenizeAny(u8, value, &std.ascii.whitespace);
const first = it.next() orelse return false;
if (std.ascii.eqlIgnoreCase(first, "none")) {
return true;
}
if (flexFactor(first) == null) {
return false;
}
const second = it.next() orelse return false;
return flexFactor(second) == 0;
}
// `contain` keeps the scroll in the box, `none` also kills the bounce we
// don't render anyway; only `auto` lets a scroll chain outward.
fn overscrollContains(value: []const u8) bool {
@@ -2136,10 +2280,11 @@ const Geometry = struct {
}
};
const Computed = packed struct(u146) {
const Computed = packed struct(u217) {
overflow_x_scrolls: bool = false,
overflow_x_clips: bool = false,
flex_grow: bool = false,
flex_no_shrink: bool = false,
overflow_y_scrolls: bool = false,
overscroll_x_contains: bool = false,
overscroll_y_contains: bool = false,
@@ -2148,6 +2293,9 @@ const Geometry = struct {
// auto, none and the intrinsic keywords don't limit
min_width: Length = .{},
max_width: Length = .{},
// auto, and values that need layout, are 0
margin_left: Length = .{},
margin_right: Length = .{},
};
};
+175
View File
@@ -444,6 +444,8 @@
testing.expectEqual(600, item.offsetWidth);
column.style.flexFlow = 'column wrap';
testing.expectEqual(600, item.offsetWidth);
column.style.flexFlow = 'wrap column-reverse';
testing.expectEqual(600, item.offsetWidth);
column.remove();
const grid = document.createElement('div');
@@ -560,6 +562,179 @@
}
</script>
<script id="flexItemsShrink">
{
// An overflowing row takes the overflow from its items in proportion to
// their widths. A header's `width: 100%` menu beside a fixed logo is what
// the logo leaves it, so a "logo + menu fits" check holds.
const row = document.createElement('div');
row.style.cssText = 'display: flex; width: 1000px';
const logo = document.createElement('div');
logo.style.cssText = 'width: 200px; flex-shrink: 0; margin-right: 32px';
const menu = document.createElement('div');
menu.style.width = '100%';
row.append(logo, menu);
document.body.appendChild(row);
testing.expectEqual(200, logo.offsetWidth);
testing.expectEqual(768, menu.offsetWidth);
testing.expectEqual(768, menu.getBoundingClientRect().width);
row.remove();
const pair = (a, b) => {
const row = document.createElement('div');
row.style.cssText = 'display: flex; width: 600px';
const first = document.createElement('div');
first.style.cssText = 'width: 400px; ' + a;
const second = document.createElement('div');
second.style.cssText = 'width: 400px; ' + b;
row.append(first, second);
document.body.appendChild(row);
return { row, first, second };
};
{
const { row, first, second } = pair('', '');
testing.expectEqual(300, first.offsetWidth);
testing.expectEqual(300, second.offsetWidth);
row.remove();
}
// flex-shrink: 0 and the shorthands without a shrink keep their width
for (const css of ['flex-shrink: 0', 'flex: none', 'flex: 1 0 auto', 'flex: 0 0 400px']) {
const { row, first, second } = pair(css, '');
testing.expectEqual(400, first.offsetWidth, css);
testing.expectEqual(200, second.offsetWidth, css);
row.remove();
}
for (const css of ['flex: 1', 'flex: 0 1 auto', 'flex: auto', 'flex-shrink: 2']) {
const { row, first } = pair(css, '');
testing.expectEqual(300, first.offsetWidth, css);
row.remove();
}
// An item doesn't shrink below its content (min-width: auto); what it
// can't give, the others do
{
const { row, first, second } = pair('', '');
const content = document.createElement('div');
content.style.width = '350px';
first.appendChild(content);
testing.expectEqual(350, first.offsetWidth);
testing.expectEqual(250, second.offsetWidth);
// ... unless a min-width replaces that floor, or it clips its content
for (const css of ['min-width: 0', 'overflow: hidden']) {
first.style.cssText = 'width: 400px; ' + css;
second.style.flexShrink = '0';
testing.expectEqual(200, first.offsetWidth, css);
second.style.flexShrink = '';
}
row.remove();
}
// A content-sized item counts content a descendant clips (rc-menu's
// overflow: hidden list), and can shrink as far as the content left
// unclipped. Chrome stops at the widest unbreakable child instead (no
// min-content model here); this overflow lands on it either way.
{
const row = document.createElement('div');
row.style.cssText = 'display: flex; width: 600px';
const wrapper = document.createElement('div');
const list = document.createElement('ul');
list.style.overflow = 'hidden';
for (let i = 0; i < 3; i++) {
const item = document.createElement('li');
item.style.cssText = 'display: inline-block; width: 100px';
list.appendChild(item);
}
wrapper.appendChild(list);
row.appendChild(wrapper);
document.body.appendChild(row);
testing.expectEqual(300, wrapper.offsetWidth);
testing.expectEqual(300, list.offsetWidth);
const filler = document.createElement('div');
filler.style.cssText = 'width: 500px; flex-shrink: 0';
row.appendChild(filler);
testing.expectEqual(100, wrapper.offsetWidth);
testing.expectEqual(100, list.offsetWidth);
row.remove();
}
// A wrapping row moves items to new lines instead of shrinking them
for (const css of ['flex-wrap: wrap', 'flex-wrap: wrap-reverse', 'flex-flow: row wrap', 'flex-flow: wrap']) {
const row = document.createElement('div');
row.style.cssText = 'display: flex; width: 600px; ' + css;
const items = [];
for (let i = 0; i < 4; i++) {
const item = document.createElement('div');
item.style.width = '200px';
items.push(item);
}
row.append(...items);
document.body.appendChild(row);
testing.expectEqual(200, items[0].offsetWidth, css);
// A later nowrap puts it back
row.style.flexWrap = 'nowrap';
testing.expectEqual(150, items[0].offsetWidth, css);
row.remove();
}
// A row with room to spare shrinks nothing
{
const { row, first } = pair('', '');
row.style.width = '900px';
testing.expectEqual(400, first.offsetWidth);
row.remove();
}
}
</script>
<script id="horizontalMargins">
{
const section = document.createElement('div');
section.style.width = '700px';
document.body.appendChild(section);
// A block fills its containing block less its margins; negative margins
// widen it, percentages resolve against the containing block
const block = document.createElement('div');
section.appendChild(block);
for (const [css, width] of [['margin: 0 50px', 600], ['margin: 0 10px 0 40px', 650], ['margin-left: -20px', 720], ['margin: 10%', 560], ['margin-inline: 25px 75px', 600], ['margin-inline: 50px; margin-left: 0', 650], ['margin: 0 auto', 700]]) {
block.style.cssText = css;
testing.expectEqual(width, block.offsetWidth, css);
}
// A shrink-to-fit box counts its children's margins
const inline = document.createElement('div');
inline.style.display = 'inline-block';
const child = document.createElement('div');
child.style.cssText = 'width: 40px; margin: 0 10px';
inline.appendChild(child);
section.appendChild(inline);
testing.expectEqual(60, inline.offsetWidth);
section.remove();
// Growers share what's left after everyone's margins
const row = document.createElement('div');
row.style.cssText = 'display: flex; width: 800px';
const menu = document.createElement('div');
menu.style.cssText = 'width: 40px; flex-shrink: 0; margin-right: 10px';
const pane = document.createElement('div');
pane.style.flex = '1';
row.append(menu, pane);
document.body.appendChild(row);
testing.expectEqual(750, pane.offsetWidth);
// Out-of-flow children aren't flex items
const badge = document.createElement('div');
badge.style.cssText = 'position: absolute; width: 100px';
row.appendChild(badge);
testing.expectEqual(750, pane.offsetWidth);
row.remove();
}
</script>
<script id="growingItemInShrinkToFitRow">
{
// The row sizes from its items and a growing item from the row: the row
+171 -25
View File
@@ -1712,13 +1712,27 @@ fn boxWidth(self: *Element, owner: *Frame) f64 {
// html/body return their set value regardless of children.
break :blk own.value;
}
const width = if (own.explicit) own.value else switch (style_manager.widthSizing(self)) {
const sizing = style_manager.widthSizing(self);
if (sizing == .contents) {
break :blk 0;
}
if (style_manager.isFlexRowItem(self)) {
const row = style_manager.boxParent(self).?;
const item = self.flexBase(row.boxWidth(owner), owner);
// Snapped to Chrome's 1/64px layout unit, past the float error
const shrunk = @round(item.base * (1 - row.rowShrinkRatio(owner)) * 64) / 64;
var width = @max(item.floor, shrunk);
if (sizing == .grow and own.explicit == false) {
width = @max(width, self.growShare(owner));
}
break :blk self.clampWidth(width, .box, owner);
}
const width = if (own.explicit) own.value else switch (sizing) {
.fixed => |width| width,
.percent => |fraction| fraction * self.containingBlockWidth(owner),
.fill => self.containingBlockWidth(owner),
.grow => @max(own.value, self.growShare(owner), self.contentWidth(owner)),
.shrink => @max(own.value, self.contentWidth(owner)),
.contents => break :blk 0,
.fill => self.fillWidth(owner),
.grow, .shrink => @max(own.value, self.contentWidth(.clipped, owner)),
.contents => unreachable,
};
break :blk self.clampWidth(width, .box, owner);
};
@@ -1726,17 +1740,144 @@ fn boxWidth(self: *Element, owner: *Frame) f64 {
return width;
}
// What the box adds to its parent's content width.
fn widthContribution(self: *Element, owner: *Frame) f64 {
// How a box that clips its overflow counts toward its ancestors' content.
const ContentMode = enum {
full,
clipped,
};
// What the box, with its margins, adds to its parent's content width.
fn widthContribution(self: *Element, comptime mode: ContentMode, owner: *Frame) f64 {
const style_manager = &owner._style_manager;
const own = self.getElementAxis(owner, .width);
const width = if (own.explicit) own.value else switch (style_manager.widthSizing(self)) {
.fixed => |width| width,
.shrink, .grow => @max(own.value, self.contentWidth(owner)),
.percent, .fill => if (style_manager.clipsOverflowX(self)) 0 else self.contentWidth(owner),
.contents => return self.contentWidth(owner),
.shrink, .grow => @max(own.value, self.contentWidth(mode, owner)),
.percent, .fill => if (mode == .clipped and style_manager.clipsOverflowX(self)) 0 else self.contentWidth(mode, owner),
.contents => return self.contentWidth(mode, owner),
};
return self.clampWidth(width, .contribution, owner);
return self.clampWidth(width, .contribution, owner) + style_manager.inlineMargins(self, null);
}
const FlexBase = struct {
base: f64, // The width before the row shrinks it
floor: f64, // The least it shrinks to; base when it doesn't shrink
};
// Get a flex row's item hypothetical width and how far it can shrink.
// No flex-basis (yet?)/
fn flexBase(self: *Element, row_width: f64, owner: *Frame) FlexBase {
const style_manager = &owner._style_manager;
const own = self.getElementAxis(owner, .width);
const width = if (own.explicit) own.value else switch (style_manager.widthSizing(self)) {
.fixed => |width| width,
.percent => |fraction| fraction * row_width,
.shrink, .grow, .fill => @max(own.value, self.contentWidth(.full, owner)),
.contents => return .{
.base = self.widthContribution(.full, owner),
.floor = self.widthContribution(.clipped, owner),
},
};
const base = self.clampWidth(width, .box, owner);
if (style_manager.flexShrinks(self) == false) {
return .{ .base = base, .floor = base };
}
return .{ .base = base, .floor = self.flexMinWidth(base, owner) };
}
// min-width: auto on a flex item: no narrower than its content, or than its
// width if that's narrower; 0 if it clips its overflow (CSS Flexbox §4.5).
// Content its descendants clip doesn't count. A declared min-width replaces
// it, and clampWidth applies that one.
fn flexMinWidth(self: *Element, base: f64, owner: *Frame) f64 {
const style_manager = &owner._style_manager;
if (style_manager.widthLimits(self).min != null or style_manager.clipsOverflowX(self)) {
return 0;
}
if (self.getTag().isReplacedOrControl()) {
// Its intrinsic width is unknown
return base;
}
const own = self.getElementAxis(owner, .width);
const content = self.contentWidth(.clipped, owner);
// An auto width's fallback stands in for its text
return @min(base, if (own.explicit) content else @max(own.value, content));
}
// The fraction of its base each shrinking item of an overflowing flex row
// gives up: the overflow split in proportion to the bases (a flex-shrink is
// 0 or 1 here). Items that would drop below their floor freeze there and the
// rest share what they couldn't give (CSS Flexbox §9.7). A wrapping row moves
// items to a new line instead, so it shrinks nothing.
fn rowShrinkRatio(self: *Element, owner: *Frame) f64 {
const style_manager = &owner._style_manager;
if (style_manager.flexWraps(self)) {
return 0;
}
if (style_manager.cachedWidth(self, .row_shrink)) |ratio| {
return ratio;
}
const row_width = self.boxWidth(owner);
var total: f64 = 0;
var items: TreeWalker.FlatChildren = .init(self.asNode(), .{ .frame = owner });
while (nextFlexItem(&items, style_manager)) |el| {
total += el.flexBase(row_width, owner).base + flexItemMargins(el, row_width, style_manager);
}
var ratio: f64 = 0;
const overflow = total - row_width;
if (overflow > 0) {
// Freezing an item leaves more for the others to give, so the ratio
// only grows between passes and a frozen item stays frozen: each pass
// freezes at least one more item, or the count holds and it's settled.
var frozen_before: ?usize = null;
while (true) {
var frozen: usize = 0;
var given: f64 = 0;
var shrinkable: f64 = 0;
items = .init(self.asNode(), .{ .frame = owner });
while (nextFlexItem(&items, style_manager)) |el| {
const item = el.flexBase(row_width, owner);
if (item.base <= item.floor) {
continue;
}
if (item.base * (1 - ratio) < item.floor) {
frozen += 1;
given += item.base - item.floor;
} else {
shrinkable += item.base;
}
}
ratio = if (shrinkable == 0) 1 else std.math.clamp((overflow - given) / shrinkable, 0, 1);
if (frozen_before) |previous| {
if (frozen <= previous) break;
}
frozen_before = frozen;
}
}
style_manager.cacheWidth(self, .row_shrink, ratio);
return ratio;
}
// The next child of a flex row that is one of its items: rendered and in flow.
fn nextFlexItem(children: *TreeWalker.FlatChildren, style_manager: *StyleManager) ?*Element {
while (children.next()) |node| {
const el = node.is(Element) orelse continue;
if (style_manager.hasDisplayNone(el) or style_manager.isFlexRowItem(el) == false) {
continue;
}
return el;
}
return null;
}
// display: contents has no margins; its children's are in its contribution.
fn flexItemMargins(el: *Element, row_width: f64, style_manager: *StyleManager) f64 {
if (style_manager.widthSizing(el) == .contents) {
return 0;
}
return style_manager.inlineMargins(el, row_width);
}
// max-width caps the width, min-width wins over both.
@@ -1759,6 +1900,12 @@ fn resolveWidthLimit(self: *Element, limit: StyleManager.WidthLimit, resolve_per
};
}
// A block in normal flow fills its containing block, less its margins.
fn fillWidth(self: *Element, owner: *Frame) f64 {
const containing = self.containingBlockWidth(owner);
return @max(0, containing - owner._style_manager.inlineMargins(self, containing));
}
/// The width of the containing block; the viewport above the root.
fn containingBlockWidth(self: *Element, owner: *Frame) f64 {
const parent = owner._style_manager.containingBlock(self) orelse {
@@ -1768,27 +1915,25 @@ fn containingBlockWidth(self: *Element, owner: *Frame) f64 {
}
/// A growing flex item's share: the row's free space, after the siblings
/// that don't grow, split evenly among those that do.
/// that don't grow and everyone's margins, split evenly among those that do.
fn growShare(self: *Element, owner: *Frame) f64 {
const style_manager = &owner._style_manager;
const parent = style_manager.boxParent(self) orelse return 0;
const row_width = parent.boxWidth(owner);
var taken: f64 = 0;
var growers: f64 = 0;
var children: TreeWalker.FlatChildren = .init(parent.asNode(), .{ .frame = owner });
while (children.next()) |node| {
const el = node.is(Element) orelse continue;
if (style_manager.hasDisplayNone(el)) {
continue;
}
var items: TreeWalker.FlatChildren = .init(parent.asNode(), .{ .frame = owner });
while (nextFlexItem(&items, style_manager)) |el| {
switch (style_manager.widthSizing(el)) {
.grow => growers += 1,
// Its children are the row's items
.contents => taken += el.widthContribution(owner),
.contents => taken += el.widthContribution(.clipped, owner),
// Only walks up, and the row's width never reads a child's box
else => taken += el.boxWidth(owner),
}
taken += flexItemMargins(el, row_width, style_manager);
}
return @max(0, parent.boxWidth(owner) - taken) / @max(1, growers);
return @max(0, row_width - taken) / @max(1, growers);
}
// The width the children would take unconstrained: the sum of their
@@ -1797,9 +1942,10 @@ fn growShare(self: *Element, owner: *Frame) f64 {
// share one line that never wraps (the dummy layout has no line breaking,
// which keeps the "append until the lane outgrows its shell" idiom
// terminating).
fn contentWidth(self: *Element, owner: *Frame) f64 {
fn contentWidth(self: *Element, comptime mode: ContentMode, owner: *Frame) f64 {
const kind: StyleManager.WidthKind = if (mode == .clipped) .content else .full_content;
const style_manager = &owner._style_manager;
if (style_manager.cachedWidth(self, .content)) |width| {
if (style_manager.cachedWidth(self, kind)) |width| {
return width;
}
@@ -1812,7 +1958,7 @@ fn contentWidth(self: *Element, owner: *Frame) f64 {
if (style_manager.hasDisplayNone(el)) {
continue;
}
const width = el.widthContribution(owner);
const width = el.widthContribution(mode, owner);
if (in_row or style_manager.isBlockLevel(el) == false) {
line += width;
} else {
@@ -1821,7 +1967,7 @@ fn contentWidth(self: *Element, owner: *Frame) f64 {
}
}
const width = @max(widest, line);
style_manager.cacheWidth(self, .content, width);
style_manager.cacheWidth(self, kind, width);
return width;
}
@@ -1955,7 +2101,7 @@ pub fn getScrollWidth(self: *Element, frame: *Frame) f64 {
}
const owner = self.ownerFrame(frame) orelse return self.getElementAxis(frame, .width).value;
return @max(self.boxWidth(owner), self.contentWidth(owner));
return @max(self.boxWidth(owner), self.contentWidth(.clipped, owner));
}
/// Null where we can't prove a limit, which leaves the offset unbounded: