fix: preserve intrinsic row sizing across containing blocks

This commit is contained in:
Kinneyzhang 2026-08-25 20:55:43 +08:00
parent b25328198c
commit 175ae2ed4e
11 changed files with 239 additions and 19 deletions

View File

@ -87,6 +87,12 @@ container children are normally supplied to `ebox-row`, `ebox-column`,
use the variadic row/column helpers for more than two children. Nil children use the variadic row/column helpers for more than two children. Nil children
are ignored. An empty row or column becomes a spacer. are ignored. An empty row or column becomes a spacer.
In a `row`, omitted or `auto` widths use each childs intrinsic width when the
children are concatenated; the parent viewport is not reused as every childs
width. Explicit `viewport`, `stretch`, and definite widths still resolve against
their containing block. A `column` fills narrower layout rows to its containing
block, while a visual box paints its border and padding at its own used width.
`:key` is the stable application identity for a sibling. `:id` is a logical `:key` is the stable application identity for a sibling. `:id` is a logical
selector/update identifier. `:host-ref` is an opaque application-owned anchor selector/update identifier. `:host-ref` is an opaque application-owned anchor
used by host-reference lookup and logical candidate replacement. Host refs used by host-reference lookup and logical candidate replacement. Host refs

View File

@ -78,6 +78,12 @@ window再切换到渲染后的 buffer。如果窗口选择由调用方负责
`ebox-stack` 接受两个节点;超过两个子节点时使用 row/column。nil 子节点会 `ebox-stack` 接受两个节点;超过两个子节点时使用 row/column。nil 子节点会
被忽略;空 row/column 会变成 spacer。 被忽略;空 row/column 会变成 spacer。
`row` 中省略或 `auto` 的宽度按子节点自身的 intrinsic width 参与横向拼接;
父级 viewport 不会被重复当成每个子节点的宽度。显式 `viewport`、`stretch`
或 definite width 仍按指定 containing block 解析。`column` 会把较窄的布局
行补齐到自己的 containing block但带视觉 box 的子节点会在自己的 used width
上绘制边框和 padding。
`:key` 是兄弟节点范围内的稳定业务 identity`:id` 是 logical selector/update `:key` 是兄弟节点范围内的稳定业务 identity`:id` 是 logical selector/update
标识;`:host-ref` 是应用拥有的不透明锚点,用于 host-reference 查询和逻辑 标识;`:host-ref` 是应用拥有的不透明锚点,用于 host-reference 查询和逻辑
candidate replacement。一个 source tree 内的非 nil host-ref 必须唯一。 candidate replacement。一个 source tree 内的非 nil host-ref 必须唯一。

View File

@ -424,12 +424,14 @@ this avoids signature and retained-cost work for one-off responsive sizes."
(context (context
(list (ebox--ensure-node-id source) (list (ebox--ensure-node-id source)
axis main cross align viewport axis main cross align viewport
ebox--intrinsic-layout-measurement))) ebox--intrinsic-layout-measurement
ebox--inline-auto-width-intrinsic-p)))
(if (gethash context observations) (if (gethash context observations)
(let ((ebox-viewport-width viewport)) (let ((ebox-viewport-width viewport))
(list (ebox--current-display-signature) (list (ebox--current-display-signature)
viewport ebox-viewport-height viewport ebox-viewport-height
ebox--intrinsic-layout-measurement ebox--intrinsic-layout-measurement
ebox--inline-auto-width-intrinsic-p
ebox--render-region-id ebox--render-region-id
ebox--scroll-window-initial-lookahead-lines-override ebox--scroll-window-initial-lookahead-lines-override
ebox--scroll-window-render-disabled ebox--scroll-window-render-disabled
@ -964,6 +966,8 @@ Return a sized render entry containing the rendered string and dimensions."
source axis main cross align item-viewport source axis main cross align item-viewport
(list :intrinsic-layout-measurement (list :intrinsic-layout-measurement
ebox--intrinsic-layout-measurement ebox--intrinsic-layout-measurement
:inline-auto-width-intrinsic
ebox--inline-auto-width-intrinsic-p
:scroll-window-render-disabled :scroll-window-render-disabled
ebox--scroll-window-render-disabled ebox--scroll-window-render-disabled
:viewport-height-dependent :viewport-height-dependent

View File

@ -333,6 +333,8 @@ the short runway that makes immediate post-resize smooth scrolling cheap."
(defvar ebox--viewport-dependent-subtree-cache nil (defvar ebox--viewport-dependent-subtree-cache nil
"Dynamic render-pass cache for viewport dependency existence checks.") "Dynamic render-pass cache for viewport dependency existence checks.")
(defvar ebox--inline-auto-width-intrinsic-p)
(defvar ebox--viewport-height-dependent-subtree-cache nil (defvar ebox--viewport-height-dependent-subtree-cache nil
"Dynamic render-pass cache for viewport-height dependency checks.") "Dynamic render-pass cache for viewport-height dependency checks.")
@ -420,6 +422,7 @@ Kept as a narrow predicate for callers that need box-only sizing semantics."
ebox-viewport-width) ebox-viewport-width)
(when (ebox--viewport-height-dependent-subtree-p node) (when (ebox--viewport-height-dependent-subtree-p node)
ebox-viewport-height) ebox-viewport-height)
ebox--inline-auto-width-intrinsic-p
(ebox--render-cache-node-body-signature node))) (ebox--render-cache-node-body-signature node)))
(defun ebox--render-cache-box-signature (box) (defun ebox--render-cache-box-signature (box)

View File

@ -69,6 +69,12 @@
Descendants still see the current containing block, but stack/column containers Descendants still see the current containing block, but stack/column containers
must not stretch their own outer width to that containing block.") must not stretch their own outer width to that containing block.")
(defvar ebox--inline-auto-width-intrinsic-p nil
"Non-nil while a horizontal layout sizes auto-width children intrinsically.
The ambient viewport remains available to explicit `viewport' and `stretch'
widths, but an omitted or `auto' width must not consume the whole horizontal
containing block merely because the row itself is being rendered in it.")
(defvar ebox--box-content-render-cache nil (defvar ebox--box-content-render-cache nil
"Dynamic render-pass cache for rendered lazy child content.") "Dynamic render-pass cache for rendered lazy child content.")
@ -882,6 +888,7 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
(list ebox-viewport-width (list ebox-viewport-width
ebox-viewport-height ebox-viewport-height
ebox--intrinsic-layout-measurement ebox--intrinsic-layout-measurement
ebox--inline-auto-width-intrinsic-p
ebox--render-region-id)) ebox--render-region-id))
(defun ebox--render-box-content-node (box node) (defun ebox--render-box-content-node (box node)
@ -1021,7 +1028,8 @@ property parsers."
props (ebox-get props :max-width))) props (ebox-get props :max-width)))
(preferred-width (preferred-width
(or (ebox--definite-content-size-pixel props width-value) (or (ebox--definite-content-size-pixel props width-value)
(and (or (null width-value) (and (not ebox--inline-auto-width-intrinsic-p)
(or (null width-value)
(eq width-value 'auto) (eq width-value 'auto)
(eq width-value 'fit-content)) (eq width-value 'fit-content))
(ebox--stretch-content-pixel props)) (ebox--stretch-content-pixel props))
@ -1041,10 +1049,15 @@ an inherited viewport."
(cond (cond
((eq (ebox-get props :width) 'max-content) ((eq (ebox-get props :width) 'max-content)
(let ((ebox-viewport-width nil) (let ((ebox-viewport-width nil)
(ebox--intrinsic-layout-measurement t)) (ebox--intrinsic-layout-measurement t)
(ebox--inline-auto-width-intrinsic-p nil))
(ebox-render node))) (ebox-render node)))
(content-viewport (content-viewport
(let ((ebox-viewport-width content-viewport)) (let ((ebox-viewport-width content-viewport)
;; A definite wrapper establishes a new containing block. Its
;; child layout must resume normal cross-axis stretch semantics,
;; even when the wrapper itself is an item of an intrinsic row.
(ebox--inline-auto-width-intrinsic-p nil))
(if (and (hash-table-p ebox--render-cache-table) (if (and (hash-table-p ebox--render-cache-table)
(not ebox--intrinsic-layout-measurement) (not ebox--intrinsic-layout-measurement)
(null ebox--render-region-id) (null ebox--render-region-id)
@ -1058,6 +1071,8 @@ an inherited viewport."
:content-viewport content-viewport :content-viewport content-viewport
:viewport-height ebox-viewport-height :viewport-height ebox-viewport-height
:intrinsic ebox--intrinsic-layout-measurement :intrinsic ebox--intrinsic-layout-measurement
:auto-width-intrinsic
ebox--inline-auto-width-intrinsic-p
:render-region-id ebox--render-region-id :render-region-id ebox--render-region-id
:scroll-lookahead :scroll-lookahead
ebox--scroll-window-initial-lookahead-lines-override ebox--scroll-window-initial-lookahead-lines-override
@ -1103,7 +1118,8 @@ FALLBACK is used for nil and auto values."
(preferred-width (preferred-width
(cond (cond
((or (null width-value) (eq width-value 'auto)) ((or (null width-value) (eq width-value 'auto))
(or (ebox--stretch-content-pixel box) (or (and (not ebox--inline-auto-width-intrinsic-p)
(ebox--stretch-content-pixel box))
(ebox--content-max-pixel box))) (ebox--content-max-pixel box)))
((or (eq width-value 'viewport) ((or (eq width-value 'viewport)
(memq width-value '(stretch contain))) (memq width-value '(stretch contain)))
@ -1688,9 +1704,18 @@ A sole child Range keeps a material row parent so the Range stays addressable."
(defun ebox--render-concat (node) (defun ebox--render-concat (node)
"Render a concat NODE to a string (internal, called by `ebox-render'). "Render a concat NODE to a string (internal, called by `ebox-render').
Children are rendered; shorter children are padded with blank lines so all Auto-width children are rendered at intrinsic inline size while explicit
reach the same height before horizontal concatenation." viewport/definite widths keep their containing-block semantics. Shorter
(let* ((rendered (mapcar #'ebox--render-with-cache children are padded with blank lines so all reach the same height before
horizontal concatenation."
(let* ((rendered
(mapcar (lambda (child)
;; A row owns the horizontal concatenation. Its auto
;; width children must therefore measure intrinsically;
;; explicit viewport/stretch widths still resolve against
;; the ambient containing block.
(let ((ebox--inline-auto-width-intrinsic-p t))
(ebox--render-with-cache child)))
(ebox--layout-children node))) (ebox--layout-children node)))
(max-h (if rendered (max-h (if rendered
(apply #'max (mapcar #'ebox-string-height rendered)) (apply #'max (mapcar #'ebox-string-height rendered))
@ -1925,7 +1950,9 @@ Return a plist with :lines and :complete. This mirrors the normal stack
renderer for the top window of a scroll container, but stops before hidden renderer for the top window of a scroll container, but stops before hidden
leaves are rendered. When CACHE is supplied, rendering continues from the leaves are rendered. When CACHE is supplied, rendering continues from the
last cached leaf instead of walking the stack from the beginning." last cached leaf instead of walking the stack from the beginning."
(let* ((target-w (or (ebox--viewport-pixel-width nil) 0)) (let* ((target-w (if ebox--inline-auto-width-intrinsic-p
0
(or (ebox--viewport-pixel-width nil) 0)))
(cached (and (ebox--stack-window-cache-compatible-p (cached (and (ebox--stack-window-cache-compatible-p
cache node target-w) cache node target-w)
cache)) cache))
@ -3054,7 +3081,9 @@ Stack leaves are rendered independently. When a containing-block viewport is
bound, narrower rows are padded to that available width, while wider definite bound, narrower rows are padded to that available width, while wider definite
children keep their own width and overflow only on their own rows. During children keep their own width and overflow only on their own rows. During
intrinsic measurement, descendants still receive the containing block but the intrinsic measurement, descendants still receive the containing block but the
stack itself uses its natural max child width." stack itself uses its natural max child width. A stack used as an intrinsic
inline child follows the same natural-width rule and does not consume the
ambient horizontal viewport."
(let* ((rendered-items (let* ((rendered-items
(mapcar (lambda (leaf) (mapcar (lambda (leaf)
(let ((rendered (ebox--render-with-cache leaf))) (let ((rendered (ebox--render-with-cache leaf)))
@ -3064,7 +3093,8 @@ stack itself uses its natural max child width."
(max-w (if rendered-items (max-w (if rendered-items
(apply #'max (mapcar #'cdr rendered-items)) (apply #'max (mapcar #'cdr rendered-items))
0)) 0))
(target-w (if ebox--intrinsic-layout-measurement (target-w (if (or ebox--intrinsic-layout-measurement
ebox--inline-auto-width-intrinsic-p)
max-w max-w
(or (ebox--viewport-pixel-width nil) max-w)))) (or (ebox--viewport-pixel-width nil) max-w))))
(let ((rendered (let ((rendered

View File

@ -330,6 +330,7 @@ pub struct LayoutContext {
pub viewport_width: i64, pub viewport_width: i64,
pub viewport_width_known: bool, pub viewport_width_known: bool,
pub viewport_height: i64, pub viewport_height: i64,
pub inline_auto_width_intrinsic: bool,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -3416,7 +3417,9 @@ fn resolve_child_viewport_width(
let minimum = definite(min_width).unwrap_or(0); let minimum = definite(min_width).unwrap_or(0);
let maximum = definite(max_width); let maximum = definite(max_width);
let auto_stretch = if matches!(width, Size::Auto) { let auto_stretch = if matches!(width, Size::Auto) {
stretch (!context.inline_auto_width_intrinsic)
.then_some(stretch)
.flatten()
} else { } else {
None None
}; };
@ -3512,7 +3515,11 @@ fn resolve_definite_box_content_width(
let maximum = definite(max_width); let maximum = definite(max_width);
let preferred = override_width let preferred = override_width
.or_else(|| definite(width)) .or_else(|| definite(width))
.or_else(|| matches!(width, Size::Auto).then_some(stretch).flatten()) .or_else(|| {
(matches!(width, Size::Auto) && !context.inline_auto_width_intrinsic)
.then_some(stretch)
.flatten()
})
.or(maximum); .or(maximum);
preferred.map(|preferred| minimum.max(preferred.max(0).min(maximum.unwrap_or(i64::MAX)))) preferred.map(|preferred| minimum.max(preferred.max(0).min(maximum.unwrap_or(i64::MAX))))
} }
@ -4177,6 +4184,7 @@ fn measure_flex_item<'a>(
}, },
viewport_width_known: uses_inline_viewport && inline_viewport.is_some(), viewport_width_known: uses_inline_viewport && inline_viewport.is_some(),
viewport_height: context.viewport_height, viewport_height: context.viewport_height,
inline_auto_width_intrinsic: context.inline_auto_width_intrinsic,
}; };
let rendered = render_node(&item.node, measurement_context, uses_inline_viewport)?; let rendered = render_node(&item.node, measurement_context, uses_inline_viewport)?;
let min_main = flex_min_main(&item.node, &rendered, axis, context)?; let min_main = flex_min_main(&item.node, &rendered, axis, context)?;
@ -4546,6 +4554,7 @@ fn render_flex_sized_entry(
item_viewport.is_some() || context.viewport_width_known item_viewport.is_some() || context.viewport_width_known
}, },
viewport_height: context.viewport_height, viewport_height: context.viewport_height,
inline_auto_width_intrinsic: context.inline_auto_width_intrinsic,
}; };
let override_size = box_override_for_flex(item.source, axis, main, cross, stretch); let override_size = box_override_for_flex(item.source, axis, main, cross, stretch);
let mut rendered = let mut rendered =
@ -4706,7 +4715,11 @@ fn render_node_window(
return Some(render_node(node, context, intrinsic)); return Some(render_node(node, context, intrinsic));
} }
match node { match node {
LayoutNode::Column { children } if !intrinsic && context.viewport_width_known => { LayoutNode::Column { children }
if !intrinsic
&& !context.inline_auto_width_intrinsic
&& context.viewport_width_known =>
{
Some(render_column_window(children, context, start, height)) Some(render_column_window(children, context, start, height))
} }
_ => Some( _ => Some(
@ -5286,6 +5299,8 @@ fn render_node_with_override(
viewport_width_known: !intrinsic_child viewport_width_known: !intrinsic_child
&& (preliminary_child_width.is_some() || context.viewport_width_known), && (preliminary_child_width.is_some() || context.viewport_width_known),
viewport_height: preliminary_child_height.unwrap_or(context.viewport_height), viewport_height: preliminary_child_height.unwrap_or(context.viewport_height),
inline_auto_width_intrinsic: context.inline_auto_width_intrinsic
&& matches!(effective_width, Size::Auto),
}; };
if simple_scroll_window { if simple_scroll_window {
if let Some(total_height) = exact_rendered_height(child, child_context) { if let Some(total_height) = exact_rendered_height(child, child_context) {
@ -5331,7 +5346,11 @@ fn render_node_with_override(
let rendered = child_rendered.as_ref().expect("validated child"); let rendered = child_rendered.as_ref().expect("validated child");
(rendered.min_content_width(*wrap_mode), rendered.max_width()) (rendered.min_content_width(*wrap_mode), rendered.max_width())
}; };
let auto_width = stretch.unwrap_or(max_content); let auto_width = if context.inline_auto_width_intrinsic {
max_content
} else {
stretch.unwrap_or(max_content)
};
let minimum = resolve_width( let minimum = resolve_width(
min_width, min_width,
Some(0), Some(0),
@ -5630,9 +5649,13 @@ fn render_node_with_override(
Ok(rendered) Ok(rendered)
} }
LayoutNode::Row { children } => { LayoutNode::Row { children } => {
let child_context = LayoutContext {
inline_auto_width_intrinsic: true,
..context
};
let rendered = children let rendered = children
.iter() .iter()
.map(|child| render_node(child, context, intrinsic)) .map(|child| render_node(child, child_context, intrinsic))
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let height = rendered let height = rendered
.iter() .iter()
@ -5665,7 +5688,10 @@ fn render_node_with_override(
.map(Rendered::first_width) .map(Rendered::first_width)
.max() .max()
.unwrap_or(0); .unwrap_or(0);
let target = if intrinsic || !context.viewport_width_known { let target = if intrinsic
|| context.inline_auto_width_intrinsic
|| !context.viewport_width_known
{
maximum maximum
} else { } else {
context.viewport_width.max(0) context.viewport_width.max(0)
@ -5810,6 +5836,19 @@ mod tests {
} }
} }
fn auto_text_box(
region_id: i64,
content: MeasuredText,
surface_template_id: Option<u32>,
) -> LayoutNode {
let mut node = text_box(region_id, content, surface_template_id);
let LayoutNode::Box { width, .. } = &mut node else {
unreachable!();
};
*width = Size::Auto;
node
}
fn child_box( fn child_box(
region_id: i64, region_id: i64,
child: LayoutNode, child: LayoutNode,
@ -5899,6 +5938,7 @@ mod tests {
viewport_width: 80, viewport_width: 80,
viewport_width_known: true, viewport_width_known: true,
viewport_height: 24, viewport_height: 24,
inline_auto_width_intrinsic: false,
} }
} }
@ -5953,6 +5993,7 @@ mod tests {
viewport_width: 80, viewport_width: 80,
viewport_width_known: true, viewport_width_known: true,
viewport_height: MAX_LAYOUT_WORK_UNITS as i64 + 1, viewport_height: MAX_LAYOUT_WORK_UNITS as i64 + 1,
inline_auto_width_intrinsic: false,
}, },
&mut context_work, &mut context_work,
) )
@ -6279,6 +6320,7 @@ mod tests {
viewport_width: 320, viewport_width: 320,
viewport_width_known: true, viewport_width_known: true,
viewport_height: 10, viewport_height: 10,
inline_auto_width_intrinsic: false,
}; };
let fit_content = Size::FitContent { let fit_content = Size::FitContent {
limit: Some(Box::new(Size::Pixels { value: 180 })), limit: Some(Box::new(Size::Pixels { value: 180 })),
@ -6319,6 +6361,7 @@ mod tests {
viewport_width: 0, viewport_width: 0,
viewport_width_known: false, viewport_width_known: false,
viewport_height: 0, viewport_height: 0,
inline_auto_width_intrinsic: false,
}; };
assert_eq!( assert_eq!(
resolve_width( resolve_width(
@ -6347,6 +6390,7 @@ mod tests {
viewport_width: 298, viewport_width: 298,
viewport_width_known: true, viewport_width_known: true,
viewport_height: 20, viewport_height: 20,
inline_auto_width_intrinsic: false,
}, },
BoxSizing::BorderBox, BoxSizing::BorderBox,
0, 0,
@ -6365,11 +6409,13 @@ mod tests {
viewport_width: 0, viewport_width: 0,
viewport_width_known: true, viewport_width_known: true,
viewport_height: 10, viewport_height: 10,
inline_auto_width_intrinsic: false,
}; };
let unavailable = LayoutContext { let unavailable = LayoutContext {
viewport_width: 0, viewport_width: 0,
viewport_width_known: false, viewport_width_known: false,
viewport_height: 10, viewport_height: 10,
inline_auto_width_intrinsic: false,
}; };
assert_eq!(flex_horizontal_size(&Size::Auto, known_zero), Some(0)); assert_eq!(flex_horizontal_size(&Size::Auto, known_zero), Some(0));
@ -6874,6 +6920,50 @@ mod tests {
); );
} }
#[test]
fn row_auto_children_do_not_duplicate_column_viewport() {
let row = LayoutNode::Row {
children: vec![
auto_text_box(2, measured_text(vec![vec![cluster("Left", 4, None)]]), None),
auto_text_box(
3,
measured_text(vec![vec![cluster("Right", 5, None)]]),
None,
),
],
};
let mut root = child_box(
1,
LayoutNode::Column {
children: vec![
row,
auto_text_box(4, measured_text(vec![vec![cluster("Body", 4, None)]]), None),
],
},
None,
);
let LayoutNode::Box { width, .. } = &mut root else {
unreachable!();
};
*width = Size::Pixels { value: 120 };
let document = LayoutDocument {
version: LAYOUT_VERSION,
space_width: 1,
style_count: 0,
property_template_count: 0,
styles: Vec::new(),
root,
};
document.validate().unwrap();
let tape = document.layout_tape(test_context(), None).unwrap();
assert!(
tape.lines.iter().all(|line| line.width <= 120),
"row children duplicated the containing column width: {:?}",
tape.lines.iter().map(|line| line.width).collect::<Vec<_>>()
);
}
#[test] #[test]
fn fixed_height_scroll_layout_uses_nonzero_offset_window() { fn fixed_height_scroll_layout_uses_nonzero_offset_window() {
let document = fixed_scroll_column_document(7); let document = fixed_scroll_column_document(7);

View File

@ -995,6 +995,7 @@ fn checked_layout_context(
viewport_width, viewport_width,
viewport_width_known, viewport_width_known,
viewport_height, viewport_height,
inline_auto_width_intrinsic: false,
}; };
document.validate_context(context)?; document.validate_context(context)?;
Ok(context) Ok(context)

View File

@ -4246,6 +4246,16 @@
(equal (ebox--render-cache-value-signature value) (equal (ebox--render-cache-value-signature value)
value)))) value))))
(ert-deftest ebox-render-cache-signature-includes-inline-auto-width-context ()
"Row intrinsic sizing must not reuse a cross-axis stretch render."
(ebox-test--reset-runtime-state)
(let ((node (ebox-create :content "context-sensitive")))
(let ((normal (let ((ebox--inline-auto-width-intrinsic-p nil))
(ebox--render-cache-node-signature node)))
(intrinsic (let ((ebox--inline-auto-width-intrinsic-p t))
(ebox--render-cache-node-signature node))))
(should-not (equal normal intrinsic)))))
(ert-deftest ebox-render-cache-signature-includes-rendered-region-identity () (ert-deftest ebox-render-cache-signature-includes-rendered-region-identity ()
"Cached render strings should never cross distinct region identities." "Cached render strings should never cross distinct region identities."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)

View File

@ -237,6 +237,53 @@
(dolist (width widths) (dolist (width widths)
(should (<= width 180))))) (should (<= width 180)))))
(ert-deftest ebox-build-column-contains-auto-row-children ()
"A column wrapper must not let an auto-width row child duplicate its viewport."
(let* ((node (ebox-build
'(column :width (120)
(row
(box :content "Left" :border t)
(box :content "Right" :border t))
(box :content "Body" :border t))))
(widths (ebox-dsl-test--line-widths node)))
(should (cl-every (lambda (width) (<= width 120)) widths))))
(ert-deftest ebox-row-keeps-explicit-viewport-widths-intrinsic-context ()
"Row intrinsic sizing must not change explicit viewport-sized children."
(let* ((ebox-viewport-width 120)
(node (ebox-row
(ebox-create :content "Left" :width 'viewport)
(ebox-create :content "Right" :width 'stretch)))
(widths (ebox-dsl-test--line-widths node)))
(should (equal widths '(240)))))
(ert-deftest ebox-row-keeps-composite-column-intrinsic-width ()
"A composite column child must not consume the row viewport as auto width."
(let* ((ebox-viewport-width 120)
(node (ebox-row
(ebox-column
(ebox-create :content "Top")
(ebox-create :content "Bottom"))
(ebox-create :content "Tail")))
(widths (ebox-dsl-test--line-widths node)))
(should (cl-every (lambda (width) (<= width 120)) widths))))
(ert-deftest ebox-build-lazy-column-contains-auto-row-children ()
"Lazy column windows must apply the same intrinsic row sizing rule."
(let* ((ebox-viewport-width 120)
(ebox-viewport-height 4)
(node (ebox-build
'(box :width (viewport)
:height (viewport-height)
:overflow scroll
(column
(row
(box :content "Left" :border t)
(box :content "Right" :border t))
(box :content "Body" :border t)))))
(widths (ebox-dsl-test--line-widths node)))
(should (cl-every (lambda (width) (<= width 120)) widths))))
(ert-deftest ebox-build-wrapper-viewport-width-bounds-child-layout-viewport () (ert-deftest ebox-build-wrapper-viewport-width-bounds-child-layout-viewport ()
"A wrapper's `(viewport)' width should pass its content box to child layouts." "A wrapper's `(viewport)' width should pass its content box to child layouts."
(let ((cases (let ((cases

View File

@ -738,6 +738,17 @@
(should (= (nth 2 widths) (nth 0 widths))) (should (= (nth 2 widths) (nth 0 widths)))
(should (= (nth 3 widths) (nth 0 widths))))) (should (= (nth 3 widths) (nth 0 widths)))))
(ert-deftest ebox-flex-composite-auto-row-respects-assigned-width ()
"A composite auto-width row must not duplicate its Flex item viewport."
(let* ((layout
(ebox-flex :width '(120)
(ebox-row
(ebox-create :content "Left" :border t)
(ebox-create :content "Right" :border t))))
(widths (mapcar #'ebox--string-pixel-width
(ebox-string-lines (ebox-render layout)))))
(should (cl-every (lambda (width) (<= width 120)) widths))))
(ert-deftest ebox-flex-sized-persistent-hit-populates-pass-local-cache () (ert-deftest ebox-flex-sized-persistent-hit-populates-pass-local-cache ()
"Persistent sized renders should become reusable in the current pass." "Persistent sized renders should become reusable in the current pass."
(let* ((source (ebox-create :content "A" :width '(80))) (let* ((source (ebox-create :content "A" :width '(80)))

View File

@ -80,6 +80,18 @@
(widths (mapcar #'ebox--string-pixel-width lines))) (widths (mapcar #'ebox--string-pixel-width lines)))
(should (equal widths (make-list (length widths) 120))))) (should (equal widths (make-list (length widths) 120)))))
(ert-deftest ebox-grid-composite-auto-row-respects-track-width ()
"A composite auto-width row must not duplicate its Grid track viewport."
(let* ((node (ebox-grid
:width '(120)
:grid-template-columns '((120))
(ebox-row
(ebox-create :content "Left" :border t)
(ebox-create :content "Right" :border t))))
(widths (mapcar #'ebox--string-pixel-width
(ebox-string-lines (ebox-render node)))))
(should (equal widths '(120)))))
(ert-deftest ebox-grid-sizes-auto-row-after-assigned-width-wrap () (ert-deftest ebox-grid-sizes-auto-row-after-assigned-width-wrap ()
"Auto rows should use the height of width-constrained child content." "Auto rows should use the height of width-constrained child content."
(let* ((node (ebox-grid (let* ((node (ebox-grid