diff --git a/integration_tests/specs/css/css-flexbox/flex-grow-chat-layout-issue-520.ts b/integration_tests/specs/css/css-flexbox/flex-grow-chat-layout-issue-520.ts index 0e9151ac27..0f7b35decb 100644 --- a/integration_tests/specs/css/css-flexbox/flex-grow-chat-layout-issue-520.ts +++ b/integration_tests/specs/css/css-flexbox/flex-grow-chat-layout-issue-520.ts @@ -102,6 +102,17 @@ describe('flex-grow column chat layout (issue #520)', () => { await snapshot(); + const lastItem = list.querySelectorAll('.list-item')[13] as HTMLElement; + const listRect = list.getBoundingClientRect(); + const lastRect = lastItem.getBoundingClientRect(); + const itemStyle = getComputedStyle(lastItem); + const listStyle = getComputedStyle(list); + const bottomOverflowAtTop = lastRect.bottom - listRect.bottom; + const expectedScrollGap = + bottomOverflowAtTop + + parseFloat(itemStyle.marginBottom || '0') + + parseFloat(listStyle.paddingBottom || '0'); + // Programmatic assertions to ensure expected layout behavior const chatH = chat.offsetHeight; const headerH = header.offsetHeight; @@ -119,6 +130,9 @@ describe('flex-grow column chat layout (issue #520)', () => { expect(firstItem.offsetHeight).toBe(secondItem.offsetHeight); // Items should not be stretched to container height expect(firstItem.offsetHeight).toBeLessThan(listH); + // Centered overflow should not leave extra blank scroll range after the + // last item's margin box and the container's bottom padding. + expect(Math.round(list.scrollHeight - list.clientHeight)) + .toBe(Math.round(expectedScrollGap)); }); }); - diff --git a/integration_tests/specs/css/css-flexbox/relayout-align-to-stretch.ts b/integration_tests/specs/css/css-flexbox/relayout-align-to-stretch.ts index bc51d68a21..2d9c196764 100644 --- a/integration_tests/specs/css/css-flexbox/relayout-align-to-stretch.ts +++ b/integration_tests/specs/css/css-flexbox/relayout-align-to-stretch.ts @@ -16,7 +16,7 @@ describe('relayout-align', () => { class: 'flexbox align-items-flex-start', style: { display: 'flex', - '-webkit-align-items': 'flex-start', + 'align-items': 'flex-start', height: '100px', position: 'relative', 'box-sizing': 'border-box', @@ -37,7 +37,6 @@ describe('relayout-align', () => { 'data-offset-y': '0', class: 'align-self-auto', style: { - '-webkit-align-self': 'auto', 'align-self': 'auto', border: '5px solid green', width: '50px', @@ -49,7 +48,6 @@ describe('relayout-align', () => { 'data-offset-y': '0', class: 'align-self-flex-start', style: { - '-webkit-align-self': 'flex-start', 'align-self': 'flex-start', border: '5px solid green', width: '50px', @@ -61,7 +59,6 @@ describe('relayout-align', () => { 'data-offset-y': '90', class: 'align-self-flex-end', style: { - '-webkit-align-self': 'flex-end', 'align-self': 'flex-end', border: '5px solid green', width: '50px', @@ -73,7 +70,6 @@ describe('relayout-align', () => { 'data-offset-y': '45', class: 'align-self-center', style: { - '-webkit-align-self': 'center', 'align-self': 'center', border: '5px solid green', width: '50px', @@ -85,7 +81,6 @@ describe('relayout-align', () => { 'data-offset-y': '0', class: 'align-self-baseline', style: { - '-webkit-align-self': 'baseline', 'align-self': 'baseline', border: '5px solid green', width: '50px', @@ -97,7 +92,6 @@ describe('relayout-align', () => { 'data-offset-y': '0', class: 'align-self-stretch', style: { - '-webkit-align-self': 'stretch', 'align-self': 'stretch', border: '5px solid green', width: '50px', diff --git a/webf/lib/foundation.dart b/webf/lib/foundation.dart index 7fd2b36e82..ca4d64d65b 100644 --- a/webf/lib/foundation.dart +++ b/webf/lib/foundation.dart @@ -27,6 +27,7 @@ export 'src/foundation/logger.dart'; export 'src/foundation/form_data/form_data.dart'; export 'src/foundation/loading_state_registry.dart'; export 'src/foundation/debug_flags.dart'; +export 'src/foundation/perf_debug_identity.dart'; export 'src/foundation/positioned_layout_logging.dart'; export 'src/foundation/flex_layout_logging.dart'; export 'src/foundation/widget_logging.dart'; diff --git a/webf/lib/src/css/border.dart b/webf/lib/src/css/border.dart index 42a0c277a2..451fd12797 100644 --- a/webf/lib/src/css/border.dart +++ b/webf/lib/src/css/border.dart @@ -154,6 +154,7 @@ mixin CSSBorderMixin on RenderStyle { set borderTopWidth(CSSLengthValue? value) { if (value == _borderTopWidth) return; _borderTopWidth = value; + markNeedsIntrinsicMeasurement('borderWidth'); markNeedsLayout(); resetBoxDecoration(); } @@ -170,6 +171,7 @@ mixin CSSBorderMixin on RenderStyle { set borderRightWidth(CSSLengthValue? value) { if (value == _borderRightWidth) return; _borderRightWidth = value; + markNeedsIntrinsicMeasurement('borderWidth'); markNeedsLayout(); resetBoxDecoration(); } @@ -186,6 +188,7 @@ mixin CSSBorderMixin on RenderStyle { set borderBottomWidth(CSSLengthValue? value) { if (value == _borderBottomWidth) return; _borderBottomWidth = value; + markNeedsIntrinsicMeasurement('borderWidth'); markNeedsLayout(); resetBoxDecoration(); } @@ -202,6 +205,7 @@ mixin CSSBorderMixin on RenderStyle { set borderLeftWidth(CSSLengthValue? value) { if (value == _borderLeftWidth) return; _borderLeftWidth = value; + markNeedsIntrinsicMeasurement('borderWidth'); markNeedsLayout(); resetBoxDecoration(); } @@ -266,6 +270,8 @@ mixin CSSBorderMixin on RenderStyle { set borderTopStyle(CSSBorderStyleType? value) { if (value == _borderTopStyle) return; _borderTopStyle = value; + markNeedsIntrinsicMeasurement('borderStyle'); + markNeedsLayout(); markNeedsPaint(); resetBoxDecoration(); } @@ -277,6 +283,8 @@ mixin CSSBorderMixin on RenderStyle { set borderRightStyle(CSSBorderStyleType? value) { if (value == _borderRightStyle) return; _borderRightStyle = value; + markNeedsIntrinsicMeasurement('borderStyle'); + markNeedsLayout(); markNeedsPaint(); resetBoxDecoration(); } @@ -288,6 +296,8 @@ mixin CSSBorderMixin on RenderStyle { set borderBottomStyle(CSSBorderStyleType? value) { if (value == _borderBottomStyle) return; _borderBottomStyle = value; + markNeedsIntrinsicMeasurement('borderStyle'); + markNeedsLayout(); markNeedsPaint(); resetBoxDecoration(); } @@ -299,6 +309,8 @@ mixin CSSBorderMixin on RenderStyle { set borderLeftStyle(CSSBorderStyleType? value) { if (value == _borderLeftStyle) return; _borderLeftStyle = value; + markNeedsIntrinsicMeasurement('borderStyle'); + markNeedsLayout(); markNeedsPaint(); resetBoxDecoration(); } diff --git a/webf/lib/src/css/display.dart b/webf/lib/src/css/display.dart index 75678dbd74..32b3364430 100644 --- a/webf/lib/src/css/display.dart +++ b/webf/lib/src/css/display.dart @@ -33,6 +33,7 @@ mixin CSSDisplayMixin on RenderStyle { set display(CSSDisplay? value) { if (_display != value) { _display = value; + markNeedsIntrinsicMeasurement('display'); markNeedsLayout(); // CSS display affects accessibility visibility (e.g., display:none) attachedRenderBoxModel?.markNeedsSemanticsUpdate(); diff --git a/webf/lib/src/css/font_face.dart b/webf/lib/src/css/font_face.dart index 91a0501973..50e2826321 100644 --- a/webf/lib/src/css/font_face.dart +++ b/webf/lib/src/css/font_face.dart @@ -13,6 +13,7 @@ import 'package:flutter/rendering.dart'; import 'package:webf/css.dart'; import 'package:webf/foundation.dart'; import 'package:webf/launcher.dart'; +import 'package:webf/rendering.dart'; import 'dart:convert'; import 'package:webf/src/foundation/logger.dart'; @@ -72,6 +73,9 @@ class CSSFontFace { static void _markRenderSubtreeNeedsLayout(RenderObject root) { root.visitChildren(_markRenderSubtreeNeedsLayout); + if (root is RenderBoxModel) { + root.markNeedsIntrinsicMeasurementUpdate('fontFaceSubtree'); + } root.markNeedsLayout(); root.markNeedsPaint(); } @@ -357,6 +361,7 @@ class CSSFontFace { } finally { // Remove from loading map when done _loadingFonts.remove(descriptorKey); + renderStyle.markNeedsIntrinsicMeasurement('fontFaceLoad'); renderStyle.markNeedsLayout(); } } diff --git a/webf/lib/src/css/padding.dart b/webf/lib/src/css/padding.dart index aa6e773bf2..1fc57cf4bb 100644 --- a/webf/lib/src/css/padding.dart +++ b/webf/lib/src/css/padding.dart @@ -116,6 +116,7 @@ mixin CSSPaddingMixin on RenderStyle { CSSLengthValue get paddingTop => _normalizePaddingLength(_paddingTop) ?? CSSLengthValue.zero; void _markSelfAndParentNeedsLayout() { + markNeedsIntrinsicMeasurement('padding'); markNeedsLayout(); // Sizing may affect parent size, mark parent as needsLayout in case // renderBoxModel has tight constraints which will prevent parent from marking. diff --git a/webf/lib/src/css/render_style.dart b/webf/lib/src/css/render_style.dart index e35c595141..aa430b96b3 100644 --- a/webf/lib/src/css/render_style.dart +++ b/webf/lib/src/css/render_style.dart @@ -1284,6 +1284,14 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { widgetRenderBox?.clearIntersectionChangeListeners(); } + @pragma('vm:prefer-inline') + void markNeedsIntrinsicMeasurement([String reason = 'renderStyle']) { + everyAttachedWidgetRenderBox((element, renderObject) { + renderObject.markNeedsIntrinsicMeasurementUpdate(reason); + return true; + }); + } + @pragma('vm:prefer-inline') void markNeedsLayout() { everyAttachedWidgetRenderBox((element, renderObject) { diff --git a/webf/lib/src/css/sizing.dart b/webf/lib/src/css/sizing.dart index 027acbda32..c79462cd54 100644 --- a/webf/lib/src/css/sizing.dart +++ b/webf/lib/src/css/sizing.dart @@ -222,6 +222,7 @@ mixin CSSSizingMixin on RenderStyle { void _markSelfAndParentNeedsLayout() { if (!hasRenderBox()) return; + markNeedsIntrinsicMeasurement('sizing'); markNeedsLayout(); // Sizing may affect parent size, mark parent as needsLayout in case diff --git a/webf/lib/src/css/text.dart b/webf/lib/src/css/text.dart index 5a89b67ff9..f2a993e27c 100644 --- a/webf/lib/src/css/text.dart +++ b/webf/lib/src/css/text.dart @@ -265,6 +265,7 @@ mixin CSSTextMixin on RenderStyle { void updateFontRelativeLength() { if (_fontRelativeProperties.isEmpty) return; + markNeedsIntrinsicMeasurement('fontRelativeLength'); markNeedsLayout(); if (isSelfBoxModelSizeTight()) { markParentNeedsLayout(); @@ -278,6 +279,7 @@ mixin CSSTextMixin on RenderStyle { void updateRootFontRelativeLength() { if (_rootFontRelativeProperties.isEmpty) return; + markNeedsIntrinsicMeasurement('rootFontRelativeLength'); markNeedsLayout(); if (isSelfBoxModelSizeTight()) { markParentNeedsLayout(); @@ -597,6 +599,7 @@ mixin CSSTextMixin on RenderStyle { // text and layout (line-height, white-space) changes. void _markNestChildrenTextAndLayoutNeedsLayout(RenderStyle renderStyle, String styleProperty) { if (renderStyle.isSelfRenderLayoutBox()) { + renderStyle.markNeedsIntrinsicMeasurement('textLayout:$styleProperty'); renderStyle.markNeedsLayout(); visitor(RenderObject child) { @@ -620,6 +623,7 @@ mixin CSSTextMixin on RenderStyle { void _markTextNeedsLayout() { visitor(RenderObject child) { if (child is RenderTextBox) { + child.renderStyle.markNeedsIntrinsicMeasurement('textDirect'); child.renderStyle.markNeedsLayout(); } else { child.visitChildren(visitor); @@ -635,7 +639,9 @@ mixin CSSTextMixin on RenderStyle { void _markChildrenTextNeedsLayout(RenderStyle renderStyle, String styleProperty) { visitor(dom.Node child) { if (child is dom.TextNode) { - child.parentElement!.attachedRenderer?.markNeedsLayout(); + final RenderStyle parentStyle = child.parentElement!.renderStyle; + parentStyle.markNeedsIntrinsicMeasurement('textInherited:$styleProperty'); + parentStyle.markNeedsLayout(); } if (child is dom.Element && child.style[styleProperty].isEmpty) { diff --git a/webf/lib/src/foundation/debug_flags.dart b/webf/lib/src/foundation/debug_flags.dart index ffe8b814c5..7857ce2c15 100644 --- a/webf/lib/src/foundation/debug_flags.dart +++ b/webf/lib/src/foundation/debug_flags.dart @@ -117,6 +117,20 @@ class DebugFlags { static int cssGridProfilingMinMs = 2; // Removed: Use FlexLog filters to enable flex logs. + static bool enableFlexFastPathProfiling = + const bool.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH', defaultValue: false); + static int flexFastPathProfilingSummaryEvery = + const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_SUMMARY_EVERY', defaultValue: 50); + static int flexFastPathProfilingMaxDetailLogs = + const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_MAX_DETAIL_LOGS', defaultValue: 20); + static bool enableFlexAnonymousMetricsProfiling = + const bool.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS', defaultValue: false); + static int flexAnonymousMetricsProfilingSummaryEvery = + const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_SUMMARY_EVERY', defaultValue: 50); + static int flexAnonymousMetricsProfilingMaxDetailLogs = + const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_MAX_DETAIL_LOGS', defaultValue: 20); + static String flexAnonymousMetricsProfilingWatchedPathContains = + const String.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_WATCH_PATH', defaultValue: ''); /// Debug flag to enable inline layout visualization. /// When true, paints debug information for line boxes, margins, padding, etc. diff --git a/webf/lib/src/foundation/perf_debug_identity.dart b/webf/lib/src/foundation/perf_debug_identity.dart new file mode 100644 index 0000000000..129530ae05 --- /dev/null +++ b/webf/lib/src/foundation/perf_debug_identity.dart @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2024-present The OpenWebF Company. All rights reserved. + * Licensed under GNU GPL with Enterprise exception. + */ + +String perfDescribeElementNode(dynamic element, {int maxClasses = 2}) { + if (element == null) return 'unknown'; + + final StringBuffer buffer = StringBuffer(_perfReadTagName(element)); + final String? id = _perfReadStringProperty(element, 'id'); + if (id != null && id.isNotEmpty) { + buffer + ..write('#') + ..write(_perfSanitizeToken(id, maxLength: 32)); + } + + for (final String className + in _perfReadClasses(element, maxCount: maxClasses)) { + buffer + ..write('.') + ..write(className); + } + + return buffer.toString(); +} + +String perfDescribeElementPath( + dynamic element, { + int maxDepth = 4, + int maxClassesPerSegment = 1, +}) { + if (element == null) return 'unknown'; + + final List segments = []; + dynamic cursor = element; + bool truncated = false; + + while (cursor != null) { + if (segments.length >= maxDepth) { + truncated = true; + break; + } + segments + .add(perfDescribeElementNode(cursor, maxClasses: maxClassesPerSegment)); + cursor = _perfReadParentElement(cursor); + } + + final String path = segments.reversed.join('>'); + return truncated ? '...>$path' : path; +} + +String perfFormatMilliseconds(int microseconds, {int fractionDigits = 1}) { + return (microseconds / 1000.0).toStringAsFixed(fractionDigits); +} + +List _perfReadClasses(dynamic element, {required int maxCount}) { + final List classes = []; + if (maxCount <= 0) return classes; + + try { + final dynamic raw = element.classList; + if (raw is Iterable) { + for (final dynamic value in raw) { + if (value is! String || value.isEmpty) continue; + final String sanitized = _perfSanitizeToken(value, maxLength: 24); + if (sanitized.isEmpty || classes.contains(sanitized)) continue; + classes.add(sanitized); + if (classes.length >= maxCount) return classes; + } + } + } catch (_) {} + + try { + final dynamic raw = element.className; + if (raw is String && raw.isNotEmpty) { + for (final String value in raw.split(RegExp(r'\s+'))) { + if (value.isEmpty) continue; + final String sanitized = _perfSanitizeToken(value, maxLength: 24); + if (sanitized.isEmpty || classes.contains(sanitized)) continue; + classes.add(sanitized); + if (classes.length >= maxCount) return classes; + } + } + } catch (_) {} + + return classes; +} + +dynamic _perfReadParentElement(dynamic element) { + try { + return element.parentElement; + } catch (_) { + return null; + } +} + +String _perfReadTagName(dynamic element) { + final String? tagName = _perfReadStringProperty(element, 'tagName'); + if (tagName != null && tagName.isNotEmpty) { + return _perfSanitizeToken(tagName.toLowerCase(), maxLength: 20); + } + return _perfSanitizeToken(element.runtimeType.toString().toLowerCase(), + maxLength: 20); +} + +String? _perfReadStringProperty(dynamic element, String propertyName) { + try { + final dynamic value; + switch (propertyName) { + case 'id': + value = element.id; + break; + case 'tagName': + value = element.tagName; + break; + default: + return null; + } + if (value is String) { + return value; + } + } catch (_) {} + return null; +} + +String _perfSanitizeToken(String value, {required int maxLength}) { + final StringBuffer buffer = StringBuffer(); + for (int i = 0; i < value.length; i++) { + final int codeUnit = value.codeUnitAt(i); + final bool isDigit = codeUnit >= 48 && codeUnit <= 57; + final bool isUpper = codeUnit >= 65 && codeUnit <= 90; + final bool isLower = codeUnit >= 97 && codeUnit <= 122; + final bool isSafePunctuation = + codeUnit == 45 || codeUnit == 95 || codeUnit == 58; + if (isDigit || isUpper || isLower || isSafePunctuation) { + buffer.writeCharCode(codeUnit); + } else { + buffer.write('_'); + } + if (buffer.length >= maxLength) break; + } + + if (buffer.isEmpty) { + return 'x'; + } + return buffer.toString(); +} diff --git a/webf/lib/src/rendering/box_model.dart b/webf/lib/src/rendering/box_model.dart index 48c6373728..e2c483ecf1 100644 --- a/webf/lib/src/rendering/box_model.dart +++ b/webf/lib/src/rendering/box_model.dart @@ -28,6 +28,7 @@ List renderBoxInLayoutHashCodes = []; // tree instance in cases where the same DOM element is mounted into multiple // Flutter subtrees simultaneously (e.g. CupertinoContextMenu preview/modal). final List renderBoxModelInLayoutStack = []; +int renderBoxModelLayoutPassId = 0; class RenderLayoutParentData extends ContainerBoxParentData { // Row index of child when wrapping @@ -392,6 +393,17 @@ abstract class RenderBoxModel extends RenderBox // Whether it needs relayout due to percentage calculation. bool needsRelayout = false; + bool _hasPendingIntrinsicMeasurementInvalidation = true; + bool _hasPendingSubtreeIntrinsicMeasurementInvalidation = true; + String? _debugIntrinsicMeasurementDirtyReason = 'initial'; + int _clearIntrinsicMeasurementInvalidationAfterLayoutPass = 1; + + bool get hasPendingIntrinsicMeasurementInvalidation => + _hasPendingIntrinsicMeasurementInvalidation; + bool get hasPendingSubtreeIntrinsicMeasurementInvalidation => + _hasPendingSubtreeIntrinsicMeasurementInvalidation; + String? get debugIntrinsicMeasurementDirtyReason => + _debugIntrinsicMeasurementDirtyReason; // Mark parent as needs relayout used in cases such as // child has percentage length and parent's size can not be calculated by style @@ -409,13 +421,62 @@ abstract class RenderBoxModel extends RenderBox needsRelayout = true; } + void markNeedsIntrinsicMeasurementUpdate([String reason = 'unspecified']) { + _hasPendingIntrinsicMeasurementInvalidation = true; + _debugIntrinsicMeasurementDirtyReason = reason; + _clearIntrinsicMeasurementInvalidationAfterLayoutPass = + renderBoxModelLayoutPassId + 1; + _markNeedsSubtreeIntrinsicMeasurementUpdate(reason); + } + + void markNeedsSubtreeIntrinsicMeasurementUpdate( + [String reason = 'descendant']) { + _markNeedsSubtreeIntrinsicMeasurementUpdate(reason); + } + + void _markNeedsSubtreeIntrinsicMeasurementUpdate(String reason) { + if (_hasPendingSubtreeIntrinsicMeasurementInvalidation) { + return; + } + _hasPendingSubtreeIntrinsicMeasurementInvalidation = true; + RenderObject? ancestor = parent; + while (ancestor != null) { + if (ancestor is RenderBoxModel) { + if (ancestor._hasPendingSubtreeIntrinsicMeasurementInvalidation) { + break; + } + ancestor._hasPendingSubtreeIntrinsicMeasurementInvalidation = true; + } + ancestor = ancestor.parent; + } + } + + void updateIntrinsicMeasurementInvalidationForCurrentLayoutPass() { + if (_hasPendingIntrinsicMeasurementInvalidation && + renderBoxModelLayoutPassId > + _clearIntrinsicMeasurementInvalidationAfterLayoutPass) { + _hasPendingIntrinsicMeasurementInvalidation = false; + _debugIntrinsicMeasurementDirtyReason = null; + } + } + + void clearIntrinsicMeasurementInvalidationAfterMeasurement() { + _hasPendingIntrinsicMeasurementInvalidation = false; + _hasPendingSubtreeIntrinsicMeasurementInvalidation = false; + _debugIntrinsicMeasurementDirtyReason = null; + _clearIntrinsicMeasurementInvalidationAfterLayoutPass = + renderBoxModelLayoutPassId; + } + // A flag to detect the size of this renderBox had changed during this layout. bool isSelfSizeChanged = false; + bool _lastLaidOutAsRelayoutBoundary = false; /// Mark children needs layout when drop child as Flutter did /// @override void dropChild(RenderObject child) { + markNeedsIntrinsicMeasurementUpdate('dropChild'); super.dropChild(child); } @@ -423,6 +484,11 @@ abstract class RenderBoxModel extends RenderBox @override void layout(Constraints constraints, {bool parentUsesSize = false}) { + _lastLaidOutAsRelayoutBoundary = + !parentUsesSize || sizedByParent || constraints.isTight || parent == null; + if (renderBoxModelInLayoutStack.isEmpty) { + renderBoxModelLayoutPassId++; + } renderBoxInLayoutHashCodes.add(hashCode); renderBoxModelInLayoutStack.add(this); @@ -835,15 +901,25 @@ abstract class RenderBoxModel extends RenderBox // Box size equals to RenderBox.size to avoid flutter complain when read size property. Size? _boxSize; + RenderObject? _relayoutParentOnSizeChange; Size? get boxSize { - assert(_boxSize != null, 'box does not have laid out.'); return _boxSize; } + void setRelayoutParentOnSizeChange(RenderObject? parent) { + _relayoutParentOnSizeChange = parent; + } + + @protected + RenderObject? get relayoutParentOnSizeChange => _relayoutParentOnSizeChange; + + @protected + bool get lastLaidOutAsRelayoutBoundary => _lastLaidOutAsRelayoutBoundary; + @override set size(Size value) { - _boxSize = value; + _boxSize = Size.copy(value); Size? previousSize = hasSize ? super.size : null; if (previousSize != null && previousSize != value) { @@ -853,6 +929,21 @@ abstract class RenderBoxModel extends RenderBox super.size = value; } + @override + void markNeedsLayout() { + final RenderObject? relayoutParent = _relayoutParentOnSizeChange; + super.markNeedsLayout(); + + // Some wrapper parents mirror child.boxSize while laying the child out + // with parentUsesSize: false to keep a local relayout boundary. + if (relayoutParent != null && + lastLaidOutAsRelayoutBoundary && + identical(parent, relayoutParent) && + relayoutParent.attached) { + relayoutParent.markNeedsLayout(); + } + } + Size getBoxSize(Size contentSize) { _contentSize = contentConstraints!.constrain(contentSize); Size paddingBoxSize = renderStyle.wrapPaddingSize(_contentSize!); @@ -983,6 +1074,7 @@ abstract class RenderBoxModel extends RenderBox this.contentConstraints = contentConstraints; clearOverflowLayout(); isSelfSizeChanged = false; + updateIntrinsicMeasurementInvalidationForCurrentLayoutPass(); // Reset cached CSS baselines before a new layout pass. They will be // updated by subclasses that can establish inline formatting context diff --git a/webf/lib/src/rendering/event_listener.dart b/webf/lib/src/rendering/event_listener.dart index 60ebe760a7..9e5405f254 100644 --- a/webf/lib/src/rendering/event_listener.dart +++ b/webf/lib/src/rendering/event_listener.dart @@ -58,7 +58,15 @@ class RenderEventListener extends RenderBoxModel @override void markNeedsLayout() { super.markNeedsLayout(); - parent?.markNeedsLayout(); + + // Most of the engine still expects RenderEventListener to bubble layout + // dirtiness to its parent. Boundary-sensitive callers opt out by + // registering an explicit relayout parent via RenderBoxModel. + if (relayoutParentOnSizeChange == null && + lastLaidOutAsRelayoutBoundary && + parent != null) { + parent!.markNeedsLayout(); + } } @override @@ -161,6 +169,7 @@ class RenderEventListener extends RenderBoxModel @override void performLayout() { + updateIntrinsicMeasurementInvalidationForCurrentLayoutPass(); size = (child?..layout(constraints, parentUsesSize: true))?.size ?? computeSizeForNoChild(constraints); diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index eccf74326c..16c505a100 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -15,9 +15,416 @@ import 'package:webf/dom.dart'; import 'package:webf/foundation.dart'; import 'package:webf/rendering.dart'; import 'package:webf/css.dart'; +import 'package:webf/src/html/forms.dart' show ButtonElement; +import 'package:webf/src/html/semantics_text.dart' show SpanElement; import 'package:webf/src/html/text.dart'; import 'package:webf/widget.dart'; +enum _FlexFastPathRejectReason { + verticalDirection, + wrappedContainer, + positionedPlaceholderChild, + containerAlignItemsBaseline, + containerAlignItemsStretch, + childAlignSelfBaseline, + childAlignSelfStretch, + childNonTightWidth, + wouldGrow, + wouldShrink, +} + +String _flexFastPathRejectReasonLabel(_FlexFastPathRejectReason reason) { + switch (reason) { + case _FlexFastPathRejectReason.verticalDirection: + return 'verticalDirection'; + case _FlexFastPathRejectReason.wrappedContainer: + return 'wrappedContainer'; + case _FlexFastPathRejectReason.positionedPlaceholderChild: + return 'positionedPlaceholderChild'; + case _FlexFastPathRejectReason.containerAlignItemsBaseline: + return 'containerAlignItemsBaseline'; + case _FlexFastPathRejectReason.containerAlignItemsStretch: + return 'containerAlignItemsStretch'; + case _FlexFastPathRejectReason.childAlignSelfBaseline: + return 'childAlignSelfBaseline'; + case _FlexFastPathRejectReason.childAlignSelfStretch: + return 'childAlignSelfStretch'; + case _FlexFastPathRejectReason.childNonTightWidth: + return 'childNonTightWidth'; + case _FlexFastPathRejectReason.wouldGrow: + return 'wouldGrow'; + case _FlexFastPathRejectReason.wouldShrink: + return 'wouldShrink'; + } +} + +enum _FlexAnonymousMetricsRejectReason { + verticalDirection, + wrappedContainer, + positionedPlaceholderChild, + containerAlignItemsBaseline, + containerAlignItemsStretch, + childAlignSelfBaseline, + childAlignSelfStretch, + childNotFlexNone, + noMetricsOnlyFlowChild, + mixedMetricsOnlyChildren, +} + +String _flexAnonymousMetricsRejectReasonLabel( + _FlexAnonymousMetricsRejectReason reason) { + switch (reason) { + case _FlexAnonymousMetricsRejectReason.verticalDirection: + return 'verticalDirection'; + case _FlexAnonymousMetricsRejectReason.wrappedContainer: + return 'wrappedContainer'; + case _FlexAnonymousMetricsRejectReason.positionedPlaceholderChild: + return 'positionedPlaceholderChild'; + case _FlexAnonymousMetricsRejectReason.containerAlignItemsBaseline: + return 'containerAlignItemsBaseline'; + case _FlexAnonymousMetricsRejectReason.containerAlignItemsStretch: + return 'containerAlignItemsStretch'; + case _FlexAnonymousMetricsRejectReason.childAlignSelfBaseline: + return 'childAlignSelfBaseline'; + case _FlexAnonymousMetricsRejectReason.childAlignSelfStretch: + return 'childAlignSelfStretch'; + case _FlexAnonymousMetricsRejectReason.childNotFlexNone: + return 'childNotFlexNone'; + case _FlexAnonymousMetricsRejectReason.noMetricsOnlyFlowChild: + return 'noMetricsOnlyFlowChild'; + case _FlexAnonymousMetricsRejectReason.mixedMetricsOnlyChildren: + return 'mixedMetricsOnlyChildren'; + } +} + +enum _FlexAnonymousMetricsMissReason { + flowNeedsRelayout, + childNeedsRelayout, + subtreeIntrinsicDirty, + missingCacheEntry, + constraintsMismatch, +} + +String _flexAnonymousMetricsMissReasonLabel( + _FlexAnonymousMetricsMissReason reason) { + switch (reason) { + case _FlexAnonymousMetricsMissReason.flowNeedsRelayout: + return 'flowNeedsRelayout'; + case _FlexAnonymousMetricsMissReason.childNeedsRelayout: + return 'childNeedsRelayout'; + case _FlexAnonymousMetricsMissReason.subtreeIntrinsicDirty: + return 'subtreeIntrinsicDirty'; + case _FlexAnonymousMetricsMissReason.missingCacheEntry: + return 'missingCacheEntry'; + case _FlexAnonymousMetricsMissReason.constraintsMismatch: + return 'constraintsMismatch'; + } +} + +typedef _FlexFastPathRejectCallback = void Function( + _FlexFastPathRejectReason reason, { + Map? details, +}); + +class _FlexFastPathProfiler { + static int _attempts = 0; + static int _hits = 0; + static int _detailLogs = 0; + static final Map<_FlexFastPathRejectReason, int> _rejectCounts = + <_FlexFastPathRejectReason, int>{}; + + static bool get enabled => DebugFlags.enableFlexFastPathProfiling; + + static int get _summaryEvery { + final int configured = DebugFlags.flexFastPathProfilingSummaryEvery; + return configured > 0 ? configured : 50; + } + + static int get _maxDetailLogs { + final int configured = DebugFlags.flexFastPathProfilingMaxDetailLogs; + return configured >= 0 ? configured : 0; + } + + static void recordHit(String path, {required int childCount}) { + if (!enabled) return; + _attempts++; + _hits++; + _maybeLogSummary(); + } + + static void recordReject( + String path, + _FlexFastPathRejectReason reason, { + String? childLabel, + int? childIndex, + BoxConstraints? childConstraints, + Map? details, + }) { + if (!enabled) return; + _attempts++; + _rejectCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); + + if (_detailLogs < _maxDetailLogs) { + final StringBuffer message = StringBuffer() + ..write('[FlexFastPath][reject] path=') + ..write(path) + ..write(' reason=') + ..write(_flexFastPathRejectReasonLabel(reason)); + if (childIndex != null) { + message + ..write(' childIndex=') + ..write(childIndex); + } + if (childLabel != null) { + message + ..write(' child=') + ..write(childLabel); + } + if (childConstraints != null) { + message + ..write(' constraints=') + ..write(childConstraints); + } + if (details != null && details.isNotEmpty) { + message + ..write(' details=') + ..write(_formatDetails(details)); + } + renderingLogger.info(message.toString()); + _detailLogs++; + } + + _maybeLogSummary(); + } + + static void _maybeLogSummary() { + if (!enabled) return; + if (_attempts == 0 || _attempts % _summaryEvery != 0) return; + + final int rejects = _attempts - _hits; + final double hitRate = _attempts == 0 ? 0.0 : (_hits / _attempts) * 100.0; + final List> rejectEntries = + _rejectCounts.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + final String rejectSummary = rejectEntries.isEmpty + ? 'none' + : rejectEntries + .map((MapEntry<_FlexFastPathRejectReason, int> entry) => + '${_flexFastPathRejectReasonLabel(entry.key)}=${entry.value}') + .join(', '); + + renderingLogger.info( + '[FlexFastPath][summary] attempts=$_attempts hits=$_hits ' + 'hitRate=${hitRate.toStringAsFixed(1)}% rejects=$rejects reasons=$rejectSummary', + ); + } + + static String _formatDetails(Map details) { + return details.entries + .map((MapEntry entry) => '${entry.key}=${entry.value}') + .join(', '); + } +} + +class _FlexAnonymousMetricsProfiler { + static int _rowsEvaluated = 0; + static int _rowsEligible = 0; + static int _childCacheHits = 0; + static int _childCacheMisses = 0; + static int _detailLogs = 0; + static int _lastSummaryRows = -1; + static final Map<_FlexAnonymousMetricsRejectReason, int> _rejectCounts = + <_FlexAnonymousMetricsRejectReason, int>{}; + static final Map<_FlexAnonymousMetricsMissReason, int> _missCounts = + <_FlexAnonymousMetricsMissReason, int>{}; + + static bool get enabled => DebugFlags.enableFlexAnonymousMetricsProfiling; + + static int get _summaryEvery { + final int configured = DebugFlags.flexAnonymousMetricsProfilingSummaryEvery; + return configured > 0 ? configured : 50; + } + + static int get _maxDetailLogs { + final int configured = DebugFlags.flexAnonymousMetricsProfilingMaxDetailLogs; + return configured >= 0 ? configured : 0; + } + + static String get _watchedPathSubstring => + DebugFlags.flexAnonymousMetricsProfilingWatchedPathContains.trim(); + + static bool _shouldForceDetail(String path) { + final String watched = _watchedPathSubstring; + return watched.isNotEmpty && path.contains(watched); + } + + static void recordEligibleRow( + String path, { + required int childCount, + required int candidateChildCount, + }) { + if (!enabled) return; + _rowsEvaluated++; + _rowsEligible++; + + _maybeLogDetail( + path, + '[FlexAnonymousMetrics][eligible] path=$path childCount=$childCount ' + 'candidateChildCount=$candidateChildCount', + ); + _maybeLogSummary(); + } + + static void recordRejectedRow( + String path, + _FlexAnonymousMetricsRejectReason reason, { + String? childLabel, + int? childIndex, + Map? details, + }) { + if (!enabled) return; + _rowsEvaluated++; + _rejectCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); + + final StringBuffer message = StringBuffer() + ..write('[FlexAnonymousMetrics][reject] path=') + ..write(path) + ..write(' reason=') + ..write(_flexAnonymousMetricsRejectReasonLabel(reason)); + if (childIndex != null) { + message + ..write(' childIndex=') + ..write(childIndex); + } + if (childLabel != null) { + message + ..write(' child=') + ..write(childLabel); + } + if (details != null && details.isNotEmpty) { + message + ..write(' details=') + ..write(_formatDetails(details)); + } + _maybeLogDetail(path, message.toString()); + _maybeLogSummary(); + } + + static void recordChildCacheHit( + String path, { + required String childLabel, + required int childIndex, + }) { + if (!enabled) return; + _childCacheHits++; + _maybeLogDetail( + path, + '[FlexAnonymousMetrics][cacheHit] path=$path childIndex=$childIndex child=$childLabel', + ); + _maybeLogSummary(); + } + + static void recordChildCacheMiss( + String path, { + required String childLabel, + required int childIndex, + required _FlexAnonymousMetricsMissReason reason, + Map? details, + }) { + if (!enabled) return; + _childCacheMisses++; + _missCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); + _maybeLogDetail( + path, + '[FlexAnonymousMetrics][cacheMiss] path=$path childIndex=$childIndex child=$childLabel ' + 'reason=${_flexAnonymousMetricsMissReasonLabel(reason)}' + '${details != null && details.isNotEmpty ? ' details=${_formatDetails(details)}' : ''}', + ); + _maybeLogSummary(); + } + + static void _maybeLogDetail(String path, String message) { + final bool forceDetail = _shouldForceDetail(path); + if (!forceDetail) { + if (_detailLogs >= _maxDetailLogs) return; + _detailLogs++; + } + renderingLogger.info(message); + } + + static void _maybeLogSummary() { + if (!enabled) return; + if (_rowsEvaluated == 0 || _rowsEvaluated % _summaryEvery != 0) return; + if (_lastSummaryRows == _rowsEvaluated) return; + _lastSummaryRows = _rowsEvaluated; + + final int rowsRejected = _rowsEvaluated - _rowsEligible; + final double rowHitRate = + _rowsEvaluated == 0 ? 0.0 : (_rowsEligible / _rowsEvaluated) * 100.0; + final int childAttempts = _childCacheHits + _childCacheMisses; + final double childHitRate = + childAttempts == 0 ? 0.0 : (_childCacheHits / childAttempts) * 100.0; + final List> rejectEntries = + _rejectCounts.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + final String rejectSummary = rejectEntries.isEmpty + ? 'none' + : rejectEntries + .map((MapEntry<_FlexAnonymousMetricsRejectReason, int> entry) => + '${_flexAnonymousMetricsRejectReasonLabel(entry.key)}=${entry.value}') + .join(', '); + final List> missEntries = + _missCounts.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + final String missSummary = missEntries.isEmpty + ? 'none' + : missEntries + .map((MapEntry<_FlexAnonymousMetricsMissReason, int> entry) => + '${_flexAnonymousMetricsMissReasonLabel(entry.key)}=${entry.value}') + .join(', '); + + renderingLogger.info( + '[FlexAnonymousMetrics][summary] rows=$_rowsEvaluated eligible=$_rowsEligible ' + 'eligibleRate=${rowHitRate.toStringAsFixed(1)}% rejected=$rowsRejected ' + 'childCacheHits=$_childCacheHits childCacheMisses=$_childCacheMisses ' + 'childHitRate=${childHitRate.toStringAsFixed(1)}% reasons=$rejectSummary ' + 'misses=$missSummary', + ); + } + + static String _formatDetails(Map details) { + return details.entries + .map((MapEntry entry) => '${entry.key}=${entry.value}') + .join(', '); + } +} + +class _FlexIntrinsicMeasurementCacheEntry { + const _FlexIntrinsicMeasurementCacheEntry({ + required this.constraints, + required this.size, + required this.intrinsicMainSize, + }); + + final BoxConstraints constraints; + final Size size; + final double intrinsicMainSize; +} + +class _FlexIntrinsicMeasurementLookupResult { + const _FlexIntrinsicMeasurementLookupResult({ + this.entry, + this.missReason, + this.missDetails, + }); + + final _FlexIntrinsicMeasurementCacheEntry? entry; + final _FlexAnonymousMetricsMissReason? missReason; + final Map? missDetails; +} + // Position and size info of each run (flex line) in flex layout. // https://www.w3.org/TR/css-flexbox-1/#flex-lines class _RunMetrics { @@ -204,6 +611,20 @@ class _FlexFactorTotals { double flexShrink; } +class _FlexResolutionInputs { + const _FlexResolutionInputs({ + required this.contentBoxLogicalWidth, + required this.contentBoxLogicalHeight, + required this.maxMainSize, + required this.isMainSizeDefinite, + }); + + final double? contentBoxLogicalWidth; + final double? contentBoxLogicalHeight; + final double? maxMainSize; + final bool isMainSizeDefinite; +} + class _FlexContainerInvariants { const _FlexContainerInvariants({ required this.isHorizontalFlexDirection, @@ -518,6 +939,12 @@ class RenderFlexLayout extends RenderLayoutBox { // Cache original constraints of children on the first layout. Expando _childrenOldConstraints = Expando('childrenOldConstraints'); + Expando<_FlexIntrinsicMeasurementCacheEntry> _childrenIntrinsicMeasureCache = + Expando<_FlexIntrinsicMeasurementCacheEntry>('childrenIntrinsicMeasureCache'); + Expando _childrenRequirePostMeasureLayout = + Expando('childrenRequirePostMeasureLayout'); + Expando? _transientChildSizeOverrides; + Expando? _metricsOnlyIntrinsicMeasureChildEligibilityCache; _FlexContainerInvariants? _layoutInvariants; @@ -529,6 +956,12 @@ class RenderFlexLayout extends RenderLayoutBox { _flexLineBoxMetrics.clear(); _childrenIntrinsicMainSizes = Expando('childrenIntrinsicMainSizes'); _childrenOldConstraints = Expando('childrenOldConstraints'); + _childrenIntrinsicMeasureCache = + Expando<_FlexIntrinsicMeasurementCacheEntry>('childrenIntrinsicMeasureCache'); + _childrenRequirePostMeasureLayout = + Expando('childrenRequirePostMeasureLayout'); + _transientChildSizeOverrides = null; + _metricsOnlyIntrinsicMeasureChildEligibilityCache = null; } @override @@ -1516,75 +1949,856 @@ class RenderFlexLayout extends RenderLayoutBox { } } - double _getMainSize(RenderBox child, {bool shouldUseIntrinsicMainSize = false}) { - Size? childSize = _getChildSize(child, shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); - if (_isHorizontalFlexDirection) { - return childSize!.width; - } else { - return childSize!.height; - } - } + double _getMainSize(RenderBox child, {bool shouldUseIntrinsicMainSize = false}) { + Size? childSize = _getChildSize(child, shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); + if (_isHorizontalFlexDirection) { + return childSize!.width; + } else { + return childSize!.height; + } + } + + // Get gap spacing for main axis (between flex items) + double _getMainAxisGap() { + final _FlexContainerInvariants? inv = _layoutInvariants; + if (inv != null) return inv.mainAxisGap; + CSSLengthValue gap = _isHorizontalFlexDirection + ? renderStyle.columnGap + : renderStyle.rowGap; + if (gap.type == CSSLengthType.NORMAL) return 0; + return gap.computedValue; + } + + // Get gap spacing for cross axis (between flex lines) + double _getCrossAxisGap() { + final _FlexContainerInvariants? inv = _layoutInvariants; + if (inv != null) return inv.crossAxisGap; + CSSLengthValue gap = _isHorizontalFlexDirection + ? renderStyle.rowGap + : renderStyle.columnGap; + if (gap.type == CSSLengthType.NORMAL) return 0; + return gap.computedValue; + } + + // Sort flex items by their order property (default order is 0), stably. + // When multiple items have the same order, preserve their original DOM order. + List _getSortedFlexItems(List children) { + if (children.length < 2) return children; + + int getOrder(RenderBox box) { + if (box is RenderBoxModel) return box.renderStyle.order; + if (box is RenderEventListener) { + final RenderBox? inner = box.child; + if (inner is RenderBoxModel) return inner.renderStyle.order; + } + return 0; + } + + // Fast path: avoid sorting/allocation when all orders are 0, or already sorted. + bool anyNonZero = false; + bool alreadySorted = true; + int prevOrder = getOrder(children[0]); + anyNonZero = prevOrder != 0; + for (int i = 1; i < children.length; i++) { + final int order = getOrder(children[i]); + anyNonZero = anyNonZero || order != 0; + if (order < prevOrder) alreadySorted = false; + prevOrder = order; + } + if (!anyNonZero || alreadySorted) return children; + + // Stable sort by (order, originalIndex). + final List<_OrderedFlexItem> items = List<_OrderedFlexItem>.generate( + children.length, + (int i) => _OrderedFlexItem(children[i], getOrder(children[i]), i), + growable: false, + ); + items.sort((_OrderedFlexItem a, _OrderedFlexItem b) { + final int byOrder = a.order.compareTo(b.order); + return byOrder != 0 ? byOrder : a.originalIndex.compareTo(b.originalIndex); + }); + return List.generate(items.length, (int i) => items[i].child, growable: false); + } + + _FlexResolutionInputs _computeFlexResolutionInputs() { + final bool isHorizontal = _isHorizontalFlexDirection; + final double? contentBoxLogicalWidth = renderStyle.contentBoxLogicalWidth; + final double? contentBoxLogicalHeight = renderStyle.contentBoxLogicalHeight; + + double? containerWidth; + if (contentBoxLogicalWidth != null) { + containerWidth = contentBoxLogicalWidth; + } else if (contentConstraints!.hasTightWidth) { + containerWidth = contentConstraints!.maxWidth; + } + + double? containerHeight; + if (containerWidth == null) { + if ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) { + containerWidth = contentConstraints!.maxWidth; + } else if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { + containerWidth = constraints.maxWidth; + } + } + if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { + containerWidth = + (containerWidth == null) ? constraints.maxWidth : math.min(containerWidth, constraints.maxWidth); + } + if ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) { + containerHeight = contentConstraints!.maxHeight; + } else if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { + containerHeight = constraints.maxHeight; + } + if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { + containerHeight = + (containerHeight == null) ? constraints.maxHeight : math.min(containerHeight, constraints.maxHeight); + } + if (contentBoxLogicalHeight != null) { + containerHeight = contentBoxLogicalHeight; + } else if (contentConstraints!.hasTightHeight) { + containerHeight = contentConstraints!.maxHeight; + } + + final double? maxMainSize = isHorizontal ? containerWidth : containerHeight; + final bool isMainSizeDefinite = isHorizontal + ? (contentBoxLogicalWidth != null || (contentConstraints?.hasTightWidth ?? false) || + constraints.hasTightWidth || + ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) || + (constraints.hasBoundedWidth && constraints.maxWidth.isFinite)) + : (contentBoxLogicalHeight != null || (contentConstraints?.hasTightHeight ?? false) || + constraints.hasTightHeight || + ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) || + (constraints.hasBoundedHeight && constraints.maxHeight.isFinite)); + + return _FlexResolutionInputs( + contentBoxLogicalWidth: contentBoxLogicalWidth, + contentBoxLogicalHeight: contentBoxLogicalHeight, + maxMainSize: maxMainSize, + isMainSizeDefinite: isMainSizeDefinite, + ); + } + + _RunChild _createRunChildMetadata(RenderBox child, double originalMainSize, + {required RenderBoxModel? effectiveChild, required double? usedFlexBasis}) { + double mainAxisMargin = 0.0; + if (effectiveChild != null) { + final RenderStyle s = effectiveChild.renderStyle; + final double marginHorizontal = s.marginLeft.computedValue + s.marginRight.computedValue; + final double marginVertical = s.marginTop.computedValue + s.marginBottom.computedValue; + mainAxisMargin = _isHorizontalFlexDirection ? marginHorizontal : marginVertical; + } + + final RenderBoxModel? marginBoxModel = + child is RenderBoxModel ? child : (child is RenderPositionPlaceholder ? child.positioned : null); + final RenderStyle? marginStyle = marginBoxModel?.renderStyle; + final bool marginLeftAuto = marginStyle?.marginLeft.isAuto ?? false; + final bool marginRightAuto = marginStyle?.marginRight.isAuto ?? false; + final bool marginTopAuto = marginStyle?.marginTop.isAuto ?? false; + final bool marginBottomAuto = marginStyle?.marginBottom.isAuto ?? false; + final bool hasAutoMainAxisMargin = _isHorizontalFlexDirection + ? (marginLeftAuto || marginRightAuto) + : (marginTopAuto || marginBottomAuto); + final bool hasAutoCrossAxisMargin = _isHorizontalFlexDirection + ? (marginTopAuto || marginBottomAuto) + : (marginLeftAuto || marginRightAuto); + + final double flexGrow = _getFlexGrow(child); + final double flexShrink = _getFlexShrink(child); + + return _RunChild( + child, + originalMainSize, + 0, + false, + effectiveChild: effectiveChild, + alignSelf: _getAlignSelf(child), + flexGrow: flexGrow, + flexShrink: flexShrink, + usedFlexBasis: usedFlexBasis, + mainAxisMargin: mainAxisMargin, + mainAxisStartMargin: _flowAwareChildMainAxisMargin(child) ?? 0.0, + mainAxisEndMargin: _flowAwareChildMainAxisMargin(child, isEnd: true) ?? 0.0, + crossAxisStartMargin: _flowAwareChildCrossAxisMargin(child) ?? 0.0, + crossAxisEndMargin: _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0.0, + hasAutoMainAxisMargin: hasAutoMainAxisMargin, + hasAutoCrossAxisMargin: hasAutoCrossAxisMargin, + marginLeftAuto: marginLeftAuto, + marginRightAuto: marginRightAuto, + marginTopAuto: marginTopAuto, + marginBottomAuto: marginBottomAuto, + isReplaced: effectiveChild?.renderStyle.isSelfRenderReplaced() ?? false, + aspectRatio: effectiveChild?.renderStyle.aspectRatio, + ); + } + + void _cacheOriginalConstraintsIfNeeded(RenderBox child, BoxConstraints appliedConstraints) { + RenderBoxModel? box = child is RenderBoxModel + ? child + : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + if (box == null) return; + + bool hasPercentageMaxWidth = box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxHeight = box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; + + if (hasPercentageMaxWidth || hasPercentageMaxHeight) { + _childrenOldConstraints[box] = appliedConstraints; + } + } + + RenderFlowLayout? _getCacheableIntrinsicMeasureFlowChild( + RenderBox child, { + bool allowAnonymous = false, + }) { + if (child is RenderFlowLayout) { + if (child.renderStyle.isSelfAnonymousFlowLayout()) { + if (!allowAnonymous || !_canReuseAnonymousFlowMeasurement(child)) { + return null; + } + } + return child; + } + if (child is RenderEventListener) { + final RenderBox? wrapped = child.child; + if (wrapped is RenderFlowLayout) { + if (wrapped.renderStyle.isSelfAnonymousFlowLayout() && + (!allowAnonymous || !_canReuseAnonymousFlowMeasurement(wrapped))) { + return null; + } + return wrapped; + } + } + return null; + } + + bool _canReuseAnonymousFlowMeasurement(RenderFlowLayout flowChild) { + Element? parentElement = flowChild.renderStyle.target.parentElement; + // Button-owned anonymous wrappers still regress :hover/:active snapshots + // when their intrinsic measurement is reused across flex passes. + while (parentElement != null) { + if (parentElement is ButtonElement) { + return false; + } + parentElement = parentElement.parentElement; + } + return true; + } + + bool _canUseAnonymousMetricsOnlyCache(List children) { + int metricsOnlyChildCount = 0; + for (int childIndex = 0; childIndex < children.length; childIndex++) { + final RenderBox child = children[childIndex]; + if (_isMetricsOnlyIntrinsicMeasureChild(child)) { + metricsOnlyChildCount++; + } + + final _FlexAnonymousMetricsRejectReason? rejectReason = + _getAnonymousMetricsRejectReason(child); + if (rejectReason != null) { + _recordAnonymousMetricsReject( + rejectReason, + child: child, + childIndex: childIndex, + ); + return false; + } + } + + if (metricsOnlyChildCount == 0) { + _recordAnonymousMetricsReject( + _FlexAnonymousMetricsRejectReason.noMetricsOnlyFlowChild, + ); + return false; + } + if (metricsOnlyChildCount != children.length) { + _recordAnonymousMetricsReject( + _FlexAnonymousMetricsRejectReason.mixedMetricsOnlyChildren, + details: { + 'childCount': children.length, + 'candidateChildCount': metricsOnlyChildCount, + }, + ); + return false; + } + + _recordAnonymousMetricsEligible( + childCount: children.length, + candidateChildCount: metricsOnlyChildCount, + ); + return true; + } + + bool _hasBaselineAlignmentForChild(RenderBox child) { + if (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline) { + return true; + } + final AlignSelf alignSelf = _getAlignSelf(child); + return alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline; + } + + bool _subtreeHasPendingIntrinsicMeasureInvalidation(RenderBox root) { + if (root is RenderTextBox && root.hasPendingTextLayoutUpdate) { + return true; + } + if (root is RenderBoxModel && + root.hasPendingSubtreeIntrinsicMeasurementInvalidation) { + return true; + } + + if (root is ContainerRenderObjectMixin>) { + RenderBox? child = (root as dynamic).firstChild as RenderBox?; + while (child != null) { + if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { + return true; + } + child = (root as dynamic).childAfter(child) as RenderBox?; + } + return false; + } + + if (root is RenderObjectWithChildMixin) { + final RenderBox? child = (root as dynamic).child as RenderBox?; + if (child != null) { + return _subtreeHasPendingIntrinsicMeasureInvalidation(child); + } + } + + return false; + } + + bool _hasWrappingFlexAncestor() { + RenderObject? ancestor = parent; + while (ancestor != null) { + if (ancestor is RenderFlexLayout) { + final FlexWrap wrap = ancestor.renderStyle.flexWrap; + if (wrap == FlexWrap.wrap || wrap == FlexWrap.wrapReverse) { + return true; + } + } + ancestor = ancestor.parent; + } + return false; + } + + Map? _describeFirstPendingIntrinsicMeasureInvalidation( + RenderBox root) { + if (root is RenderTextBox && root.hasPendingTextLayoutUpdate) { + return { + 'dirtyNode': 'RenderTextBox', + 'dirtyType': root.runtimeType, + 'dirtyReason': 'pendingTextLayoutUpdate', + }; + } + if (root is RenderBoxModel && + root.hasPendingIntrinsicMeasurementInvalidation) { + return { + 'dirtyNode': _describeFastPathChild(root), + 'dirtyType': root.runtimeType, + 'dirtyReason': root.debugIntrinsicMeasurementDirtyReason ?? 'unknown', + }; + } + + if (root is ContainerRenderObjectMixin>) { + RenderBox? child = (root as dynamic).firstChild as RenderBox?; + while (child != null) { + final Map? details = + _describeFirstPendingIntrinsicMeasureInvalidation(child); + if (details != null) return details; + child = (root as dynamic).childAfter(child) as RenderBox?; + } + return null; + } + + if (root is RenderObjectWithChildMixin) { + final RenderBox? child = (root as dynamic).child as RenderBox?; + if (child != null) { + return _describeFirstPendingIntrinsicMeasureInvalidation(child); + } + } + + return null; + } + + void _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement( + RenderBox root) { + if (root is RenderTextBox && root.hasPendingTextLayoutUpdate) { + root.clearPendingTextLayoutUpdateAfterMeasurement(); + } + if (root is RenderBoxModel) { + root.clearIntrinsicMeasurementInvalidationAfterMeasurement(); + } + + if (root + is ContainerRenderObjectMixin>) { + RenderBox? child = (root as dynamic).firstChild as RenderBox?; + while (child != null) { + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); + child = (root as dynamic).childAfter(child) as RenderBox?; + } + return; + } + + if (root is RenderObjectWithChildMixin) { + final RenderBox? child = (root as dynamic).child as RenderBox?; + if (child != null) { + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); + } + } + } + + _FlexIntrinsicMeasurementLookupResult _lookupReusableIntrinsicMeasurement( + RenderBox child, + BoxConstraints childConstraints, + { + bool allowAnonymous = false, + } + ) { + if (!allowAnonymous) { + return const _FlexIntrinsicMeasurementLookupResult(); + } + final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( + child, + allowAnonymous: allowAnonymous, + ); + final bool allowMetricsOnlyReuse = + flowChild != null && + _isMetricsOnlyIntrinsicMeasureFlowChild(flowChild); + if ((!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) && + !allowMetricsOnlyReuse) { + return const _FlexIntrinsicMeasurementLookupResult(); + } + if (_hasBaselineAlignmentForChild(child)) { + return const _FlexIntrinsicMeasurementLookupResult(); + } + if (flowChild == null) { + return const _FlexIntrinsicMeasurementLookupResult(); + } + if (flowChild.needsRelayout) { + return const _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.flowNeedsRelayout, + ); + } + if (child is RenderBoxModel && child.needsRelayout) { + return const _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.childNeedsRelayout, + ); + } + final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = + _childrenIntrinsicMeasureCache[child]; + if (cacheEntry == null) { + return const _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.missingCacheEntry, + ); + } + if (cacheEntry.constraints != childConstraints) { + return const _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.constraintsMismatch, + ); + } + if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { + return _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.subtreeIntrinsicDirty, + missDetails: _FlexAnonymousMetricsProfiler.enabled + ? _describeFirstPendingIntrinsicMeasureInvalidation(child) + : null, + ); + } + return _FlexIntrinsicMeasurementLookupResult(entry: cacheEntry); + } + + void _storeIntrinsicMeasurementCache( + RenderBox child, + BoxConstraints childConstraints, + Size childSize, + double intrinsicMainSize, + ) { + if (_getCacheableIntrinsicMeasureFlowChild(child, allowAnonymous: true) == + null) { + return; + } + _childrenIntrinsicMeasureCache[child] = _FlexIntrinsicMeasurementCacheEntry( + constraints: childConstraints, + size: Size.copy(childSize), + intrinsicMainSize: intrinsicMainSize, + ); + } + + bool _shouldRequirePostMeasureLayout(RenderBox child) { + final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( + child, + allowAnonymous: true, + ); + if (flowChild == null) { + return false; + } + return child is RenderEventListener || flowChild.renderStyle.isSelfAnonymousFlowLayout(); + } + + bool _shouldAvoidParentUsesSizeForFlexChild(RenderBox child) { + // Match Flutter's relayout-boundary model when the flex container only + // needs the wrapper's cached boxSize, not RenderBox.size dependency wiring. + // RenderEventListener falls back to legacy bubbling only when its parent + // did not establish a size dependency. + return child is RenderEventListener; + } + + void _layoutChildForFlex(RenderBox child, BoxConstraints childConstraints) { + if (child is RenderBoxModel) { + child.setRelayoutParentOnSizeChange( + _shouldAvoidParentUsesSizeForFlexChild(child) ? this : null, + ); + } + child.layout( + childConstraints, + parentUsesSize: !_shouldAvoidParentUsesSizeForFlexChild(child), + ); + _childrenRequirePostMeasureLayout[child] = false; + } + + _FlexFastPathRejectReason? _getEarlyNoFlexNoStretchNoBaselineRejectReason( + RenderBox child, BoxConstraints childConstraints) { + if (child is RenderPositionPlaceholder) { + return _FlexFastPathRejectReason.positionedPlaceholderChild; + } + + if (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline) { + return _FlexFastPathRejectReason.containerAlignItemsBaseline; + } + if (renderStyle.alignItems == AlignItems.stretch) { + return _FlexFastPathRejectReason.containerAlignItemsStretch; + } + + final AlignSelf alignSelf = _getAlignSelf(child); + if (alignSelf == AlignSelf.baseline || + alignSelf == AlignSelf.lastBaseline) { + return _FlexFastPathRejectReason.childAlignSelfBaseline; + } + if (alignSelf == AlignSelf.stretch) { + return _FlexFastPathRejectReason.childAlignSelfStretch; + } + + return null; + } + + String _describeFastPathContainer() { + return perfDescribeElementPath( + renderStyle.target, + maxDepth: 5, + maxClassesPerSegment: 1, + ); + } + + String _describeFastPathChild(RenderBox child) { + RenderBoxModel? effectiveChild; + if (child is RenderBoxModel) { + effectiveChild = child; + } else if (child is RenderEventListener) { + effectiveChild = child.child as RenderBoxModel?; + } else if (child is RenderPositionPlaceholder) { + effectiveChild = child.positioned; + } + + if (effectiveChild != null) { + return perfDescribeElementNode( + effectiveChild.renderStyle.target, + maxClasses: 1, + ); + } + + return child.runtimeType.toString(); + } + + bool _isMetricsOnlyIntrinsicMeasureChild(RenderBox child) { + final Expando? eligibilityCache = + _metricsOnlyIntrinsicMeasureChildEligibilityCache; + final int? cachedValue = eligibilityCache?[child]; + if (cachedValue != null) { + return cachedValue == 1; + } + final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( + child, + allowAnonymous: true, + ); + final bool isEligible = + flowChild != null && _isMetricsOnlyIntrinsicMeasureFlowChild(flowChild); + eligibilityCache?[child] = isEligible ? 1 : 0; + return isEligible; + } + + bool _isMetricsOnlyIntrinsicMeasureFlowChild(RenderFlowLayout flowChild) { + if (!_canReuseAnonymousFlowMeasurement(flowChild)) { + return false; + } + if (flowChild.renderStyle.isSelfAnonymousFlowLayout()) { + return true; + } + if (flowChild.renderStyle.target is SpanElement) { + return false; + } + return _flowSubtreeContainsReusableTextHeavyContent(flowChild); + } + + bool _flowSubtreeContainsReusableTextHeavyContent(RenderBox root) { + if (root is RenderPositionPlaceholder) { + return false; + } + + RenderBox effectiveRoot = root; + if (root is RenderEventListener) { + final RenderBox? wrapped = root.child; + if (wrapped == null) { + return false; + } + effectiveRoot = wrapped; + } + + if (effectiveRoot is RenderTextBox) { + return effectiveRoot.data.trim().isNotEmpty; + } + + if (effectiveRoot is RenderBoxModel) { + final CSSPositionType position = effectiveRoot.renderStyle.position; + if (position == CSSPositionType.absolute || + position == CSSPositionType.fixed) { + return false; + } + } + + if (effectiveRoot is RenderFlowLayout) { + if (effectiveRoot.renderStyle.isSelfAnonymousFlowLayout()) { + return _canReuseAnonymousFlowMeasurement(effectiveRoot); + } + if (effectiveRoot.establishIFC || + effectiveRoot.inlineFormattingContext != null) { + return true; + } + } + + if (effectiveRoot + is ContainerRenderObjectMixin>) { + RenderBox? child = (effectiveRoot as dynamic).firstChild as RenderBox?; + while (child != null) { + if (_flowSubtreeContainsReusableTextHeavyContent(child)) { + return true; + } + child = (effectiveRoot as dynamic).childAfter(child) as RenderBox?; + } + return false; + } + + if (effectiveRoot is RenderObjectWithChildMixin) { + final RenderBox? child = (effectiveRoot as dynamic).child as RenderBox?; + if (child != null) { + return _flowSubtreeContainsReusableTextHeavyContent(child); + } + } + + return false; + } + + _FlexAnonymousMetricsRejectReason? _getAnonymousMetricsRejectReason( + RenderBox child) { + if (child is RenderPositionPlaceholder) { + return _FlexAnonymousMetricsRejectReason.positionedPlaceholderChild; + } + + if (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline) { + return _FlexAnonymousMetricsRejectReason.containerAlignItemsBaseline; + } + + final AlignSelf alignSelf = _getAlignSelf(child); + if (alignSelf == AlignSelf.baseline || + alignSelf == AlignSelf.lastBaseline) { + return _FlexAnonymousMetricsRejectReason.childAlignSelfBaseline; + } + + return null; + } + + void _recordEarlyFastPathReject( + _FlexFastPathRejectReason reason, { + RenderBox? child, + int? childIndex, + BoxConstraints? childConstraints, + Map? details, + }) { + if (!_FlexFastPathProfiler.enabled) return; + _FlexFastPathProfiler.recordReject( + _describeFastPathContainer(), + reason, + childLabel: child == null ? null : _describeFastPathChild(child), + childIndex: childIndex, + childConstraints: childConstraints, + details: details, + ); + } + + void _recordEarlyFastPathHit(int childCount) { + if (!_FlexFastPathProfiler.enabled) return; + _FlexFastPathProfiler.recordHit( + _describeFastPathContainer(), + childCount: childCount, + ); + } + + void _recordAnonymousMetricsReject( + _FlexAnonymousMetricsRejectReason reason, { + RenderBox? child, + int? childIndex, + Map? details, + }) { + if (!_FlexAnonymousMetricsProfiler.enabled) return; + _FlexAnonymousMetricsProfiler.recordRejectedRow( + _describeFastPathContainer(), + reason, + childLabel: child == null ? null : _describeFastPathChild(child), + childIndex: childIndex, + details: details, + ); + } + + void _recordAnonymousMetricsEligible({ + required int childCount, + required int candidateChildCount, + }) { + if (!_FlexAnonymousMetricsProfiler.enabled) return; + _FlexAnonymousMetricsProfiler.recordEligibleRow( + _describeFastPathContainer(), + childCount: childCount, + candidateChildCount: candidateChildCount, + ); + } + + void _recordAnonymousMetricsChildCache( + RenderBox child, { + required int childIndex, + required bool hit, + _FlexAnonymousMetricsMissReason? missReason, + Map? missDetails, + }) { + if (!_FlexAnonymousMetricsProfiler.enabled) return; + final String path = _describeFastPathContainer(); + final String childLabel = _describeFastPathChild(child); + if (hit) { + _FlexAnonymousMetricsProfiler.recordChildCacheHit( + path, + childLabel: childLabel, + childIndex: childIndex, + ); + } else { + _FlexAnonymousMetricsProfiler.recordChildCacheMiss( + path, + childLabel: childLabel, + childIndex: childIndex, + reason: missReason ?? _FlexAnonymousMetricsMissReason.missingCacheEntry, + details: missDetails, + ); + } + } + + bool _canAttemptFullEarlyFastPath(List<_RunMetrics> runMetrics) { + for (final _RunMetrics metrics in runMetrics) { + for (final _RunChild runChild in metrics.runChildren) { + if (!runChild.child.constraints.hasTightWidth) { + _recordEarlyFastPathReject( + _FlexFastPathRejectReason.childNonTightWidth, + child: runChild.child, + childConstraints: runChild.child.constraints, + ); + return false; + } + } + } + return true; + } + + List<_RunMetrics>? _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(List children) { + if (!_isHorizontalFlexDirection) { + _recordEarlyFastPathReject( + _FlexFastPathRejectReason.verticalDirection, + details: {'flexDirection': renderStyle.flexDirection}, + ); + return null; + } + if (renderStyle.flexWrap != FlexWrap.nowrap) { + _recordEarlyFastPathReject( + _FlexFastPathRejectReason.wrappedContainer, + details: {'flexWrap': renderStyle.flexWrap}, + ); + return null; + } + + final double mainAxisGap = _getMainAxisGap(); + double runMainAxisExtent = 0.0; + double runCrossAxisExtent = 0.0; + double totalFlexGrow = 0.0; + double totalFlexShrink = 0.0; + final List<_RunChild> runChildren = <_RunChild>[]; + + for (int childIndex = 0; childIndex < children.length; childIndex++) { + final RenderBox child = children[childIndex]; + final BoxConstraints childConstraints; + if (child is RenderBoxModel) { + childConstraints = child.getConstraints(); + } else if (child is RenderConstrainedBox) { + childConstraints = child.additionalConstraints; + } else { + childConstraints = constraints; + } + + final _FlexFastPathRejectReason? rejectReason = + _getEarlyNoFlexNoStretchNoBaselineRejectReason( + child, childConstraints); + if (rejectReason != null) { + _recordEarlyFastPathReject( + rejectReason, + child: child, + childIndex: childIndex, + childConstraints: childConstraints, + ); + return null; + } + + _layoutChildForFlex(child, childConstraints); + _cacheOriginalConstraintsIfNeeded(child, childConstraints); - // Get gap spacing for main axis (between flex items) - double _getMainAxisGap() { - final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) return inv.mainAxisGap; - CSSLengthValue gap = _isHorizontalFlexDirection - ? renderStyle.columnGap - : renderStyle.rowGap; - if (gap.type == CSSLengthType.NORMAL) return 0; - return gap.computedValue; - } + final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; + childParentData?.runIndex = 0; - // Get gap spacing for cross axis (between flex lines) - double _getCrossAxisGap() { - final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) return inv.crossAxisGap; - CSSLengthValue gap = _isHorizontalFlexDirection - ? renderStyle.rowGap - : renderStyle.columnGap; - if (gap.type == CSSLengthType.NORMAL) return 0; - return gap.computedValue; - } + final double childMainSize = _getMainSize(child); + _childrenIntrinsicMainSizes[child] = childMainSize; - // Sort flex items by their order property (default order is 0), stably. - // When multiple items have the same order, preserve their original DOM order. - List _getSortedFlexItems(List children) { - if (children.length < 2) return children; + if (runChildren.isNotEmpty) { + runMainAxisExtent += mainAxisGap; + } + runMainAxisExtent += _getMainAxisExtent(child); + runCrossAxisExtent = math.max(runCrossAxisExtent, _getCrossAxisExtent(child)); - int getOrder(RenderBox box) { - if (box is RenderBoxModel) return box.renderStyle.order; - if (box is RenderEventListener) { - final RenderBox? inner = box.child; - if (inner is RenderBoxModel) return inner.renderStyle.order; + final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; + final _RunChild runChild = _createRunChildMetadata( + child, + childMainSize, + effectiveChild: effectiveChild, + usedFlexBasis: effectiveChild != null ? _getUsedFlexBasis(child) : null, + ); + runChildren.add(runChild); + + if (runChild.flexGrow > 0) { + totalFlexGrow += runChild.flexGrow; + } + if (runChild.flexShrink > 0) { + totalFlexShrink += runChild.flexShrink; } - return 0; } - // Fast path: avoid sorting/allocation when all orders are 0, or already sorted. - bool anyNonZero = false; - bool alreadySorted = true; - int prevOrder = getOrder(children[0]); - anyNonZero = prevOrder != 0; - for (int i = 1; i < children.length; i++) { - final int order = getOrder(children[i]); - anyNonZero = anyNonZero || order != 0; - if (order < prevOrder) alreadySorted = false; - prevOrder = order; - } - if (!anyNonZero || alreadySorted) return children; + final List<_RunMetrics> runMetrics = <_RunMetrics>[ + _RunMetrics(runMainAxisExtent, runCrossAxisExtent, totalFlexGrow, totalFlexShrink, 0, runChildren, 0) + ]; - // Stable sort by (order, originalIndex). - final List<_OrderedFlexItem> items = List<_OrderedFlexItem>.generate( - children.length, - (int i) => _OrderedFlexItem(children[i], getOrder(children[i]), i), - growable: false, - ); - items.sort((_OrderedFlexItem a, _OrderedFlexItem b) { - final int byOrder = a.order.compareTo(b.order); - return byOrder != 0 ? byOrder : a.originalIndex.compareTo(b.originalIndex); - }); - return List.generate(items.length, (int i) => items[i].child, growable: false); + _flexLineBoxMetrics = runMetrics; + return runMetrics; } @override @@ -1733,6 +2947,9 @@ class RenderFlexLayout extends RenderLayoutBox { // 3. Set flex container size according to children size and its own size styles. // 4. Align children according to alignment properties. void _layoutFlexItems(List children) { + _childrenRequirePostMeasureLayout = + Expando('childrenRequirePostMeasureLayout'); + // If no child exists, stop layout. if (children.isEmpty) { _setContainerSizeWithNoChild(); @@ -1742,16 +2959,46 @@ class RenderFlexLayout extends RenderLayoutBox { return; } - if (!kReleaseMode) { - developer.Timeline.startSync('RenderFlex.layoutFlexItems.computeRunMetrics', - arguments: {'renderObject': describeIdentity(this)}); + List<_RunMetrics>? runMetrics = _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(children); + if (runMetrics != null) { + final bool hasStretchedChildren = _hasStretchedChildrenInCrossAxis(runMetrics); + if (!hasStretchedChildren && _canAttemptFullEarlyFastPath(runMetrics)) { + final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); + if (_tryNoFlexNoStretchNoBaselineFastPath( + runMetrics, + maxMainSize: inputs.maxMainSize, + isMainSizeDefinite: inputs.isMainSizeDefinite, + contentBoxLogicalWidth: inputs.contentBoxLogicalWidth, + contentBoxLogicalHeight: inputs.contentBoxLogicalHeight, + onReject: ( + _FlexFastPathRejectReason reason, { + Map? details, + }) { + _recordEarlyFastPathReject(reason, details: details); + }, + )) { + _recordEarlyFastPathHit(children.length); + _setContainerSize(runMetrics); + _setChildrenOffset(runMetrics); + _setMaxScrollableSize(runMetrics); + calculateBaseline(); + return; + } + } + runMetrics = null; } - // Layout children to compute metrics of flex lines. - List<_RunMetrics> runMetrics = _computeRunMetrics(children); + if (runMetrics == null) { + // Layout children to compute metrics of flex lines. + if (!kReleaseMode) { + developer.Timeline.startSync('RenderFlex.layoutFlexItems.computeRunMetrics', + arguments: {'renderObject': describeIdentity(this)}); + } - if (!kReleaseMode) { - developer.Timeline.finishSync(); + runMetrics = _computeRunMetrics(children); + if (!kReleaseMode) { + developer.Timeline.finishSync(); + } } // Set flex container size. @@ -2069,294 +3316,304 @@ class RenderFlexLayout extends RenderLayoutBox { List<_RunChild> runChildren = <_RunChild>[]; // PASS 1+2: Intrinsic layout + compute run metrics in one pass. - for (RenderBox child in children) { - final BoxConstraints childConstraints = _getIntrinsicConstraints(child); - child.layout(childConstraints, parentUsesSize: true); + _metricsOnlyIntrinsicMeasureChildEligibilityCache = + Expando('metricsOnlyIntrinsicMeasureChildEligibilityCache'); + final bool allowAnonymousMetricsOnlyCache = + _canUseAnonymousMetricsOnlyCache(children); + _transientChildSizeOverrides = Expando('transientChildSizeOverrides'); + try { + for (int childIndex = 0; childIndex < children.length; childIndex++) { + final RenderBox child = children[childIndex]; + final BoxConstraints childConstraints = _getIntrinsicConstraints(child); + final bool isMetricsOnlyMeasureChild = + allowAnonymousMetricsOnlyCache && + _isMetricsOnlyIntrinsicMeasureChild(child); + final _FlexIntrinsicMeasurementLookupResult cacheLookup = + _lookupReusableIntrinsicMeasurement( + child, + childConstraints, + allowAnonymous: allowAnonymousMetricsOnlyCache, + ); + final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = + cacheLookup.entry; + if (isMetricsOnlyMeasureChild) { + _recordAnonymousMetricsChildCache( + child, + childIndex: childIndex, + hit: cacheEntry != null, + missReason: cacheLookup.missReason, + missDetails: cacheLookup.missDetails, + ); + } - if (child is RenderBoxModel) { - child.clearOverrideContentSize(); - } + final Size childSize; + double intrinsicMain; + if (cacheEntry != null) { + childSize = cacheEntry.size; + intrinsicMain = cacheEntry.intrinsicMainSize; + _transientChildSizeOverrides![child] = childSize; + if (_shouldRequirePostMeasureLayout(child) || + isMetricsOnlyMeasureChild) { + _childrenRequirePostMeasureLayout[child] = true; + } + } else { + _layoutChildForFlex(child, childConstraints); + if (isMetricsOnlyMeasureChild && + renderStyle.flexWrap == FlexWrap.nowrap && + !_hasWrappingFlexAncestor()) { + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement( + child, + ); + } - final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; + if (child is RenderBoxModel) { + child.clearOverrideContentSize(); + } - // Use intrinsic size for run calculations - final Size childSize = child.size; - double intrinsicMain = isHorizontal ? childSize.width : childSize.height; - - // CSS Flexbox §9.2: For flex-basis:auto with an auto main-size, the flex base size - // should come from the item's max-content contribution in the main axis, not from - // the block formatting context's "fill-available" used size. Our intrinsic pass can - // mistakenly inherit a container-bounded width for block-level items that establish - // an inline formatting context (IFC), causing the base size to equal the container - // width. Detect that case and prefer the IFC's max-intrinsic width instead. - if (isHorizontal && child is RenderFlowLayout) { - final RenderFlowLayout flowChild = child; - final CSSRenderStyle cs = flowChild.renderStyle; - final bool autoMain = cs.width.isAuto; - final bool hasDefiniteBasis = _getFlexBasis(flowChild) != null; - if (autoMain && !hasDefiniteBasis) { - double? candidate; - if (flowChild.inlineFormattingContext != null) { - // Paragraph max-intrinsic width approximates the max-content contribution. - final double paraMax = flowChild.inlineFormattingContext!.paragraphMaxIntrinsicWidth; - // Convert content-width to border-box width by adding horizontal padding + borders. - final double paddingBorderH = - cs.paddingLeft.computedValue + - cs.paddingRight.computedValue + - cs.effectiveBorderLeftWidth.computedValue + - cs.effectiveBorderRightWidth.computedValue; - candidate = (paraMax.isFinite ? paraMax : 0) + paddingBorderH; - } else { - // Fallback: use max intrinsic width (already includes padding/border). - final double maxIntrinsic = flowChild.getMaxIntrinsicWidth(double.infinity); - if (maxIntrinsic.isFinite) { - candidate = maxIntrinsic; + childSize = Size.copy(_getChildSize(child)!); + _transientChildSizeOverrides![child] = childSize; + intrinsicMain = isHorizontal ? childSize.width : childSize.height; + + // CSS Flexbox §9.2: For flex-basis:auto with an auto main-size, the flex base size + // should come from the item's max-content contribution in the main axis, not from + // the block formatting context's "fill-available" used size. Our intrinsic pass can + // mistakenly inherit a container-bounded width for block-level items that establish + // an inline formatting context (IFC), causing the base size to equal the container + // width. Detect that case and prefer the IFC's max-intrinsic width instead. + if (isHorizontal && child is RenderFlowLayout) { + final RenderFlowLayout flowChild = child; + final CSSRenderStyle cs = flowChild.renderStyle; + final bool autoMain = cs.width.isAuto; + final bool hasDefiniteBasis = _getFlexBasis(flowChild) != null; + if (autoMain && !hasDefiniteBasis) { + double? candidate; + if (flowChild.inlineFormattingContext != null) { + // Paragraph max-intrinsic width approximates the max-content contribution. + final double paraMax = flowChild.inlineFormattingContext!.paragraphMaxIntrinsicWidth; + // Convert content-width to border-box width by adding horizontal padding + borders. + final double paddingBorderH = + cs.paddingLeft.computedValue + + cs.paddingRight.computedValue + + cs.effectiveBorderLeftWidth.computedValue + + cs.effectiveBorderRightWidth.computedValue; + candidate = (paraMax.isFinite ? paraMax : 0) + paddingBorderH; + } else { + // Fallback: use max intrinsic width (already includes padding/border). + final double maxIntrinsic = flowChild.getMaxIntrinsicWidth(double.infinity); + if (maxIntrinsic.isFinite) { + candidate = maxIntrinsic; + } + } + // If the currently measured intrinsic width is larger (e.g., filled to container), + // prefer the content-based candidate to avoid unintended expansion. + if (candidate != null && candidate > 0 && candidate < intrinsicMain) { + intrinsicMain = candidate; + } } } - // If the currently measured intrinsic width is larger (e.g., filled to container), - // prefer the content-based candidate to avoid unintended expansion. - if (candidate != null && candidate > 0 && candidate < intrinsicMain) { - intrinsicMain = candidate; - } - } - } - - // Clamp intrinsic main size by child's min/max constraints before flexing, - // so percentage max-width/height act as caps on the base size per spec. - if (child is RenderBoxModel) { - final CSSRenderStyle cs = child.renderStyle; - // Determine min/max along the main axis - double? minMain; - double? maxMain; - if (isHorizontal) { - if (cs.minWidth.isNotAuto) minMain = cs.minWidth.computedValue; - if (!cs.maxWidth.isNone) maxMain = cs.maxWidth.computedValue; - } else { - if (cs.minHeight.isNotAuto) minMain = cs.minHeight.computedValue; - if (!cs.maxHeight.isNone) maxMain = cs.maxHeight.computedValue; - } - // intrinsicMain is the border-box main size. In WebF (border-box model), - // min-width/max-width are already specified for the border box. Do not - // add padding/border again when clamping. - if (maxMain != null && maxMain.isFinite && intrinsicMain > maxMain) { - intrinsicMain = maxMain; - } - if (minMain != null && minMain.isFinite && intrinsicMain < minMain) { - intrinsicMain = minMain; - } - } + // Clamp intrinsic main size by child's min/max constraints before flexing, + // so percentage max-width/height act as caps on the base size per spec. + if (child is RenderBoxModel) { + final CSSRenderStyle cs = child.renderStyle; + // Determine min/max along the main axis + double? minMain; + double? maxMain; + if (isHorizontal) { + if (cs.minWidth.isNotAuto) minMain = cs.minWidth.computedValue; + if (!cs.maxWidth.isNone) maxMain = cs.maxWidth.computedValue; + } else { + if (cs.minHeight.isNotAuto) minMain = cs.minHeight.computedValue; + if (!cs.maxHeight.isNone) maxMain = cs.maxHeight.computedValue; + } - // If a flex item has percentage max-size and is truly empty, its base size should be - // its padding+border box (do not expand to the percentage constraint). - if (child is RenderBoxModel) { - bool hasPctMaxMain = isHorizontal - ? child.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE - : child.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; - bool hasAutoMain = isHorizontal ? child.renderStyle.width.isAuto : child.renderStyle.height - .isAuto; - if (hasPctMaxMain && hasAutoMain) { - double paddingBorderMain = isHorizontal - ? (child.renderStyle.effectiveBorderLeftWidth.computedValue + - child.renderStyle.effectiveBorderRightWidth.computedValue + - child.renderStyle.paddingLeft.computedValue + - child.renderStyle.paddingRight.computedValue) - : (child.renderStyle.effectiveBorderTopWidth.computedValue + - child.renderStyle.effectiveBorderBottomWidth.computedValue + - child.renderStyle.paddingTop.computedValue + - child.renderStyle.paddingBottom.computedValue); + // intrinsicMain is the border-box main size. In WebF (border-box model), + // min-width/max-width are already specified for the border box. Do not + // add padding/border again when clamping. + if (maxMain != null && maxMain.isFinite && intrinsicMain > maxMain) { + intrinsicMain = maxMain; + } + if (minMain != null && minMain.isFinite && intrinsicMain < minMain) { + intrinsicMain = minMain; + } + } - // Check if this is an empty element (no content) using DOM-based detection - bool isEmptyElement = false; - Element domElement = child.renderStyle.target; - isEmptyElement = !domElement.hasChildren(); + // If a flex item has percentage max-size and is truly empty, its base size should be + // its padding+border box (do not expand to the percentage constraint). + if (child is RenderBoxModel) { + bool hasPctMaxMain = isHorizontal + ? child.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE + : child.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; + bool hasAutoMain = isHorizontal ? child.renderStyle.width.isAuto : child.renderStyle.height + .isAuto; + if (hasPctMaxMain && hasAutoMain) { + double paddingBorderMain = isHorizontal + ? (child.renderStyle.effectiveBorderLeftWidth.computedValue + + child.renderStyle.effectiveBorderRightWidth.computedValue + + child.renderStyle.paddingLeft.computedValue + + child.renderStyle.paddingRight.computedValue) + : (child.renderStyle.effectiveBorderTopWidth.computedValue + + child.renderStyle.effectiveBorderBottomWidth.computedValue + + child.renderStyle.paddingTop.computedValue + + child.renderStyle.paddingBottom.computedValue); + + // Check if this is an empty element (no content) using DOM-based detection + bool isEmptyElement = false; + Element domElement = child.renderStyle.target; + isEmptyElement = !domElement.hasChildren(); + + // For empty elements, force intrinsic size to padding+border + if (isEmptyElement) { + intrinsicMain = paddingBorderMain; + } + } + } - // For empty elements, force intrinsic size to padding+border - if (isEmptyElement) { - intrinsicMain = paddingBorderMain; + // Enforce automatic minimum main size (min-size:auto) so preserved sizes + // never fall below min-content contributions in the main axis. + if (child is RenderBoxModel) { + final double autoMinMain = _getMinMainAxisSize(child); + if (intrinsicMain < autoMinMain) { + intrinsicMain = autoMinMain; + } } - } - } - // Enforce automatic minimum main size (min-size:auto) so preserved sizes - // never fall below min-content contributions in the main axis. - if (child is RenderBoxModel) { - final double autoMinMain = _getMinMainAxisSize(child); - if (intrinsicMain < autoMinMain) { - intrinsicMain = autoMinMain; + _storeIntrinsicMeasurementCache(child, childConstraints, childSize, intrinsicMain); } - } - - _childrenIntrinsicMainSizes[child] = intrinsicMain; - - Size? intrinsicChildSize = _getChildSize(child, shouldUseIntrinsicMainSize: true); - - double childMainAxisExtent = _getMainAxisExtent(child, shouldUseIntrinsicMainSize: true); - double childCrossAxisExtent = _getCrossAxisExtent(child); - // Include gap spacing in flex line limit check - double gapSpacing = runChildren.isNotEmpty ? mainAxisGap : 0; - bool isExceedFlexLineLimit = runMainAxisExtent + gapSpacing + childMainAxisExtent > flexLineLimit; - // calculate flex line - if (isWrap && - runChildren.isNotEmpty && - isExceedFlexLineLimit) { - runMetrics.add(_RunMetrics( - runMainAxisExtent, - runCrossAxisExtent, - totalFlexGrow, - totalFlexShrink, - maxSizeAboveBaseline, - runChildren, - 0)); - runChildren = <_RunChild>[]; - runMainAxisExtent = 0.0; - runCrossAxisExtent = 0.0; - maxSizeAboveBaseline = 0.0; - maxSizeBelowBaseline = 0.0; - - totalFlexGrow = 0; - totalFlexShrink = 0; - } - // Add gap spacing between items (not before the first item) - if (runChildren.isNotEmpty) { - runMainAxisExtent += mainAxisGap; - } - runMainAxisExtent += childMainAxisExtent; - runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); - // Vertical align is only valid for inline box. - // Baseline alignment in column direction behave the same as flex-start. - AlignSelf alignSelf = _getAlignSelf(child); - bool isBaselineAlign = - alignSelf == AlignSelf.baseline || - alignSelf == AlignSelf.lastBaseline || - renderStyle.alignItems == AlignItems.baseline || - renderStyle.alignItems == AlignItems.lastBaseline; - if (isHorizontal && isBaselineAlign) { - // Distance from top to baseline of child - double childAscent = _getChildAscent(child); - double childMarginTop = 0; - double childMarginBottom = 0; - if (child is RenderBoxModel) { - childMarginTop = child.renderStyle.marginTop.computedValue; - childMarginBottom = child.renderStyle.marginBottom.computedValue; - } - if (DebugFlags.debugLogFlexBaselineEnabled) { - final Size? ic = intrinsicChildSize; - renderingLogger.finer('[FlexBaseline] PASS2 child=' - '${child.runtimeType}#${child.hashCode} ' - 'intrinsicSize=${ic?.width.toStringAsFixed(2)}x${ic?.height.toStringAsFixed(2)} ' - 'ascent=${childAscent.toStringAsFixed(2)} ' - 'mT=${childMarginTop.toStringAsFixed(2)} mB=${childMarginBottom.toStringAsFixed(2)}'); + final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; + + _childrenIntrinsicMainSizes[child] = intrinsicMain; + + Size? intrinsicChildSize = _getChildSize(child, shouldUseIntrinsicMainSize: true); + + double childMainAxisExtent = _getMainAxisExtent(child, shouldUseIntrinsicMainSize: true); + double childCrossAxisExtent = _getCrossAxisExtent(child); + // Include gap spacing in flex line limit check + double gapSpacing = runChildren.isNotEmpty ? mainAxisGap : 0; + bool isExceedFlexLineLimit = runMainAxisExtent + gapSpacing + childMainAxisExtent > flexLineLimit; + // calculate flex line + if (isWrap && + runChildren.isNotEmpty && + isExceedFlexLineLimit) { + runMetrics.add(_RunMetrics( + runMainAxisExtent, + runCrossAxisExtent, + totalFlexGrow, + totalFlexShrink, + maxSizeAboveBaseline, + runChildren, + 0)); + runChildren = <_RunChild>[]; + runMainAxisExtent = 0.0; + runCrossAxisExtent = 0.0; + maxSizeAboveBaseline = 0.0; + maxSizeBelowBaseline = 0.0; + + totalFlexGrow = 0; + totalFlexShrink = 0; } - maxSizeAboveBaseline = math.max( - childAscent, - maxSizeAboveBaseline, - ); - maxSizeBelowBaseline = math.max( - childMarginTop + childMarginBottom + intrinsicChildSize!.height - childAscent, - maxSizeBelowBaseline, - ); - runCrossAxisExtent = maxSizeAboveBaseline + maxSizeBelowBaseline; - if (DebugFlags.debugLogFlexBaselineEnabled) { - renderingLogger.finer('[FlexBaseline] RUN update: maxAbove=' - '${maxSizeAboveBaseline.toStringAsFixed(2)} ' - 'maxBelow=${maxSizeBelowBaseline.toStringAsFixed(2)} ' - 'runCross=${runCrossAxisExtent.toStringAsFixed(2)}'); + // Add gap spacing between items (not before the first item) + if (runChildren.isNotEmpty) { + runMainAxisExtent += mainAxisGap; } - } else { + runMainAxisExtent += childMainAxisExtent; runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); - } - // Per CSS Flexbox §9.7, keep two sizes: - // - flex base size: from flex-basis if definite, otherwise the intrinsic - // content-based size BEFORE min/max clamping. - // - hypothetical main size: the base size clamped by min/max. - // We store the base size in runChild.originalMainSize so remaining free - // space and shrink/grow weighting use the correct base, and keep the - // clamped value in _childrenIntrinsicMainSizes for line metrics. - final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; - final double? usedFlexBasis = effectiveChild != null ? _getUsedFlexBasis(child) : null; - - double baseMainSize; - if (usedFlexBasis != null) { - // Used basis is already border-box (>= padding+border) for non-zero bases. - // For flex-basis: 0% in the main axis, use a flex base size of 0 so equal-flex - // items share free space evenly regardless of padding/border, while the - // non-flex portion (padding/border) is accounted for separately in totalSpace. - final CSSLengthValue? fb = effectiveChild?.renderStyle.flexBasis; - if (fb != null && fb.type == CSSLengthType.PERCENTAGE && fb.computedValue == 0) { - baseMainSize = 0; + // Vertical align is only valid for inline box. + // Baseline alignment in column direction behave the same as flex-start. + AlignSelf alignSelf = _getAlignSelf(child); + bool isBaselineAlign = + alignSelf == AlignSelf.baseline || + alignSelf == AlignSelf.lastBaseline || + renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline; + if (isHorizontal && isBaselineAlign) { + // Distance from top to baseline of child + double childAscent = _getChildAscent(child); + double childMarginTop = 0; + double childMarginBottom = 0; + if (child is RenderBoxModel) { + childMarginTop = child.renderStyle.marginTop.computedValue; + childMarginBottom = child.renderStyle.marginBottom.computedValue; + } + if (DebugFlags.debugLogFlexBaselineEnabled) { + final Size? ic = intrinsicChildSize; + renderingLogger.finer('[FlexBaseline] PASS2 child=' + '${child.runtimeType}#${child.hashCode} ' + 'intrinsicSize=${ic?.width.toStringAsFixed(2)}x${ic?.height.toStringAsFixed(2)} ' + 'ascent=${childAscent.toStringAsFixed(2)} ' + 'mT=${childMarginTop.toStringAsFixed(2)} mB=${childMarginBottom.toStringAsFixed(2)}'); + } + maxSizeAboveBaseline = math.max( + childAscent, + maxSizeAboveBaseline, + ); + maxSizeBelowBaseline = math.max( + childMarginTop + childMarginBottom + intrinsicChildSize!.height - childAscent, + maxSizeBelowBaseline, + ); + runCrossAxisExtent = maxSizeAboveBaseline + maxSizeBelowBaseline; + if (DebugFlags.debugLogFlexBaselineEnabled) { + renderingLogger.finer('[FlexBaseline] RUN update: maxAbove=' + '${maxSizeAboveBaseline.toStringAsFixed(2)} ' + 'maxBelow=${maxSizeBelowBaseline.toStringAsFixed(2)} ' + 'runCross=${runCrossAxisExtent.toStringAsFixed(2)}'); + } } else { - baseMainSize = usedFlexBasis; + runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); + } + + // Per CSS Flexbox §9.7, keep two sizes: + // - flex base size: from flex-basis if definite, otherwise the intrinsic + // content-based size BEFORE min/max clamping. + // - hypothetical main size: the base size clamped by min/max. + // We store the base size in runChild.originalMainSize so remaining free + // space and shrink/grow weighting use the correct base, and keep the + // clamped value in _childrenIntrinsicMainSizes for line metrics. + final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; + final double? usedFlexBasis = effectiveChild != null ? _getUsedFlexBasis(child) : null; + + double baseMainSize; + if (usedFlexBasis != null) { + // Used basis is already border-box (>= padding+border) for non-zero bases. + // For flex-basis: 0% in the main axis, use a flex base size of 0 so equal-flex + // items share free space evenly regardless of padding/border, while the + // non-flex portion (padding/border) is accounted for separately in totalSpace. + final CSSLengthValue? fb = effectiveChild?.renderStyle.flexBasis; + if (fb != null && fb.type == CSSLengthType.PERCENTAGE && fb.computedValue == 0) { + baseMainSize = 0; + } else { + baseMainSize = usedFlexBasis; + } + } else { + // childSize is the intrinsic measurement from PASS 1 (pre-clamp). + baseMainSize = isHorizontal ? childSize.width : childSize.height; } - } else { - // childSize is the intrinsic measurement from PASS 1 (pre-clamp). - baseMainSize = isHorizontal ? childSize.width : childSize.height; - } - - // Use clamped intrinsic main size as the hypothetical size for line metrics. - double originalMainSize = baseMainSize; - double mainAxisMargin = 0.0; - if (effectiveChild != null) { - final RenderStyle s = effectiveChild.renderStyle; - final double marginHorizontal = s.marginLeft.computedValue + s.marginRight.computedValue; - final double marginVertical = s.marginTop.computedValue + s.marginBottom.computedValue; - mainAxisMargin = isHorizontal ? marginHorizontal : marginVertical; - } - - final RenderBoxModel? marginBoxModel = - child is RenderBoxModel ? child : (child is RenderPositionPlaceholder ? child.positioned : null); - final RenderStyle? marginStyle = marginBoxModel?.renderStyle; - final bool marginLeftAuto = marginStyle?.marginLeft.isAuto ?? false; - final bool marginRightAuto = marginStyle?.marginRight.isAuto ?? false; - final bool marginTopAuto = marginStyle?.marginTop.isAuto ?? false; - final bool marginBottomAuto = marginStyle?.marginBottom.isAuto ?? false; - final bool hasAutoMainAxisMargin = isHorizontal - ? (marginLeftAuto || marginRightAuto) - : (marginTopAuto || marginBottomAuto); - final bool hasAutoCrossAxisMargin = isHorizontal - ? (marginTopAuto || marginBottomAuto) - : (marginLeftAuto || marginRightAuto); - - final double flexGrow = _getFlexGrow(child); - final double flexShrink = _getFlexShrink(child); - - runChildren.add(_RunChild( - child, - originalMainSize, - 0, - false, - effectiveChild: effectiveChild, - alignSelf: alignSelf, - flexGrow: flexGrow, - flexShrink: flexShrink, - usedFlexBasis: usedFlexBasis, - mainAxisMargin: mainAxisMargin, - mainAxisStartMargin: _flowAwareChildMainAxisMargin(child) ?? 0.0, - mainAxisEndMargin: _flowAwareChildMainAxisMargin(child, isEnd: true) ?? 0.0, - crossAxisStartMargin: _flowAwareChildCrossAxisMargin(child) ?? 0.0, - crossAxisEndMargin: _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0.0, - hasAutoMainAxisMargin: hasAutoMainAxisMargin, - hasAutoCrossAxisMargin: hasAutoCrossAxisMargin, - marginLeftAuto: marginLeftAuto, - marginRightAuto: marginRightAuto, - marginTopAuto: marginTopAuto, - marginBottomAuto: marginBottomAuto, - isReplaced: effectiveChild?.renderStyle.isSelfRenderReplaced() ?? false, - aspectRatio: effectiveChild?.renderStyle.aspectRatio, - )); - childParentData!.runIndex = runMetrics.length; + // Use clamped intrinsic main size as the hypothetical size for line metrics. + final _RunChild runChild = _createRunChildMetadata( + child, + baseMainSize, + effectiveChild: effectiveChild, + usedFlexBasis: usedFlexBasis, + ); + runChildren.add(runChild); - assert(child.parentData == childParentData); + childParentData!.runIndex = runMetrics.length; - if (flexGrow > 0) { - totalFlexGrow += flexGrow; - } - if (flexShrink > 0) { - totalFlexShrink += flexShrink; + assert(child.parentData == childParentData); + + if (runChild.flexGrow > 0) { + totalFlexGrow += runChild.flexGrow; + } + if (runChild.flexShrink > 0) { + totalFlexShrink += runChild.flexShrink; + } } + } finally { + _transientChildSizeOverrides = null; + _metricsOnlyIntrinsicMeasureChildEligibilityCache = null; } if (runChildren.isNotEmpty) { @@ -2681,6 +3938,7 @@ class RenderFlexLayout extends RenderLayoutBox { required bool isMainSizeDefinite, required double? contentBoxLogicalWidth, required double? contentBoxLogicalHeight, + _FlexFastPathRejectCallback? onReject, }) { final bool isHorizontal = _isHorizontalFlexDirection; final double mainAxisGap = _getMainAxisGap(); @@ -2727,7 +3985,33 @@ class RenderFlexLayout extends RenderLayoutBox { } } - if (willGrow || willShrink) return false; + if (willShrink) { + onReject?.call( + _FlexFastPathRejectReason.wouldShrink, + details: { + 'freeSpace': freeSpace.toStringAsFixed(2), + 'totalSpace': totalSpace.toStringAsFixed(2), + 'maxMainSize': maxMainSize?.toStringAsFixed(2), + 'totalFlexShrink': metrics.totalFlexShrink.toStringAsFixed(2), + }, + ); + return false; + } + if (willGrow) { + onReject?.call( + _FlexFastPathRejectReason.wouldGrow, + details: { + 'freeSpace': freeSpace.toStringAsFixed(2), + 'totalSpace': totalSpace.toStringAsFixed(2), + 'maxMainSize': maxMainSize?.toStringAsFixed(2), + 'totalFlexGrow': metrics.totalFlexGrow.toStringAsFixed(2), + 'boundedOnly': boundedOnly, + 'isMainSizeDefinite': isMainSizeDefinite, + 'containerStyleMin': containerStyleMin.toStringAsFixed(2), + }, + ); + return false; + } } // No flexing/no stretching/no baseline alignment: relayout only items that actually need it. @@ -2742,10 +4026,11 @@ class RenderFlexLayout extends RenderLayoutBox { final RenderBoxModel? effectiveChild = runChild.effectiveChild; if (effectiveChild == null) continue; - final double childOldMainSize = isHorizontal ? child.size.width : child.size.height; + final double childOldMainSize = _getMainSize(child); final double? desiredPreservedMain = _childrenIntrinsicMainSizes[child]; - bool needsLayout = effectiveChild.needsRelayout; + bool needsLayout = effectiveChild.needsRelayout || + (_childrenRequirePostMeasureLayout[child] == true); if (!needsLayout && desiredPreservedMain != null && desiredPreservedMain != childOldMainSize) { needsLayout = true; } @@ -2764,7 +4049,7 @@ class RenderFlexLayout extends RenderLayoutBox { if (!needsLayout && !isHorizontal) { final bool childCrossAuto = effectiveChild.renderStyle.width.isAuto; if (childCrossAuto && availCross.isFinite) { - final double measuredBorderW = effectiveChild.size.width; + final double measuredBorderW = _getChildSize(effectiveChild)!.width; if (measuredBorderW > availCross + 0.5) { needsLayout = true; } @@ -2782,7 +4067,7 @@ class RenderFlexLayout extends RenderLayoutBox { runChildrenCount, preserveMainAxisSize: desiredPreservedMain, ); - child.layout(childConstraints, parentUsesSize: true); + _layoutChildForFlex(child, childConstraints); didRelayout = true; } @@ -2815,74 +4100,11 @@ class RenderFlexLayout extends RenderLayoutBox { final bool canAttemptFastPath = isHorizontal && !hasBaselineAlignment; final bool hasStretchedChildren = canAttemptFastPath ? _hasStretchedChildrenInCrossAxis(runMetrics) : true; - double? contentBoxLogicalWidth = renderStyle.contentBoxLogicalWidth; - double? contentBoxLogicalHeight = renderStyle.contentBoxLogicalHeight; - - // Container's width specified by style or inherited from parent. - // Use null to indicate an indefinite size; do not default to 0, - // which would incorrectly create free space for flex resolution. - double? containerWidth; - if (contentBoxLogicalWidth != null) { - containerWidth = contentBoxLogicalWidth; - } else if (contentConstraints!.hasTightWidth) { - containerWidth = contentConstraints!.maxWidth; - } - - // Container's height specified by style or inherited from parent. - // Use null to indicate an indefinite size; do not default to 0, - // which would incorrectly create free space for flex resolution. - double? containerHeight; - - // If not tight or explicit, consider bounded max size as a definite available size - // for resolving flexible lengths. This allows flex-shrink to operate when the - // container has a finite max-height/width (e.g., max-height: 17px in column). - if (containerWidth == null) { - if ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) { - containerWidth = contentConstraints!.maxWidth; - } else if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { - containerWidth = constraints.maxWidth; - } - } - // Prefer the actually imposed outer constraints when they are tighter than - // the content constraints (e.g., a flex item whose max inline-size is 172px - // but whose internal contentMaxConstraintsWidth is larger). This ensures - // the flex algorithm sees the correct available main size and will shrink - // items when content overflows. - if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { - containerWidth = - (containerWidth == null) ? constraints.maxWidth : math.min(containerWidth, constraints.maxWidth); - } - if ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) { - containerHeight = contentConstraints!.maxHeight; - } else if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { - containerHeight = constraints.maxHeight; - } - if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { - containerHeight = - (containerHeight == null) ? constraints.maxHeight : math.min(containerHeight, constraints.maxHeight); - } - if (contentBoxLogicalHeight != null) { - containerHeight = contentBoxLogicalHeight; - } else if (contentConstraints!.hasTightHeight) { - containerHeight = contentConstraints!.maxHeight; - } - - double? maxMainSize = isHorizontal ? containerWidth : containerHeight; - - // Flexbox has several additional cases where a length can be considered definite. - // https://www.w3.org/TR/css-flexbox-1/#definite-sizes - // Treat the main size as definite if either: - // - The flex container has a specified content-box size in the main axis, or - // - The layout constraints on the main axis are tight (e.g., fixed by parent). - bool isMainSizeDefinite = isHorizontal - ? (contentBoxLogicalWidth != null || (contentConstraints?.hasTightWidth ?? false) || - constraints.hasTightWidth || - ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) || - (constraints.hasBoundedWidth && constraints.maxWidth.isFinite)) - : (contentBoxLogicalHeight != null || (contentConstraints?.hasTightHeight ?? false) || - constraints.hasTightHeight || - ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) || - (constraints.hasBoundedHeight && constraints.maxHeight.isFinite)); + final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); + final double? contentBoxLogicalWidth = inputs.contentBoxLogicalWidth; + final double? contentBoxLogicalHeight = inputs.contentBoxLogicalHeight; + double? maxMainSize = inputs.maxMainSize; + final bool isMainSizeDefinite = inputs.isMainSizeDefinite; if (canAttemptFastPath && !hasStretchedChildren) { if (_tryNoFlexNoStretchNoBaselineFastPath( @@ -3055,7 +4277,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Non-RenderBoxModel child: nothing to tighten in phase 1. continue; } - double childOldMainSize = _isHorizontalFlexDirection ? child.size.width : child.size.height; + double childOldMainSize = _getMainSize(child); // Determine used main size from the flexible lengths result, if any. double? childFlexedMainSize; @@ -3069,7 +4291,8 @@ class RenderFlexLayout extends RenderLayoutBox { } bool needsLayout = (childFlexedMainSize != null) || - (effectiveChild.needsRelayout); + (effectiveChild.needsRelayout) || + (_childrenRequirePostMeasureLayout[child] == true); if (!needsLayout && desiredPreservedMain != null && (desiredPreservedMain != childOldMainSize)) { needsLayout = true; } @@ -3097,7 +4320,7 @@ class RenderFlexLayout extends RenderLayoutBox { final double availCross = contentConstraints?.maxWidth ?? double.infinity; if (childCrossAuto && noStretch && availCross.isFinite) { // Compare against the border-box width measured during intrinsic pass. - final double measuredBorderW = effectiveChild.size.width; + final double measuredBorderW = _getChildSize(effectiveChild)!.width; if (measuredBorderW > availCross + 0.5) { needsLayout = true; } @@ -3116,7 +4339,7 @@ class RenderFlexLayout extends RenderLayoutBox { runChildrenList.length, preserveMainAxisSize: desiredPreservedMain, ); - child.layout(childConstraints, parentUsesSize: true); + _layoutChildForFlex(child, childConstraints); } // After Phase 1, recompute the run cross extent based on the items’ natural @@ -3145,7 +4368,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (childStretchedCrossSize == null) continue; // If the current cross size already matches the stretched result, skip. - final double currentCross = _isHorizontalFlexDirection ? child.size.height : child.size.width; + final double currentCross = _isHorizontalFlexDirection + ? _getChildSize(child)!.height + : _getChildSize(child)!.width; if ((childStretchedCrossSize - currentCross).abs() < 0.5) continue; // Apply stretch by relayout with tightened cross-axis constraint. @@ -3155,7 +4380,7 @@ class RenderFlexLayout extends RenderLayoutBox { childStretchedCrossSize, runChildrenList.length, ); - child.layout(childConstraints, parentUsesSize: true); + _layoutChildForFlex(child, childConstraints); } // Finally, recompute run main & cross extents using the final sizes. @@ -3182,8 +4407,9 @@ class RenderFlexLayout extends RenderLayoutBox { for (final _RunChild runChild in runChildren) { RenderBox child = runChild.child; - double childMainSize = isHorizontal ? child.size.width : child.size.height; - double childCrossSize = isHorizontal ? child.size.height : child.size.width; + final Size childSize = _getChildSize(child)!; + double childMainSize = isHorizontal ? childSize.width : childSize.height; + double childCrossSize = isHorizontal ? childSize.height : childSize.width; double childCrossMargin = 0; if (child is RenderBoxModel) { childCrossMargin = isHorizontal @@ -3487,7 +4713,7 @@ class RenderFlexLayout extends RenderLayoutBox { // but only when it is less than the container's available cross size to avoid // regressing centering cases (e.g., column-wrap with align-self:center). if (maxContentCB <= minContentCB + 0.5) { - final double priorBorderW = child.size.width; + final double priorBorderW = _getChildSize(child)!.width; final double priorContentW = math.max(0.0, priorBorderW - (child.renderStyle.padding.horizontal + child.renderStyle.border.horizontal)); if (priorContentW.isFinite && priorContentW > minContentCB) { @@ -3541,10 +4767,10 @@ class RenderFlexLayout extends RenderLayoutBox { // stretch cases and avoids regressions. double fixedW; if (child.renderStyle.isSelfRenderReplaced() && child.renderStyle.aspectRatio != null) { - final double usedBorderBoxH = child.renderStyle.borderBoxLogicalHeight ?? child.size.height; + final double usedBorderBoxH = child.renderStyle.borderBoxLogicalHeight ?? _getChildSize(child)!.height; fixedW = usedBorderBoxH * child.renderStyle.aspectRatio!; } else { - fixedW = child.size.width; + fixedW = _getChildSize(child)!.width; } final double containerCrossMax = contentConstraints?.maxWidth ?? double.infinity; final double containerContentW = containerCrossMax.isFinite @@ -3936,7 +5162,7 @@ class RenderFlexLayout extends RenderLayoutBox { double runMainExtent = 0; for (final _RunChild runChild in runChildren) { final RenderBox child = runChild.child; - double runChildMainSize = _isHorizontalFlexDirection ? child.size.width : child.size.height; + double runChildMainSize = _getMainSize(child); // Should add main axis margin of child to the main axis auto size of parent. if (child is RenderBoxModel) { double childMarginTop = child.renderStyle.marginTop.computedValue; @@ -3976,7 +5202,8 @@ class RenderFlexLayout extends RenderLayoutBox { double runCrossExtent = 0; for (final _RunChild runChild in runChildren) { final RenderBox child = runChild.child; - final double runChildCrossSize = _isHorizontalFlexDirection ? child.size.height : child.size.width; + final Size childSize = _getChildSize(child)!; + final double runChildCrossSize = _isHorizontalFlexDirection ? childSize.height : childSize.width; runCrossExtent = math.max(runCrossExtent, runChildCrossSize); } runCrossSize.add(runCrossExtent); @@ -4006,6 +5233,12 @@ class RenderFlexLayout extends RenderLayoutBox { // Set the size of scrollable overflow area for flex layout. // https://drafts.csswg.org/css-overflow-3/#scrollable void _setMaxScrollableSize(List<_RunMetrics> runMetrics) { + final double physicalMainAxisStartBorder = _isHorizontalFlexDirection + ? renderStyle.effectiveBorderLeftWidth.computedValue + : renderStyle.effectiveBorderTopWidth.computedValue; + final double physicalMainAxisEndPadding = _isHorizontalFlexDirection + ? renderStyle.paddingRight.computedValue + : renderStyle.paddingBottom.computedValue; // Scrollable main size collection of each line. List scrollableMainSizeOfLines = []; // Scrollable cross size collection of each line. @@ -4023,9 +5256,10 @@ class RenderFlexLayout extends RenderLayoutBox { for (final _RunChild runChild in runChildren) { final RenderBox child = runChild.child; - Size childScrollableSize = child.size; + Size childScrollableSize = _getChildSize(child)!; double childOffsetX = 0; double childOffsetY = 0; + double childTransformMainOverflow = 0; if (child is RenderBoxModel) { final RenderStyle childRenderStyle = child.renderStyle; @@ -4057,25 +5291,46 @@ class RenderFlexLayout extends RenderLayoutBox { if (transformOffset != null) { childOffsetX += transformOffset.dx; childOffsetY += transformOffset.dy; + childTransformMainOverflow = math.max( + 0, + _isHorizontalFlexDirection ? transformOffset.dx : transformOffset.dy, + ); } } - final double childBoxMainSize = _isHorizontalFlexDirection ? child.size.width : child.size.height; - final double childBoxCrossSize = _isHorizontalFlexDirection ? child.size.height : child.size.width; - final double childMainOffset = _isHorizontalFlexDirection ? childOffsetX : childOffsetY; + final Size childSize = _getChildSize(child)!; + final double childBoxMainSize = _isHorizontalFlexDirection ? childSize.width : childSize.height; + final double childBoxCrossSize = _isHorizontalFlexDirection ? childSize.height : childSize.width; final double childCrossOffset = _isHorizontalFlexDirection ? childOffsetY : childOffsetX; final double childScrollableMainExtent = _isHorizontalFlexDirection - ? childScrollableSize.width + childOffsetX - : childScrollableSize.height + childOffsetY; + ? childScrollableSize.width + childTransformMainOverflow + : childScrollableSize.height + childTransformMainOverflow; final double childScrollableCrossExtent = _isHorizontalFlexDirection ? childScrollableSize.height + childOffsetY : childScrollableSize.width + childOffsetX; - // The child's extent must cover at least its offset border-box. - // child.scrollableSize may only cover padding-box, but offsets (margin/relative/transform) - // still need to be preserved so negative offsets don't create phantom trailing scroll range. - final double childScrollableMain = preSiblingsMainSize + - math.max(childBoxMainSize + childMainOffset, childScrollableMainExtent); + final RenderLayoutParentData? childParentData = + child.parentData as RenderLayoutParentData?; + final double childMainPosition = _isHorizontalFlexDirection + ? (childParentData?.offset.dx ?? preSiblingsMainSize) + : (childParentData?.offset.dy ?? preSiblingsMainSize); + double childPhysicalMainEndMargin = 0; + if (child is RenderBoxModel) { + childPhysicalMainEndMargin = _isHorizontalFlexDirection + ? child.renderStyle.marginRight.computedValue + : child.renderStyle.marginBottom.computedValue; + } + + // Use the actual laid-out main-axis position so scrollable overflow follows + // post-alignment geometry (e.g. justify-content:center on overflowing columns) + // instead of the pre-alignment stacked size. This prevents blank trailing + // scroll range after children are shifted by negative leading space. + final double childScrollableMain = math.max( + 0, + childMainPosition - physicalMainAxisStartBorder, + ) + + math.max(childBoxMainSize, childScrollableMainExtent) + + childPhysicalMainEndMargin; final double childScrollableCross = math.max( childBoxCrossSize + childCrossOffset, childScrollableCrossExtent); @@ -4084,7 +5339,7 @@ class RenderFlexLayout extends RenderLayoutBox { maxScrollableCrossSizeInLine = math.max(maxScrollableCrossSizeInLine, childScrollableCross); // Update running main size for subsequent siblings (border-box size + main-axis margins). - double childMainSize = _isHorizontalFlexDirection ? child.size.width : child.size.height; + double childMainSize = _getMainSize(child); if (child is RenderBoxModel) { if (_isHorizontalFlexDirection) { childMainSize += child.renderStyle.marginLeft.computedValue + child.renderStyle.marginRight.computedValue; @@ -4122,10 +5377,10 @@ class RenderFlexLayout extends RenderLayoutBox { bool isScrollContainer = renderStyle.effectiveOverflowX != CSSOverflowType.visible || renderStyle.effectiveOverflowY != CSSOverflowType.visible; - // Padding in the end direction of axis should be included in scroll container. + // Child positions already include physical start padding. Only the trailing + // padding needs to be added here. double maxScrollableMainSizeOfChildren = maxScrollableMainSizeOfLines + - _flowAwareMainAxisPadding() + - (isScrollContainer ? _flowAwareMainAxisPadding(isEnd: true) : 0); + (isScrollContainer ? physicalMainAxisEndPadding : 0); // Max scrollable cross size of all lines. double maxScrollableCrossSizeOfLines = scrollableCrossSizeOfLines.isEmpty @@ -5060,11 +6315,15 @@ class RenderFlexLayout extends RenderLayoutBox { // Get child size through boxSize to avoid flutter error when parentUsesSize is set to false. Size? _getChildSize(RenderBox? child, {bool shouldUseIntrinsicMainSize = false}) { Size? childSize; - if (child is RenderBoxModel) { + if (child != null) { + childSize = _transientChildSizeOverrides?[child]; + } + + if (childSize == null && child is RenderBoxModel) { childSize = child.boxSize; - } else if (child is RenderPositionPlaceholder) { + } else if (childSize == null && child is RenderPositionPlaceholder) { childSize = child.boxSize; - } else if (child != null && child.hasSize) { + } else if (childSize == null && child != null && child.hasSize) { // child is WidgetElement. childSize = child.size; } diff --git a/webf/lib/src/rendering/layout_box.dart b/webf/lib/src/rendering/layout_box.dart index d9fba73d0f..235b1f8231 100644 --- a/webf/lib/src/rendering/layout_box.dart +++ b/webf/lib/src/rendering/layout_box.dart @@ -507,24 +507,28 @@ abstract class RenderLayoutBox extends RenderBoxModel void insert(RenderBox child, {RenderBox? after}) { super.insert(child, after: after); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListInsert'); } @override void remove(RenderBox child) { super.remove(child); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListRemove'); } @override void removeAll() { super.removeAll(); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListRemoveAll'); } @override void move(RenderBox child, {RenderBox? after}) { super.move(child, after: after); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListMove'); } @override diff --git a/webf/lib/src/rendering/text.dart b/webf/lib/src/rendering/text.dart index a91e0949be..6a7b372b06 100644 --- a/webf/lib/src/rendering/text.dart +++ b/webf/lib/src/rendering/text.dart @@ -21,10 +21,30 @@ class RenderTextBox extends RenderBox with RenderObjectWithChildMixin String _data; TextPainter? _textPainter; TextSpan? _cachedSpan; + bool _hasPendingTextLayoutUpdate = false; + + bool get hasPendingTextLayoutUpdate => _hasPendingTextLayoutUpdate; + + void clearPendingTextLayoutUpdateAfterMeasurement() { + _hasPendingTextLayoutUpdate = false; + } + + void _markAncestorSubtreeIntrinsicMeasurementUpdate() { + RenderObject? ancestor = parent; + while (ancestor != null) { + if (ancestor is RenderBoxModel) { + ancestor.markNeedsSubtreeIntrinsicMeasurementUpdate('textData'); + break; + } + ancestor = ancestor.parent; + } + } set data(String value) { if (_data == value) return; _data = value; + _hasPendingTextLayoutUpdate = true; + _markAncestorSubtreeIntrinsicMeasurementUpdate(); // Text content changed. Since text boxes are measured and painted by the // parent's inline formatting context, notify the parent to relayout so the // paragraph gets rebuilt with the new text content. @@ -225,6 +245,7 @@ class RenderTextBox extends RenderBox with RenderObjectWithChildMixin @override void performLayout() { + _hasPendingTextLayoutUpdate = false; if (_data.isEmpty) { size = constraints.constrain(Size.zero); return; diff --git a/webf/lib/src/rendering/widget.dart b/webf/lib/src/rendering/widget.dart index 5c20cd9dfe..c64fbe1b36 100644 --- a/webf/lib/src/rendering/widget.dart +++ b/webf/lib/src/rendering/widget.dart @@ -22,6 +22,7 @@ class RenderWidget extends RenderBoxModel // Cache sticky children to calculate the base offset of sticky children final Set stickyChildren = {}; + RenderBoxModel? _relayoutNotifyingChild; @override BoxSizeType get widthSizeType { @@ -53,6 +54,16 @@ class RenderWidget extends RenderBoxModel return null; } + void _updateRelayoutNotifyingChild(RenderBox? child) { + final RenderBoxModel? next = child is RenderBoxModel ? child : null; + if (identical(_relayoutNotifyingChild, next)) { + return; + } + _relayoutNotifyingChild?.setRelayoutParentOnSizeChange(null); + _relayoutNotifyingChild = next; + _relayoutNotifyingChild?.setRelayoutParentOnSizeChange(this); + } + void _layoutChild(RenderBox child) { // Ensure logical content sizes are computed from CSS before deriving constraints // so that explicit width/height (e.g. h-8) can be honored. @@ -188,9 +199,22 @@ class RenderWidget extends RenderBoxModel // Deflate padding constraints. // childConstraints = renderStyle.deflatePaddingConstraints(childConstraints); - child.layout(childConstraints, parentUsesSize: true); + _updateRelayoutNotifyingChild(child); - Size childSize = child.size; + Size childSize; + if (child is RenderBoxModel) { + child.layout(childConstraints, parentUsesSize: false); + final Size? childBoxSize = child.boxSize; + if (childBoxSize != null) { + childSize = childBoxSize; + } else { + child.layout(childConstraints, parentUsesSize: true); + childSize = child.size; + } + } else { + child.layout(childConstraints, parentUsesSize: true); + childSize = child.size; + } setMaxScrollableSize(childSize); size = getBoxSize(childSize); @@ -561,18 +585,21 @@ class RenderWidget extends RenderBoxModel void insert(RenderBox child, {RenderBox? after}) { super.insert(child, after: after); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListInsert'); } @override void remove(RenderBox child) { super.remove(child); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListRemove'); } @override void move(RenderBox child, {RenderBox? after}) { super.move(child, after: after); _cachedPaintingOrder = null; + markNeedsIntrinsicMeasurementUpdate('childListMove'); } @override @@ -610,6 +637,7 @@ class RenderWidget extends RenderBoxModel if (nonPositionedChildren.isNotEmpty) { _layoutChild(nonPositionedChildren.first); } else { + _updateRelayoutNotifyingChild(null); performResize(); } diff --git a/webf/lib/src/rendering/widget_element_child.dart b/webf/lib/src/rendering/widget_element_child.dart index 9b50de403c..c204735e29 100644 --- a/webf/lib/src/rendering/widget_element_child.dart +++ b/webf/lib/src/rendering/widget_element_child.dart @@ -52,6 +52,7 @@ class WebFWidgetElementChild extends SingleChildRenderObjectWidget { /// WebF HTML elements through the [findWidgetElementChild] method. class RenderWidgetElementChild extends RenderProxyBox { BoxConstraints? _effectiveChildConstraints; + RenderBoxModel? _relayoutNotifyingChild; /// The last constraints actually used to lay out [child]. /// @@ -59,6 +60,16 @@ class RenderWidgetElementChild extends RenderProxyBox { /// we collapse to intrinsic sizing before laying out the WebF subtree. BoxConstraints get effectiveChildConstraints => _effectiveChildConstraints ?? constraints; + void _updateRelayoutNotifyingChild(RenderBox? child) { + final RenderBoxModel? next = child is RenderBoxModel ? child : null; + if (identical(_relayoutNotifyingChild, next)) { + return; + } + _relayoutNotifyingChild?.setRelayoutParentOnSizeChange(null); + _relayoutNotifyingChild = next; + _relayoutNotifyingChild?.setRelayoutParentOnSizeChange(this); + } + @override void performLayout() { final BoxConstraints incoming = constraints; @@ -88,6 +99,7 @@ class RenderWidgetElementChild extends RenderProxyBox { } _effectiveChildConstraints = effective; + _updateRelayoutNotifyingChild(c); if (c is RenderBoxModel) { // Ensure CSS sizing queries resolve constraints against the *current* @@ -103,9 +115,21 @@ class RenderWidgetElementChild extends RenderProxyBox { } if (c != null) { - c.layout(effective, parentUsesSize: true); - size = c.size; + if (c is RenderBoxModel) { + c.layout(effective, parentUsesSize: false); + final Size? childBoxSize = c.boxSize; + if (childBoxSize != null) { + size = childBoxSize; + } else { + c.layout(effective, parentUsesSize: true); + size = c.size; + } + } else { + c.layout(effective, parentUsesSize: true); + size = c.size; + } } else { + _updateRelayoutNotifyingChild(null); size = computeSizeForNoChild(incoming); } }