Merge pull request #4236 from lightpanda-io/flex-shrink

css: flex-shrink handling
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-10-28 07:12:18 +08:00
commit 46ec342591
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: