mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-28 08:36:17 +08:00
Merge pull request #4236 from lightpanda-io/flex-shrink
css: flex-shrink handling
This commit is contained in:
3 files changed
+502
-33
No files matched your search
@@ -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 = .{},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user