diff --git a/integration_tests/PROFILE_HOTSPOT_TARGETS.md b/integration_tests/PROFILE_HOTSPOT_TARGETS.md new file mode 100644 index 0000000000..cf4e7f1f3b --- /dev/null +++ b/integration_tests/PROFILE_HOTSPOT_TARGETS.md @@ -0,0 +1,55 @@ +# Profile Hotspot Targets + +## 2026-03-24 User Case A + +Source profile: `/tmp/dart_devtools_2026-03-24_14_46_17.876.json` + +Target stack to reproduce: + +- `RenderFlexLayout._layoutFlexItems` -> `RenderFlexLayout._doPerformLayout` +- `RenderFlowLayout._layoutChildren` -> `InlineFormattingContext.layout` +- `InlineFormattingContext._buildAndLayoutParagraph` +- `InlineFormattingContext._layoutParagraphForConstraints` +- `InlineFormattingContext._layoutAtomicInlineItemsForParagraph` +- `RenderEventListener.calculateBaseline` / `RenderWidget.calculateBaseline` + +Supporting costs visible in the same capture: + +- inherited text/style getters: `fontStyle`, `fontFamily`, `wordBreak`, `textIndent` +- render-style parent/render-box scans: `getAttachedRenderParentRenderStyle`, `getRenderBoxValueByType` +- repeated flex run metrics and relayout work: `_tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics`, `_computeRunMetrics`, `_adjustChildrenSize` + +Profiler caveat: + +- `InlineFormattingContext._profileSection` and `Timeline.startSync` / `finishSync` currently add substantial profiler noise, so sampled CPU stacks are more reliable than leaf-only timing totals. + +Profiler example mapping: + +- `flex_inline_layout` should target this user-case stack with wrapped non-flex cards inside a flex container, rich inline content, and atomic inline controls. + +## 2026-03-24 User Case B + +Source profile: `/tmp/dart_devtools_2026-03-24_22_57_35.368.json` + +Target stack to reproduce: + +- `RenderFlexLayout._layoutFlexItems` -> `RenderFlexLayout._doPerformLayout` +- `RenderFlexLayout._adjustChildrenSize` +- `RenderFlexLayout._tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics` +- `RenderFlexLayout._tryNoFlexNoStretchNoBaselineFastPath` +- `RenderFlexLayout._computeRunMetrics` +- `RenderFlowLayout._layoutChildren` -> `InlineFormattingContext.layout` +- `InlineFormattingContext._buildAndLayoutParagraph` + +Supporting costs visible in the same capture: + +- paragraph follow-on work: `_layoutParagraphForConstraints`, `_layoutAtomicInlineItemsForParagraph` +- baseline/widget work: `RenderEventListener.calculateBaseline`, `RenderWidget.performLayout`, `RenderWidget.calculateBaseline` +- render-style scans: `getAttachedRenderParentRenderStyle`, `getRenderBoxValueByType`, `everyAttachedWidgetRenderBox` +- value resolution: `CSSLengthValue.computedValue` +- secondary animation load: `AnimationTimeline._onTick` + +Profiler caveat: + +- This Android trace is heavily polluted by timeline markers: `_reportTaskEvent` is about half of all leaf samples and `InlineFormattingContext._profileSection` appears in more than half of stacks. +- `flex_inline_layout` should therefore be profiled with sampled CPU stacks, inline profile sections disabled by default, and horizontal `nowrap` no-flex cards so the `NoFlexNoStretchNoBaseline` path is exercised. diff --git a/integration_tests/integration_test/profile_hotspot_cases_test.dart b/integration_tests/integration_test/profile_hotspot_cases_test.dart index ad12f03885..66a7b3ae3f 100644 --- a/integration_tests/integration_test/profile_hotspot_cases_test.dart +++ b/integration_tests/integration_test/profile_hotspot_cases_test.dart @@ -16,6 +16,8 @@ import 'package:webf/webf.dart'; final developer.UserTag _paragraphRebuildProfileTag = developer.UserTag('profile_hotspots.paragraph_rebuild'); +final developer.UserTag _flexInlineLayoutProfileTag = + developer.UserTag('profile_hotspots.flex_inline_layout'); void main() { final IntegrationTestWidgetsFlutterBinding binding = @@ -191,6 +193,53 @@ void main() { expect(stage.className, isEmpty); }); + + testWidgets('profiles flex inline layout hotspot', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-flex-inline-${DateTime.now().millisecondsSinceEpoch}', + html: _buildFlexInlineLayoutHtml(cardCount: 72), + ); + + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + + binding.reportData ??= {}; + binding.reportData!['flex_inline_layout_meta'] = { + 'cardCount': 72, + 'mutationIterations': 32, + 'styleMutationPhases': 4, + 'layoutMode': 'no-flex-no-stretch-no-baseline-nowrap', + }; + + await _runFlexInlineLayoutLoop( + prepared, + mutationIterations: 10, + widths: const ['360px', '324px', '296px', '344px'], + ); + + binding.reportData!['flex_inline_layout_cpu_samples'] = + await _captureCpuSamples( + userTag: _flexInlineLayoutProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runFlexInlineLayoutLoop( + prepared, + mutationIterations: 32, + widths: const ['360px', '324px', '296px', '344px'], + ); + }, + reportKey: 'flex_inline_layout_timeline', + ); + }, + ); + + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(board.getBoundingClientRect().height, greaterThan(0)); + }); }); } @@ -386,6 +435,26 @@ Future _runParagraphRebuildLoop( } } +Future _runFlexInlineLayoutLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + host.setInlineStyle('width', widths[phase]); + host.setInlineStyle('padding', phase.isEven ? '10px' : '8px'); + board.className = 'phase-$phase'; + board.setInlineStyle('letterSpacing', phase == 1 ? '0.12px' : '0px'); + board.setInlineStyle('wordSpacing', phase == 2 ? '0.35px' : '0px'); + board.style.flushPendingProperties(); + board.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 2); + } +} + Future _pumpFrames( WidgetTester tester, int frames, { @@ -648,6 +717,283 @@ String _buildOpacityTransitionHtml({ '''; } +String _buildFlexInlineLayoutHtml({ + required int cardCount, +}) { + final String cards = List.generate(cardCount, (int index) { + final int tone = index % 4; + return ''' +
+
+ +
+ issue cluster ${index + 1} +
+
+ p${(index % 3) + 1} +
+
+
+ Long wrapped summary for inline layout and flex relayout sample ${index + 1} + active + baseline measurement path ${index + 1} +
+
+
+ +
+
+ +
+
+ active +
+
+ +
+
+
+
+ ETA ${12 + (index % 9)}h +
+
+ owner ${index + 3} +
+
+ series-${(index % 5) + 1} +
+
+ needs follow-up +
+
+
+'''; + }).join(); + + return ''' + + + + + + +
+
$cards
+
+ + +'''; +} + class _PreparedProfileCase { const _PreparedProfileCase({ required this.controller, diff --git a/integration_tests/snapshots/css/css-flexbox/resize-min-content.ts.827375281.png b/integration_tests/snapshots/css/css-flexbox/resize-min-content.ts.827375281.png index 07c15df5e2..268baf3997 100644 Binary files a/integration_tests/snapshots/css/css-flexbox/resize-min-content.ts.827375281.png and b/integration_tests/snapshots/css/css-flexbox/resize-min-content.ts.827375281.png differ diff --git a/integration_tests/snapshots/css/css-selectors/pseudo-focus-visible.ts.0be1c4c91.png b/integration_tests/snapshots/css/css-selectors/pseudo-focus-visible.ts.0be1c4c91.png index 1f971f5561..05621ecd3f 100644 Binary files a/integration_tests/snapshots/css/css-selectors/pseudo-focus-visible.ts.0be1c4c91.png and b/integration_tests/snapshots/css/css-selectors/pseudo-focus-visible.ts.0be1c4c91.png differ diff --git a/integration_tests/snapshots/css/css-text-mixin/text_comprehensive_test.ts.8af684eb1.png b/integration_tests/snapshots/css/css-text-mixin/text_comprehensive_test.ts.8af684eb1.png index 8d9134aa0d..9e1abe1c19 100644 Binary files a/integration_tests/snapshots/css/css-text-mixin/text_comprehensive_test.ts.8af684eb1.png and b/integration_tests/snapshots/css/css-text-mixin/text_comprehensive_test.ts.8af684eb1.png differ diff --git a/integration_tests/specs/css/css-flexbox/resize-min-content.ts b/integration_tests/specs/css/css-flexbox/resize-min-content.ts index 23bfea1faa..35090382b4 100644 --- a/integration_tests/specs/css/css-flexbox/resize-min-content.ts +++ b/integration_tests/specs/css/css-flexbox/resize-min-content.ts @@ -1,6 +1,9 @@ /*auto generated*/ describe('resize-min', () => { it('content-flexbox', async () => { + document.body.innerHTML = ''; + document.body.style.cssText = ''; + document.documentElement.style.cssText = ''; await resizeViewport(360, 640); let log; let content; diff --git a/webf/lib/src/css/flexbox.dart b/webf/lib/src/css/flexbox.dart index ce507f0158..d83a0cb15d 100644 --- a/webf/lib/src/css/flexbox.dart +++ b/webf/lib/src/css/flexbox.dart @@ -255,7 +255,9 @@ mixin CSSFlexboxMixin on RenderStyle { if (_alignSelf == value) return; _alignSelf = value; if (isParentRenderFlexLayout() || isParentRenderGridLayout()) { + markNeedsIntrinsicMeasurement('flexItemAlignment'); markNeedsLayout(); + markParentNeedsLayout(); } } @@ -269,7 +271,9 @@ mixin CSSFlexboxMixin on RenderStyle { } _flexBasis = value; if (isParentRenderFlexLayout()) { + markNeedsIntrinsicMeasurement('flexBasis'); markNeedsLayout(); + markParentNeedsLayout(); } } @@ -280,7 +284,9 @@ mixin CSSFlexboxMixin on RenderStyle { if (_flexGrow == value) return; _flexGrow = value; if (isParentRenderFlexLayout()) { + markNeedsIntrinsicMeasurement('flexGrow'); markNeedsLayout(); + markParentNeedsLayout(); } } @@ -291,7 +297,9 @@ mixin CSSFlexboxMixin on RenderStyle { if (_flexShrink == value) return; _flexShrink = value; if (isParentRenderFlexLayout()) { + markNeedsIntrinsicMeasurement('flexShrink'); markNeedsLayout(); + markParentNeedsLayout(); } } diff --git a/webf/lib/src/css/grid.dart b/webf/lib/src/css/grid.dart index fa6a75b4e8..08f08410e5 100644 --- a/webf/lib/src/css/grid.dart +++ b/webf/lib/src/css/grid.dart @@ -851,7 +851,11 @@ mixin CSSGridMixin on RenderStyle { set gridColumnStart(GridPlacement? value) { if (_gridColumnStart == value) return; _gridColumnStart = value; + markNeedsIntrinsicMeasurement('gridColumnStart'); markNeedsLayout(); + if (isParentRenderGridLayout()) { + markParentNeedsLayout(); + } } GridPlacement? _gridColumnEnd; @@ -860,7 +864,11 @@ mixin CSSGridMixin on RenderStyle { set gridColumnEnd(GridPlacement? value) { if (_gridColumnEnd == value) return; _gridColumnEnd = value; + markNeedsIntrinsicMeasurement('gridColumnEnd'); markNeedsLayout(); + if (isParentRenderGridLayout()) { + markParentNeedsLayout(); + } } GridPlacement? _gridRowStart; @@ -869,7 +877,11 @@ mixin CSSGridMixin on RenderStyle { set gridRowStart(GridPlacement? value) { if (_gridRowStart == value) return; _gridRowStart = value; + markNeedsIntrinsicMeasurement('gridRowStart'); markNeedsLayout(); + if (isParentRenderGridLayout()) { + markParentNeedsLayout(); + } } GridPlacement? _gridRowEnd; @@ -878,7 +890,11 @@ mixin CSSGridMixin on RenderStyle { set gridRowEnd(GridPlacement? value) { if (_gridRowEnd == value) return; _gridRowEnd = value; + markNeedsIntrinsicMeasurement('gridRowEnd'); markNeedsLayout(); + if (isParentRenderGridLayout()) { + markParentNeedsLayout(); + } } GridTemplateAreasDefinition? _gridTemplateAreasDefinition; diff --git a/webf/lib/src/css/render_style.dart b/webf/lib/src/css/render_style.dart index 9f78c3cca8..05246ed65f 100644 --- a/webf/lib/src/css/render_style.dart +++ b/webf/lib/src/css/render_style.dart @@ -798,11 +798,89 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { double getHeightByAspectRatio(); final Map _widgetRenderObjects = {}; + RenderBoxModel? _cachedAttachedSizedRenderBoxModel; + RenderBoxModel? _cachedAttachedRenderBoxModel; + RenderBoxModel? _cachedParentLookupRenderBoxModel; + RenderObject? _cachedParentLookupDirectParent; + RenderObject? _cachedParentLookupRenderObject; + RenderStyle? _cachedAttachedParentRenderStyle; Map get widgetRenderObjects => _widgetRenderObjects; Iterable get widgetRenderObjectIterator => _widgetRenderObjects.values; + @pragma('vm:prefer-inline') + void _clearRenderObjectAccessCaches() { + _cachedAttachedSizedRenderBoxModel = null; + _cachedAttachedRenderBoxModel = null; + _cachedParentLookupRenderBoxModel = null; + _cachedParentLookupDirectParent = null; + _cachedParentLookupRenderObject = null; + _cachedAttachedParentRenderStyle = null; + } + + @pragma('vm:prefer-inline') + RenderBoxModel? _preferredAttachedRenderBoxModel({required bool requireSize}) { + if (requireSize) { + final RenderBoxModel? cached = _cachedAttachedSizedRenderBoxModel; + if (cached != null && cached.attached && cached.hasSize) { + return cached; + } + } else { + final RenderBoxModel? cached = _cachedAttachedRenderBoxModel; + if (cached != null && cached.attached) { + return cached; + } + } + + RenderBoxModel? resolved = + _widgetRenderObjects.values.firstWhereOrNull((renderBox) => renderBox.attached && renderBox.hasSize); + resolved ??= _widgetRenderObjects.values.firstWhereOrNull((renderBox) => renderBox.attached); + + if (resolved != null) { + _cachedAttachedRenderBoxModel = resolved; + if (resolved.hasSize) { + _cachedAttachedSizedRenderBoxModel = resolved; + } else if (identical(_cachedAttachedSizedRenderBoxModel, resolved)) { + _cachedAttachedSizedRenderBoxModel = null; + } + } + + return resolved; + } + + @pragma('vm:prefer-inline') + RenderObject? _resolveAttachedParentRenderObject(RenderBoxModel renderBoxModel) { + final RenderObject? directParent = renderBoxModel.parent; + if (identical(_cachedParentLookupRenderBoxModel, renderBoxModel) && + identical(_cachedParentLookupDirectParent, directParent)) { + return _cachedParentLookupRenderObject; + } + + RenderObject? parent = directParent; + while (parent != null) { + if (parent is RenderEventListener && identical(parent.renderStyle, this)) { + parent = parent.parent; + continue; + } + if (parent is RenderLayoutBoxWrapper && identical(parent.renderStyle, this)) { + parent = parent.parent; + continue; + } + if (parent is RenderFlowLayout && identical(parent.renderStyle, this)) { + parent = parent.parent; + continue; + } + break; + } + + _cachedParentLookupRenderBoxModel = renderBoxModel; + _cachedParentLookupDirectParent = directParent; + _cachedParentLookupRenderObject = parent; + _cachedAttachedParentRenderStyle = parent is RenderBoxModel ? parent.renderStyle : null; + return parent; + } + // For some style changes, we needs to upgrade void requestWidgetToRebuild(AdapterUpdateReason reason) { switch (reason) { @@ -1185,7 +1263,17 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { @pragma('vm:prefer-inline') T? getAttachedRenderParentRenderStyle() { - return getRenderBoxValueByType(RenderObjectGetType.parent, (_, renderStyle) => renderStyle) as T? ?? + final RenderBoxModel? renderBoxModel = attachedRenderBoxModel; + if (renderBoxModel == null) { + return target.parentElement?.renderStyle as T?; + } + + final RenderObject? parent = _resolveAttachedParentRenderObject(renderBoxModel); + if (parent is RenderBoxModel) { + return parent.renderStyle as T?; + } + + return _cachedAttachedParentRenderStyle as T? ?? target.parentElement?.renderStyle as T?; } @@ -1414,6 +1502,16 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { @pragma('vm:prefer-inline') void markNeedsInlineCollection() { everyAttachedWidgetRenderBox((_, renderObject) { + void markOwnedFlowSubtree(RenderObject node) { + if (node is RenderFlowLayout && identical(node.renderStyle, this)) { + node.markNeedsCollectInlines(); + node.markNeedsLayout(); + } + node.visitChildren(markOwnedFlowSubtree); + } + + markOwnedFlowSubtree(renderObject); + RenderObject? node = renderObject; while (node != null) { if (node is RenderFlowLayout) { @@ -1503,9 +1601,8 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { } dynamic getRenderBoxValueByType(RenderObjectGetType getType, RenderBoxModelGetter getter) { - RenderBoxModel? widgetRenderBoxModel = - widgetRenderObjectIterator.firstWhereOrNull((renderBox) => renderBox.attached && renderBox.hasSize) ?? - widgetRenderObjectIterator.firstWhereOrNull((renderBox) => renderBox.attached); + final RenderBoxModel? widgetRenderBoxModel = + _preferredAttachedRenderBoxModel(requireSize: true); if (widgetRenderBoxModel == null) return null; @@ -1527,26 +1624,7 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { return matcher(renderBoxModel, renderBoxModel.renderStyle); case RenderObjectGetType.parent: - final directParent = renderBoxModel.parent; - RenderObject? parent = directParent; - while (parent != null) { - // Only skip wrappers that belong to this same element (same RenderStyle), - // otherwise we may accidentally skip a real ancestor element's render box - // and break layout/constraint resolution for many specs. - if (parent is RenderEventListener && identical(parent.renderStyle, this)) { - parent = parent.parent; - continue; - } - if (parent is RenderLayoutBoxWrapper && identical(parent.renderStyle, this)) { - parent = parent.parent; - continue; - } - if (parent is RenderFlowLayout && identical(parent.renderStyle, this)) { - parent = parent.parent; - continue; - } - break; - } + final RenderObject? parent = _resolveAttachedParentRenderObject(renderBoxModel); return matcher(parent, parent is RenderBoxModel ? parent.renderStyle : null); case RenderObjectGetType.firstChild: @@ -1629,6 +1707,7 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { } void removeAllRenderObject() { + _clearRenderObjectAccessCaches(); _widgetRenderObjects.clear(); } @@ -1641,10 +1720,12 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { void addOrUpdateWidgetRenderObjects( flutter.RenderObjectElement ownerRenderObjectElement, RenderBoxModel targetRenderBoxModel) { + _clearRenderObjectAccessCaches(); _widgetRenderObjects[ownerRenderObjectElement] = targetRenderBoxModel; } void unmountWidgetRenderObject(flutter.Element ownerRenderObjectElement) { + _clearRenderObjectAccessCaches(); _widgetRenderObjects.remove(ownerRenderObjectElement); } @@ -1653,7 +1734,7 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { } RenderBoxModel? get attachedRenderBoxModel { - return _widgetRenderObjects.values.firstWhereOrNull((renderBox) => renderBox.attached); + return _preferredAttachedRenderBoxModel(requireSize: false); } flutter.RenderObjectElement? get attachedRenderObjectElement { @@ -1688,6 +1769,7 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { } void dispose() { + _clearRenderObjectAccessCaches(); _widgetRenderObjects.clear(); } } diff --git a/webf/lib/src/css/sizing.dart b/webf/lib/src/css/sizing.dart index c79462cd54..fac731cc05 100644 --- a/webf/lib/src/css/sizing.dart +++ b/webf/lib/src/css/sizing.dart @@ -8,6 +8,7 @@ */ import 'package:webf/css.dart'; +import 'package:webf/dom.dart' as dom; import 'package:webf/rendering.dart'; // CSS Box Sizing: https://drafts.csswg.org/css-sizing-3/ @@ -262,6 +263,17 @@ mixin CSSSizingMixin on RenderStyle { if (isParentRenderViewportBox()) { markParentNeedsLayout(); } + + if (isDocumentRootBox()) { + void visitor(dom.Node node) { + if (node is! dom.Element) return; + node.renderStyle.markNeedsIntrinsicMeasurement('rootSizing'); + node.renderStyle.markNeedsLayout(); + node.visitChildren(visitor); + } + + target.visitChildren(visitor); + } } void _markScrollContainerNeedsLayout() { diff --git a/webf/lib/src/css/text.dart b/webf/lib/src/css/text.dart index b75aa8f9e5..a42e499242 100644 --- a/webf/lib/src/css/text.dart +++ b/webf/lib/src/css/text.dart @@ -170,7 +170,6 @@ mixin CSSTextMixin on RenderStyle { } FontStyle? _fontStyle; - @override FontStyle get fontStyle { // Get style from self or closest parent if specified style property is not set @@ -211,7 +210,6 @@ mixin CSSTextMixin on RenderStyle { } List? _fontFamily; - @override List? get fontFamily { // Get style from self or closest parent if specified style property is not set @@ -356,7 +354,6 @@ mixin CSSTextMixin on RenderStyle { // text-indent (inherited) CSSLengthValue? _textIndent; - CSSLengthValue get textIndent { if (_textIndent == null && parent != null) { return (parent as CSSRenderStyle).textIndent; @@ -409,7 +406,6 @@ mixin CSSTextMixin on RenderStyle { // word-break (inherited) WordBreak? _wordBreak; - @override WordBreak get wordBreak { if (_wordBreak == null && parent != null) { @@ -549,6 +545,17 @@ mixin CSSTextMixin on RenderStyle { } target.visitChildren(visitor); + + void renderVisitor(RenderObject child) { + if (child is RenderBoxModel) { + if (child.renderStyle.target.style[propertyName].isEmpty) { + child.renderStyle.resetInheritedTextCaches(); + } + } + child.visitChildren(renderVisitor); + } + + visitChildren(renderVisitor); } @override @@ -650,14 +657,25 @@ mixin CSSTextMixin on RenderStyle { // Mark all nested layout and text children as needs layout when properties that will affect both // text and layout (line-height, white-space) changes. void _markNestChildrenTextAndLayoutNeedsLayout(RenderStyle renderStyle, String styleProperty) { + if (renderStyle is CSSTextMixin) { + renderStyle._markTextNeedsLayout(); + } if (renderStyle.isSelfRenderLayoutBox()) { renderStyle.markNeedsIntrinsicMeasurement('textLayout:$styleProperty'); renderStyle.markNeedsLayout(); visitor(RenderObject child) { if (child is RenderLayoutBox && child is! RenderEventListener) { + final String specifiedValue = + child.renderStyle.target.style.getPropertyValue(styleProperty); + final bool dependsOnCurrentColor = styleProperty == COLOR && + specifiedValue.trim().toLowerCase() == CURRENT_COLOR; // Only need to layout when the specified style property is not set. - if (child.renderStyle.target.style[styleProperty].isEmpty) { + if (child.renderStyle.target.style[styleProperty].isEmpty || + dependsOnCurrentColor) { + if (dependsOnCurrentColor && child.renderStyle is CSSTextMixin) { + (child.renderStyle as CSSTextMixin)._markTextNeedsLayout(); + } _markNestChildrenTextAndLayoutNeedsLayout(child.renderStyle, styleProperty); } } else { @@ -673,8 +691,13 @@ mixin CSSTextMixin on RenderStyle { // None inheritable style change should only loop direct children to update text node with specified // style property not set in its parent. void _markTextNeedsLayout() { + markNeedsInlineCollection(); + markNeedsIntrinsicMeasurement('textDirectSelf'); + markNeedsLayout(); + requestWidgetToRebuild(UpdateChildNodeUpdateReason()); visitor(RenderObject child) { if (child is RenderTextBox) { + child.markTextStyleNeedsLayout(); child.renderStyle.markNeedsIntrinsicMeasurement('textDirect'); child.renderStyle.markNeedsLayout(); } else { @@ -689,15 +712,27 @@ mixin CSSTextMixin on RenderStyle { // Inheritable style change should loop nest children to update text node with specified style property // not set in its parent. void _markChildrenTextNeedsLayout(RenderStyle renderStyle, String styleProperty) { + if (renderStyle is CSSTextMixin) { + renderStyle._markTextNeedsLayout(); + } visitor(dom.Node child) { if (child is dom.TextNode) { + child.markTextStyleNeedsLayout(); final RenderStyle parentStyle = child.parentElement!.renderStyle; parentStyle.markNeedsIntrinsicMeasurement('textInherited:$styleProperty'); parentStyle.markNeedsLayout(); } - if (child is dom.Element && child.style[styleProperty].isEmpty) { - child.visitChildren(visitor); + if (child is dom.Element) { + final String specifiedValue = child.style.getPropertyValue(styleProperty); + final bool dependsOnCurrentColor = styleProperty == COLOR && + specifiedValue.trim().toLowerCase() == CURRENT_COLOR; + if (child.style[styleProperty].isEmpty || dependsOnCurrentColor) { + if (dependsOnCurrentColor && child.renderStyle is CSSTextMixin) { + (child.renderStyle as CSSTextMixin)._markTextNeedsLayout(); + } + child.visitChildren(visitor); + } } } diff --git a/webf/lib/src/css/values/length.dart b/webf/lib/src/css/values/length.dart index cd50bae4e5..bf0ba3f867 100644 --- a/webf/lib/src/css/values/length.dart +++ b/webf/lib/src/css/values/length.dart @@ -154,12 +154,16 @@ class CSSLengthValue { final CSSCalcValue? calcValue; final double? value; final CSSLengthType type; + final String? _realPropertyName; CSSLengthValue.calc(this.calcValue, this.renderStyle, this.propertyName) : value = null, - type = CSSLengthType.PX; + type = CSSLengthType.PX, + _realPropertyName = _normalizeRealPropertyName(propertyName); - CSSLengthValue(this.value, this.type, [this.renderStyle, this.propertyName, this.axisType]) : calcValue = null { + CSSLengthValue(this.value, this.type, [this.renderStyle, this.propertyName, this.axisType]) + : calcValue = null, + _realPropertyName = _normalizeRealPropertyName(propertyName) { if (propertyName != null) { if (type == CSSLengthType.EM) { renderStyle!.addFontRelativeProperty(propertyName!); @@ -177,6 +181,13 @@ class CSSLengthValue { } } + static String? _normalizeRealPropertyName(String? propertyName) { + if (propertyName == null) return null; + final int separatorIndex = propertyName.indexOf('_'); + if (separatorIndex <= 0) return propertyName; + return propertyName.substring(0, separatorIndex); + } + bool isViewportSizeRelatedLength() { return type == CSSLengthType.VH || type == CSSLengthType.VW; } @@ -323,24 +334,32 @@ class CSSLengthValue { return _computedValue!; } + final RenderStyle? currentRenderStyle = renderStyle; + final String? currentPropertyName = propertyName; + // Use cached value if type is not percentage which may needs 2 layout passes to resolve the // final computed value. - if (renderStyle?.hasRenderBox() == true && - propertyName != null && + if (currentRenderStyle?.hasRenderBox() == true && + currentPropertyName != null && type != CSSLengthType.PERCENTAGE) { - double? cachedValue = getCachedComputedValue(renderStyle!, propertyName!); + double? cachedValue = getCachedComputedValue(currentRenderStyle!, currentPropertyName); if (cachedValue != null) { return cachedValue; } } - final realPropertyName = propertyName?.split('_').first ?? propertyName; + RenderStyle? attachedParentRenderStyle; + RenderStyle? getAttachedParentRenderStyle() { + return attachedParentRenderStyle ??= currentRenderStyle?.getAttachedRenderParentRenderStyle(); + } + + final String? realPropertyName = _realPropertyName; switch (type) { case CSSLengthType.PX: _computedValue = value; break; case CSSLengthType.RPX: - FlutterView window = renderStyle!.currentFlutterView; + FlutterView window = currentRenderStyle!.currentFlutterView; _computedValue = value! / 750.0 * window.physicalSize.width / window.devicePixelRatio; break; case CSSLengthType.EM: @@ -348,13 +367,14 @@ class CSSLengthValue { // and font size of the element itself, in the case of other properties like width. if (realPropertyName == FONT_SIZE) { // If root element set fontSize as em unit. - if (renderStyle!.getAttachedRenderParentRenderStyle() == null) { + final RenderStyle? parentRenderStyle = getAttachedParentRenderStyle(); + if (parentRenderStyle == null) { _computedValue = value! * 16; } else { - _computedValue = value! * renderStyle!.getAttachedRenderParentRenderStyle()!.fontSize.computedValue; + _computedValue = value! * parentRenderStyle.fontSize.computedValue; } } else { - _computedValue = value! * renderStyle!.fontSize.computedValue; + _computedValue = value! * currentRenderStyle!.fontSize.computedValue; } break; case CSSLengthType.EX: @@ -363,13 +383,14 @@ class CSSLengthValue { // font-size are relative to the inherited font-size). double baseEmPx; if (realPropertyName == FONT_SIZE) { - if (renderStyle!.getAttachedRenderParentRenderStyle() == null) { + final RenderStyle? parentRenderStyle = getAttachedParentRenderStyle(); + if (parentRenderStyle == null) { baseEmPx = 16; // default root font size baseline } else { - baseEmPx = renderStyle!.getAttachedRenderParentRenderStyle()!.fontSize.computedValue; + baseEmPx = parentRenderStyle.fontSize.computedValue; } } else { - baseEmPx = renderStyle!.fontSize.computedValue; + baseEmPx = currentRenderStyle!.fontSize.computedValue; } _computedValue = value! * (baseEmPx * _exToEmFallbackRatio); break; @@ -380,9 +401,9 @@ class CSSLengthValue { // Avoid recursion when resolving `font-size` in terms of `ch` by measuring against the // inherited font style instead of the element's own (yet-to-be-computed) font size. - RenderStyle? metricBaseStyle = renderStyle; + RenderStyle? metricBaseStyle = currentRenderStyle; if (realPropertyName == FONT_SIZE) { - metricBaseStyle = renderStyle!.getAttachedRenderParentRenderStyle(); + metricBaseStyle = getAttachedParentRenderStyle(); } double baseEmPx; @@ -393,7 +414,7 @@ class CSSLengthValue { baseEmPx = metricBaseStyle.fontSize.computedValue; } } else { - baseEmPx = renderStyle!.fontSize.computedValue; + baseEmPx = currentRenderStyle!.fontSize.computedValue; } oneChPx = baseEmPx * _chToEmFallbackRatio; @@ -416,36 +437,37 @@ class CSSLengthValue { break; case CSSLengthType.REM: // If root element set fontSize as rem unit. - if (renderStyle!.getAttachedRenderParentRenderStyle() == null) { + if (getAttachedParentRenderStyle() == null) { _computedValue = value! * 16; } else { // Font rem is calculated against the root element's font size. - _computedValue = value! * renderStyle!.rootFontSize; + _computedValue = value! * currentRenderStyle!.rootFontSize; } break; case CSSLengthType.VH: - _computedValue = value! * (renderStyle!.getCurrentViewportBox()?.boxSize ?? renderStyle!.viewportSize).height; + _computedValue = + value! * (currentRenderStyle!.getCurrentViewportBox()?.boxSize ?? currentRenderStyle.viewportSize).height; break; case CSSLengthType.VW: - _computedValue = value! * (renderStyle!.getCurrentViewportBox()?.boxSize ?? renderStyle!.viewportSize).width; + _computedValue = + value! * (currentRenderStyle!.getCurrentViewportBox()?.boxSize ?? currentRenderStyle.viewportSize).width; break; // 1% of viewport's smaller (vw or vh) dimension. // If the height of the viewport is less than its width, 1vmin will be equivalent to 1vh. // If the width of the viewport is less than it’s height, 1vmin is equvialent to 1vw. case CSSLengthType.VMIN: - _computedValue = value! * renderStyle!.viewportSize.shortestSide; + _computedValue = value! * currentRenderStyle!.viewportSize.shortestSide; break; case CSSLengthType.VMAX: - _computedValue = value! * renderStyle!.viewportSize.longestSide; + _computedValue = value! * currentRenderStyle!.viewportSize.longestSide; break; case CSSLengthType.PERCENTAGE: - CSSPositionType positionType = renderStyle!.position; + CSSPositionType positionType = currentRenderStyle!.position; bool isPositioned = positionType == CSSPositionType.absolute || positionType == CSSPositionType.fixed; - RenderStyle? currentRenderStyle = renderStyle; RenderStyle? parentRenderStyle = isPositioned - ? currentRenderStyle?.target.getContainingBlockElement()?.renderStyle - : currentRenderStyle?.getAttachedRenderParentRenderStyle(); + ? currentRenderStyle.target.getContainingBlockElement()?.renderStyle + : getAttachedParentRenderStyle(); // Should access the renderStyle of renderBoxModel parent but not renderStyle parent // cause the element of renderStyle parent may not equal to containing block. @@ -464,7 +486,7 @@ class CSSLengthValue { if (currentLayoutBox != null && identical(currentLayoutBox.renderStyle, currentRenderStyle)) { renderWidgetElementChild = currentLayoutBox.findWidgetElementChild(); } - renderWidgetElementChild ??= currentRenderStyle?.target.attachedRenderer?.findWidgetElementChild(); + renderWidgetElementChild ??= currentRenderStyle.target.attachedRenderer?.findWidgetElementChild(); bool shouldInheritRenderWidgetElementConstraintsWidth = parentRenderStyle?.isSelfRenderWidget() == true && renderWidgetElementChild != null; double? parentWidgetConstraintWidth; @@ -476,7 +498,7 @@ class CSSLengthValue { renderWidgetElementChild != null && renderWidgetElementChild.effectiveChildConstraints.maxHeight.isFinite && renderWidgetElementChild.effectiveChildConstraints.maxHeight != - currentRenderStyle!.target.ownerView.currentViewport!.boxSize!.height; + currentRenderStyle.target.ownerView.currentViewport!.boxSize!.height; parentWidgetConstraintHeight = renderWidgetElementChild?.effectiveChildConstraints.maxHeight; } catch (_) {} @@ -541,10 +563,11 @@ class CSSLengthValue { switch (realPropertyName) { case FONT_SIZE: // Relative to the parent font size. - if (renderStyle!.getAttachedRenderParentRenderStyle() == null) { + final RenderStyle? parentFontRenderStyle = getAttachedParentRenderStyle(); + if (parentFontRenderStyle == null) { _computedValue = value! * 16; } else { - _computedValue = value! * renderStyle!.getAttachedRenderParentRenderStyle()!.fontSize.computedValue; + _computedValue = value! * parentFontRenderStyle.fontSize.computedValue; } break; case TEXT_INDENT: @@ -562,17 +585,17 @@ class CSSLengthValue { if (rbox != null && rbox.hasSize && rbox.constraints.maxWidth.isFinite) { _computedValue = value! * rbox.constraints.maxWidth; } else { - _computedValue = value! * renderStyle!.viewportSize.width; + _computedValue = value! * currentRenderStyle.viewportSize.width; } } } else { // Root-level: resolve against viewport. - _computedValue = value! * renderStyle!.viewportSize.width; + _computedValue = value! * currentRenderStyle.viewportSize.width; } break; case LINE_HEIGHT: // Relative to the font size of the element itself. - _computedValue = value! * renderStyle!.fontSize.computedValue; + _computedValue = value! * currentRenderStyle.fontSize.computedValue; break; case WIDTH: case MIN_WIDTH: @@ -727,7 +750,7 @@ class CSSLengthValue { case FLEX_BASIS: // Flex-basis computation is called in RenderFlexLayout which // will ensure parent exists. - RenderStyle? parentRenderStyle = renderStyle!.getAttachedRenderParentRenderStyle(); + RenderStyle? parentRenderStyle = getAttachedParentRenderStyle(); if (parentRenderStyle == null) { _computedValue = 0; break; @@ -977,8 +1000,8 @@ class CSSLengthValue { } // Cache computed value. - if (renderStyle?.hasRenderBox() == true && propertyName != null && type != CSSLengthType.PERCENTAGE) { - cacheComputedValue(renderStyle!, propertyName!, _computedValue!); + if (currentRenderStyle?.hasRenderBox() == true && currentPropertyName != null && type != CSSLengthType.PERCENTAGE) { + cacheComputedValue(currentRenderStyle!, currentPropertyName, _computedValue!); } return _computedValue!; } diff --git a/webf/lib/src/dom/element.dart b/webf/lib/src/dom/element.dart index 01898a4782..4b47fd0182 100644 --- a/webf/lib/src/dom/element.dart +++ b/webf/lib/src/dom/element.dart @@ -1961,6 +1961,12 @@ abstract class Element extends ContainerNode if (element.children.isNotEmpty) { for (final Element child in element.children) { if (!child.renderStyle.hasColor) { + final String specifiedColor = + child.style.getPropertyValue(COLOR).trim().toLowerCase(); + if (specifiedColor == CURRENT_COLOR) { + child.renderStyle.markNeedsIntrinsicMeasurement('currentColor'); + child.renderStyle.markNeedsLayout(); + } _updateColorRelativePropertyWithColor(child); } } diff --git a/webf/lib/src/dom/text_node.dart b/webf/lib/src/dom/text_node.dart index dbd4af5613..3b514e2e3f 100644 --- a/webf/lib/src/dom/text_node.dart +++ b/webf/lib/src/dom/text_node.dart @@ -33,6 +33,14 @@ class TextNode extends CharacterData { String get data => _data; + void markTextStyleNeedsLayout() { + for (final element in _attachedFlutterWidgetElements) { + if (!element.mounted) continue; + element.renderObject.markTextStyleNeedsLayout(); + element.markNeedsBuild(); + } + } + set data(String newData) { String oldData = data; if (oldData == newData) return; diff --git a/webf/lib/src/rendering/event_listener.dart b/webf/lib/src/rendering/event_listener.dart index 9e5405f254..96038ac3d8 100644 --- a/webf/lib/src/rendering/event_listener.dart +++ b/webf/lib/src/rendering/event_listener.dart @@ -209,12 +209,31 @@ class RenderEventListener extends RenderBoxModel @override void calculateBaseline() { - double? childBase = child?.getDistanceToBaseline(TextBaseline.alphabetic); - // Convert child's local baseline to this wrapper's coordinate system - if (childBase != null && child is RenderBox) { - final BoxParentData pd = (child as RenderBox).parentData as BoxParentData; + final RenderBox? currentChild = child; + double? childBase; + + if (currentChild is RenderBoxModel) { + childBase = + currentChild.computeCssFirstBaselineOf(TextBaseline.alphabetic); + } else if (currentChild is RenderPositionPlaceholder) { + childBase = currentChild.positioned + ?.computeCssFirstBaselineOf(TextBaseline.alphabetic); + } + + if (childBase == null && currentChild != null) { + if (!currentChild.attached) { + childBase = currentChild.hasSize ? currentChild.size.height : null; + } else { + childBase = currentChild.getDistanceToBaseline(TextBaseline.alphabetic); + } + } + + // Convert child's local baseline to this wrapper's coordinate system. + if (childBase != null && currentChild is RenderBox) { + final BoxParentData pd = currentChild.parentData as BoxParentData; childBase += pd.offset.dy; } + setCssBaselines(first: childBase, last: childBase); } diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index 16c505a100..2f1ce0c006 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -103,6 +103,7 @@ enum _FlexAnonymousMetricsMissReason { subtreeIntrinsicDirty, missingCacheEntry, constraintsMismatch, + reusableStateMismatch, } String _flexAnonymousMetricsMissReasonLabel( @@ -118,6 +119,8 @@ String _flexAnonymousMetricsMissReasonLabel( return 'missingCacheEntry'; case _FlexAnonymousMetricsMissReason.constraintsMismatch: return 'constraintsMismatch'; + case _FlexAnonymousMetricsMissReason.reusableStateMismatch: + return 'reusableStateMismatch'; } } @@ -406,11 +409,59 @@ class _FlexIntrinsicMeasurementCacheEntry { required this.constraints, required this.size, required this.intrinsicMainSize, + this.reusableStateSignature, }); final BoxConstraints constraints; final Size size; final double intrinsicMainSize; + final int? reusableStateSignature; +} + +class _FlexIntrinsicMeasurementCacheBucket { + static const int maxEntries = 6; + + final List<_FlexIntrinsicMeasurementCacheEntry> entries = + <_FlexIntrinsicMeasurementCacheEntry>[]; + + _FlexIntrinsicMeasurementCacheEntry? lookupLatest( + BoxConstraints constraints, + ) { + for (final _FlexIntrinsicMeasurementCacheEntry entry in entries) { + if (entry.constraints == constraints) { + return entry; + } + } + return null; + } + + _FlexIntrinsicMeasurementCacheEntry? lookupReusable( + BoxConstraints constraints, + int? reusableStateSignature, + ) { + if (reusableStateSignature == null) { + return null; + } + for (final _FlexIntrinsicMeasurementCacheEntry entry in entries) { + if (entry.constraints == constraints && + entry.reusableStateSignature == reusableStateSignature) { + return entry; + } + } + return null; + } + + void store(_FlexIntrinsicMeasurementCacheEntry entry) { + entries.removeWhere( + (_FlexIntrinsicMeasurementCacheEntry existing) => + existing.constraints == entry.constraints && + existing.reusableStateSignature == entry.reusableStateSignature, + ); + entries.insert(0, entry); + if (entries.length > maxEntries) { + entries.removeRange(maxEntries, entries.length); + } + } } class _FlexIntrinsicMeasurementLookupResult { @@ -743,6 +794,17 @@ class RenderFlexLayout extends RenderLayoutBox { addAll(children); } + @override + void markNeedsLayout() { + super.markNeedsLayout(); + + if (relayoutParentOnSizeChange == null && + lastLaidOutAsRelayoutBoundary && + parent != null) { + parent!.markNeedsLayout(); + } + } + double _intrinsicPaddingBorderHorizontal() { return renderStyle.paddingLeft.computedValue + renderStyle.paddingRight.computedValue + @@ -939,8 +1001,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Cache original constraints of children on the first layout. Expando _childrenOldConstraints = Expando('childrenOldConstraints'); - Expando<_FlexIntrinsicMeasurementCacheEntry> _childrenIntrinsicMeasureCache = - Expando<_FlexIntrinsicMeasurementCacheEntry>('childrenIntrinsicMeasureCache'); + Expando<_FlexIntrinsicMeasurementCacheBucket> _childrenIntrinsicMeasureCache = + Expando<_FlexIntrinsicMeasurementCacheBucket>('childrenIntrinsicMeasureCache'); Expando _childrenRequirePostMeasureLayout = Expando('childrenRequirePostMeasureLayout'); Expando? _transientChildSizeOverrides; @@ -957,7 +1019,7 @@ class RenderFlexLayout extends RenderLayoutBox { _childrenIntrinsicMainSizes = Expando('childrenIntrinsicMainSizes'); _childrenOldConstraints = Expando('childrenOldConstraints'); _childrenIntrinsicMeasureCache = - Expando<_FlexIntrinsicMeasurementCacheEntry>('childrenIntrinsicMeasureCache'); + Expando<_FlexIntrinsicMeasurementCacheBucket>('childrenIntrinsicMeasureCache'); _childrenRequirePostMeasureLayout = Expando('childrenRequirePostMeasureLayout'); _transientChildSizeOverrides = null; @@ -2346,6 +2408,140 @@ class RenderFlexLayout extends RenderLayoutBox { } } + @pragma('vm:prefer-inline') + int _hashReusableIntrinsicMeasurementState(int hash, int value) { + hash = 0x1fffffff & (hash + value); + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + @pragma('vm:prefer-inline') + int _finishReusableIntrinsicMeasurementState(int hash) { + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + hash ^= (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } + + @pragma('vm:prefer-inline') + int _quantizeReusableIntrinsicMeasurementDouble(double value) { + if (!value.isFinite) { + if (value.isNaN) return 0x1ffffffe; + return value.isNegative ? -0x1ffffffe : 0x1ffffffd; + } + return (value * 100).round(); + } + + int? _computeReusableIntrinsicMeasurementStateSignature( + RenderBox child, + BoxConstraints childConstraints, { + RenderFlowLayout? flowChild, + }) { + final RenderFlowLayout? effectiveFlowChild = + flowChild ?? + _getCacheableIntrinsicMeasureFlowChild( + child, + allowAnonymous: true, + ); + if (effectiveFlowChild == null) { + return null; + } + + int hash = 0; + final CSSRenderStyle style = effectiveFlowChild.renderStyle; + hash = _hashReusableIntrinsicMeasurementState(hash, effectiveFlowChild.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.minWidth), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.maxWidth), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.minHeight), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.maxHeight), + ); + hash = _hashReusableIntrinsicMeasurementState(hash, style.display.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.position.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.whiteSpace.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.wordBreak.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.textAlign.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.fontStyle.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.fontWeight.value); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(style.fontSize.computedValue), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(style.lineHeight.computedValue), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(style.textIndent.computedValue), + ); + hash = _hashReusableIntrinsicMeasurementState(hash, style.width.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.height.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.minWidth.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.maxWidth.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.minHeight.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.maxHeight.type.hashCode); + if (style.width.isNotAuto) { + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(style.width.computedValue), + ); + } + if (style.height.isNotAuto) { + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(style.height.computedValue), + ); + } + if (style.minWidth.isNotAuto) { + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(style.minWidth.computedValue), + ); + } + if (!style.maxWidth.isNone) { + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(style.maxWidth.computedValue), + ); + } + if (style.minHeight.isNotAuto) { + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(style.minHeight.computedValue), + ); + } + if (!style.maxHeight.isNone) { + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(style.maxHeight.computedValue), + ); + } + if (style.flexBasis != null) { + hash = _hashReusableIntrinsicMeasurementState( + hash, + style.flexBasis!.type.hashCode, + ); + } + final InlineFormattingContext? ifc = effectiveFlowChild.inlineFormattingContext; + if (ifc != null) { + hash = _hashReusableIntrinsicMeasurementState( + hash, + ifc.layoutReuseSignature(childConstraints), + ); + } + return _finishReusableIntrinsicMeasurementState(hash); + } + _FlexIntrinsicMeasurementLookupResult _lookupReusableIntrinsicMeasurement( RenderBox child, BoxConstraints childConstraints, @@ -2356,15 +2552,14 @@ class RenderFlexLayout extends RenderLayoutBox { if (!allowAnonymous) { return const _FlexIntrinsicMeasurementLookupResult(); } + if (_getMainAxisGap() > 0) { + return const _FlexIntrinsicMeasurementLookupResult(); + } final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( child, allowAnonymous: allowAnonymous, ); - final bool allowMetricsOnlyReuse = - flowChild != null && - _isMetricsOnlyIntrinsicMeasureFlowChild(flowChild); - if ((!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) && - !allowMetricsOnlyReuse) { + if (!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) { return const _FlexIntrinsicMeasurementLookupResult(); } if (_hasBaselineAlignmentForChild(child)) { @@ -2373,31 +2568,51 @@ class RenderFlexLayout extends RenderLayoutBox { 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 = + final _FlexIntrinsicMeasurementCacheBucket? cacheBucket = _childrenIntrinsicMeasureCache[child]; - if (cacheEntry == null) { + if (cacheBucket == null || cacheBucket.entries.isEmpty) { return const _FlexIntrinsicMeasurementLookupResult( missReason: _FlexAnonymousMetricsMissReason.missingCacheEntry, ); } - if (cacheEntry.constraints != childConstraints) { + final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = + cacheBucket.lookupLatest(childConstraints); + if (cacheEntry == null) { return const _FlexIntrinsicMeasurementLookupResult( missReason: _FlexAnonymousMetricsMissReason.constraintsMismatch, ); } - if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { + final bool flowNeedsRelayout = flowChild.needsRelayout; + final bool childNeedsRelayout = + child is RenderBoxModel && child.needsRelayout; + final bool subtreeIntrinsicDirty = + _subtreeHasPendingIntrinsicMeasureInvalidation(child); + if (flowNeedsRelayout || childNeedsRelayout || subtreeIntrinsicDirty) { + final int? reusableStateSignature = + _computeReusableIntrinsicMeasurementStateSignature( + child, + childConstraints, + flowChild: flowChild, + ); + final _FlexIntrinsicMeasurementCacheEntry? reusableEntry = + cacheBucket.lookupReusable(childConstraints, reusableStateSignature); + if (reusableEntry != null) { + return _FlexIntrinsicMeasurementLookupResult(entry: reusableEntry); + } + if (flowNeedsRelayout) { + return const _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.flowNeedsRelayout, + ); + } + if (childNeedsRelayout) { + return const _FlexIntrinsicMeasurementLookupResult( + missReason: _FlexAnonymousMetricsMissReason.childNeedsRelayout, + ); + } return _FlexIntrinsicMeasurementLookupResult( - missReason: _FlexAnonymousMetricsMissReason.subtreeIntrinsicDirty, + missReason: reusableStateSignature == null + ? _FlexAnonymousMetricsMissReason.subtreeIntrinsicDirty + : _FlexAnonymousMetricsMissReason.reusableStateMismatch, missDetails: _FlexAnonymousMetricsProfiler.enabled ? _describeFirstPendingIntrinsicMeasureInvalidation(child) : null, @@ -2412,15 +2627,25 @@ class RenderFlexLayout extends RenderLayoutBox { Size childSize, double intrinsicMainSize, ) { - if (_getCacheableIntrinsicMeasureFlowChild(child, allowAnonymous: true) == - null) { + final RenderFlowLayout? flowChild = + _getCacheableIntrinsicMeasureFlowChild(child, allowAnonymous: true); + if (flowChild == null) { return; } - _childrenIntrinsicMeasureCache[child] = _FlexIntrinsicMeasurementCacheEntry( + final _FlexIntrinsicMeasurementCacheBucket bucket = + _childrenIntrinsicMeasureCache[child] ?? + _FlexIntrinsicMeasurementCacheBucket(); + bucket.store(_FlexIntrinsicMeasurementCacheEntry( constraints: childConstraints, size: Size.copy(childSize), intrinsicMainSize: intrinsicMainSize, - ); + reusableStateSignature: _computeReusableIntrinsicMeasurementStateSignature( + child, + childConstraints, + flowChild: flowChild, + ), + )); + _childrenIntrinsicMeasureCache[child] = bucket; } bool _shouldRequirePostMeasureLayout(RenderBox child) { @@ -2701,7 +2926,7 @@ class RenderFlexLayout extends RenderLayoutBox { bool _canAttemptFullEarlyFastPath(List<_RunMetrics> runMetrics) { for (final _RunMetrics metrics in runMetrics) { for (final _RunChild runChild in metrics.runChildren) { - if (!runChild.child.constraints.hasTightWidth) { + if (!_hasEffectivelyTightMainAxisSize(runChild)) { _recordEarlyFastPathReject( _FlexFastPathRejectReason.childNonTightWidth, child: runChild.child, @@ -2714,6 +2939,75 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } + bool _hasEffectivelyTightMainAxisSize(_RunChild runChild) { + final BoxConstraints childConstraints = runChild.child.constraints; + if (_isHorizontalFlexDirection) { + if (childConstraints.hasTightWidth) { + return true; + } + } else if (childConstraints.hasTightHeight) { + return true; + } + + final RenderBoxModel? effectiveChild = runChild.effectiveChild; + if (effectiveChild == null) { + return false; + } + + if (runChild.usedFlexBasis != null) { + return true; + } + + final CSSLengthValue explicitMainSize = _isHorizontalFlexDirection + ? effectiveChild.renderStyle.width + : effectiveChild.renderStyle.height; + if (explicitMainSize.isAuto || explicitMainSize.isPercentage) { + return false; + } + + final double usedMainAxisSize = _isHorizontalFlexDirection + ? (effectiveChild.renderStyle.borderBoxLogicalWidth ?? + _getMainSize(runChild.child)) + : (effectiveChild.renderStyle.borderBoxLogicalHeight ?? + _getMainSize(runChild.child)); + if (!usedMainAxisSize.isFinite || usedMainAxisSize <= 0) { + return false; + } + + final double resolvedMainAxisSize = explicitMainSize.computedValue; + if (!resolvedMainAxisSize.isFinite || resolvedMainAxisSize <= 0) { + return false; + } + + return (usedMainAxisSize - resolvedMainAxisSize).abs() < 0.5; + } + + bool _canReuseEarlyRunMetrics( + List<_RunMetrics> runMetrics, { + required bool hasStretchedChildren, + }) { + if (hasStretchedChildren || + !_isHorizontalFlexDirection || + renderStyle.flexWrap != FlexWrap.nowrap) { + return false; + } + + for (final _RunMetrics metrics in runMetrics) { + for (final _RunChild runChild in metrics.runChildren) { + final RenderBoxModel? effectiveChild = runChild.effectiveChild; + if (effectiveChild == null) { + return false; + } + + if (!_hasEffectivelyTightMainAxisSize(runChild)) { + return false; + } + } + } + + return true; + } + List<_RunMetrics>? _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(List children) { if (!_isHorizontalFlexDirection) { _recordEarlyFastPathReject( @@ -2985,7 +3279,12 @@ class RenderFlexLayout extends RenderLayoutBox { return; } } - runMetrics = null; + if (!_canReuseEarlyRunMetrics( + runMetrics, + hasStretchedChildren: hasStretchedChildren, + )) { + runMetrics = null; + } } if (runMetrics == null) { @@ -4853,6 +5152,10 @@ class RenderFlexLayout extends RenderLayoutBox { child.renderStyle.display == CSSDisplay.inlineBlock || child.renderStyle.display == CSSDisplay.inlineFlex) && (child.renderStyle.isSelfRenderFlowLayout() || child.renderStyle.isSelfRenderFlexLayout()); + bool isAutoHeightLayoutContainer = + child.renderStyle.height.isAuto && + (child.renderStyle.isSelfRenderFlowLayout() || + child.renderStyle.isSelfRenderFlexLayout()); // Block-level flex items whose contents form an inline formatting context (e.g., a
with only text) // also need height to be unconstrained on the secondary pass so text can wrap after flex-shrink. // This mirrors browser behavior: first resolve the used main size, then measure cross size with auto height. @@ -4862,12 +5165,18 @@ class RenderFlexLayout extends RenderLayoutBox { // Allow dynamic height adjustment during secondary layout when width has changed and height is auto bool allowDynamicHeight = _isHorizontalFlexDirection && isSecondaryLayoutPass && - (isTextElement || isInlineElementWithText || establishesIFC) && + (isTextElement || + isInlineElementWithText || + establishesIFC || + isAutoHeightLayoutContainer) && // For non-flexed items, only allow when this is the only item on the line // so the line cross-size is content-driven. (childFlexedMainSize != null || (preserveMainAxisSize != null && lineChildrenCount == 1)) && - // Do not override stretch to a sibling's definite height when multiple items exist. - (childStretchedCrossSize == null || lineChildrenCount == 1) && + // Layout containers with auto height need a chance to grow past the + // previous stretched cross size when their own contents change. + (childStretchedCrossSize == null || + lineChildrenCount == 1 || + isAutoHeightLayoutContainer) && child.renderStyle.height.isAuto; if (allowDynamicHeight) { diff --git a/webf/lib/src/rendering/flow.dart b/webf/lib/src/rendering/flow.dart index 9c9703c743..faa57c4dff 100644 --- a/webf/lib/src/rendering/flow.dart +++ b/webf/lib/src/rendering/flow.dart @@ -753,6 +753,12 @@ class RenderFlowLayout extends RenderLayoutBox { void markNeedsLayout() { _baselineConstraintsAtLastLayout = null; super.markNeedsLayout(); + + if (relayoutParentOnSizeChange == null && + lastLaidOutAsRelayoutBoundary && + parent != null) { + parent!.markNeedsLayout(); + } } @override diff --git a/webf/lib/src/rendering/inline_formatting_context.dart b/webf/lib/src/rendering/inline_formatting_context.dart index 2f308211e2..c02608aabd 100644 --- a/webf/lib/src/rendering/inline_formatting_context.dart +++ b/webf/lib/src/rendering/inline_formatting_context.dart @@ -65,6 +65,9 @@ bool _containsInteriorWhitespace(String input) { return false; } +const bool _enableInlineProfileSections = + bool.fromEnvironment('WEBF_ENABLE_INLINE_PROFILE_SECTIONS'); + /// Manages the inline formatting context for a block container. /// Based on Blink's InlineNode. class InlineFormattingContext { @@ -77,7 +80,7 @@ class InlineFormattingContext { T _profileSection(String label, T Function() action, {Map? arguments}) { - if (kReleaseMode) { + if (kReleaseMode || !_enableInlineProfileSections) { return action(); } @@ -225,6 +228,10 @@ class InlineFormattingContext { >{}; final Map _cachedRectLineIndices = {}; List? _cachedTextRunParagraphsForReuse; + bool? _cachedAncestorHasHorizontalScroll; + static const int _resolvedLayoutPassCacheLimit = 8; + final Map _resolvedLayoutPassCache = + {}; // Public helpers for consumers outside IFC to query inline element metrics // without relying on legacy line boxes. @@ -489,6 +496,140 @@ class InlineFormattingContext { _cachedDecorationTextMetrics.clear(); _resetParagraphGeometryCaches(); _cachedTextRunParagraphsForReuse = null; + _cachedAncestorHasHorizontalScroll = null; + } + + @pragma('vm:prefer-inline') + int _hashCombineInt(int hash, int value) { + hash = 0x1fffffff & (hash + value); + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + @pragma('vm:prefer-inline') + int _hashFinishInt(int hash) { + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + hash ^= (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } + + @pragma('vm:prefer-inline') + int _quantizeDouble(double value) { + if (!value.isFinite) { + if (value.isNaN) return 0x1ffffffe; + return value.isNegative ? -0x1ffffffe : 0x1ffffffd; + } + return (value * 100).round(); + } + + int _resolvedLayoutStyleSignature(CSSRenderStyle style) { + int hash = 0; + hash = _hashCombineInt(hash, style.display.hashCode); + hash = _hashCombineInt(hash, style.direction.hashCode); + hash = _hashCombineInt(hash, style.whiteSpace.hashCode); + hash = _hashCombineInt(hash, style.wordBreak.hashCode); + hash = _hashCombineInt(hash, style.textAlign.hashCode); + hash = _hashCombineInt(hash, style.textTransform.hashCode); + hash = _hashCombineInt(hash, style.verticalAlign.hashCode); + hash = _hashCombineInt(hash, style.fontStyle.hashCode); + hash = _hashCombineInt(hash, style.fontWeight.value); + hash = _hashCombineInt(hash, _quantizeDouble(style.fontSize.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.lineHeight.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.textIndent.computedValue)); + hash = _hashCombineInt( + hash, + style.letterSpacing == null ? 0 : _quantizeDouble(style.letterSpacing!.computedValue), + ); + hash = _hashCombineInt( + hash, + style.wordSpacing == null ? 0 : _quantizeDouble(style.wordSpacing!.computedValue), + ); + hash = _hashCombineInt(hash, _quantizeDouble(style.marginLeft.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.marginRight.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.marginTop.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.marginBottom.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.paddingLeft.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.paddingRight.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.paddingTop.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.paddingBottom.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.effectiveBorderLeftWidth.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.effectiveBorderRightWidth.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.effectiveBorderTopWidth.computedValue)); + hash = _hashCombineInt(hash, _quantizeDouble(style.effectiveBorderBottomWidth.computedValue)); + return _hashFinishInt(hash); + } + + int _resolvedLayoutItemSignature(InlineItem item) { + int hash = 0; + hash = _hashCombineInt(hash, item.type.hashCode); + hash = _hashCombineInt(hash, item.startOffset); + hash = _hashCombineInt(hash, item.endOffset); + if (item.direction != null) { + hash = _hashCombineInt(hash, item.direction.hashCode); + } + final CSSRenderStyle? style = item.style; + if (style != null) { + hash = _hashCombineInt(hash, _resolvedLayoutStyleSignature(style)); + } + final RenderBoxModel? renderBox = item.renderBox; + if (renderBox != null) { + hash = _hashCombineInt(hash, renderBox.hashCode); + final Size? boxSize = renderBox.boxSize; + if (boxSize != null) { + hash = _hashCombineInt(hash, _quantizeDouble(boxSize.width)); + hash = _hashCombineInt(hash, _quantizeDouble(boxSize.height)); + } + } + return _hashFinishInt(hash); + } + + int _resolvedLayoutPassSignature(BoxConstraints constraints) { + int hash = 0; + hash = _hashCombineInt(hash, _items.length); + hash = _hashCombineInt(hash, _textContent.hashCode); + hash = _hashCombineInt(hash, _quantizeDouble(constraints.minWidth)); + hash = _hashCombineInt(hash, _quantizeDouble(constraints.maxWidth)); + hash = _hashCombineInt(hash, _quantizeDouble(constraints.minHeight)); + hash = _hashCombineInt(hash, _quantizeDouble(constraints.maxHeight)); + hash = _hashCombineInt( + hash, + _resolvedLayoutStyleSignature((container as RenderBoxModel).renderStyle), + ); + for (final InlineItem item in _items) { + hash = _hashCombineInt(hash, _resolvedLayoutItemSignature(item)); + } + return _hashFinishInt(hash); + } + + _ResolvedLayoutPassCacheEntry? _lookupResolvedLayoutPassCache(int signature) { + final _ResolvedLayoutPassCacheEntry? entry = + _resolvedLayoutPassCache.remove(signature); + if (entry != null) { + _resolvedLayoutPassCache[signature] = entry; + } + return entry; + } + + void _storeResolvedLayoutPassCache( + int signature, { + required Set forceRightExtrasOwners, + required List? textRunBaselineOffsets, + required List? atomicBaselineOffsets, + }) { + _resolvedLayoutPassCache.remove(signature); + if (_resolvedLayoutPassCache.length >= _resolvedLayoutPassCacheLimit) { + final int eldestKey = _resolvedLayoutPassCache.keys.first; + _resolvedLayoutPassCache.remove(eldestKey); + } + _resolvedLayoutPassCache[signature] = _ResolvedLayoutPassCacheEntry( + forceRightExtrasOwners: forceRightExtrasOwners.toList(growable: false), + textRunBaselineOffsets: textRunBaselineOffsets == null + ? null + : List.of(textRunBaselineOffsets, growable: false), + atomicBaselineOffsets: atomicBaselineOffsets == null + ? null + : List.of(atomicBaselineOffsets, growable: false), + ); } @pragma('vm:prefer-inline') @@ -607,6 +748,10 @@ class InlineFormattingContext { } bool _ancestorHasHorizontalScroll() { + final bool? cached = _cachedAncestorHasHorizontalScroll; + if (cached != null) { + return cached; + } RenderObject? p = container.parent; while (p != null) { if (p is RenderBoxModel) { @@ -619,6 +764,7 @@ class InlineFormattingContext { } final o = p.renderStyle.effectiveOverflowX; if (o == CSSOverflowType.scroll || o == CSSOverflowType.auto) { + _cachedAncestorHasHorizontalScroll = true; return true; } } @@ -626,6 +772,7 @@ class InlineFormattingContext { if (p is RenderWidget) break; p = p.parent; } + _cachedAncestorHasHorizontalScroll = false; return false; } @@ -1306,6 +1453,7 @@ class InlineFormattingContext { /// Mark that inline collection is needed. void setNeedsCollectInlines() { _needsCollectInlines = true; + _resolvedLayoutPassCache.clear(); // Debug: Log when recollection is triggered // print('InlineFormattingContext: setNeedsCollectInlines called'); } @@ -1320,6 +1468,11 @@ class InlineFormattingContext { } } + int layoutReuseSignature(BoxConstraints constraints) { + prepareLayout(); + return _resolvedLayoutPassSignature(constraints); + } + // Expose paragraph intrinsic widths when available. // CSS min-content width depends on white-space: // - For normal/pre-wrap: roughly the longest unbreakable segment (“word”). @@ -1695,41 +1848,64 @@ class InlineFormattingContext { try { // Prepare items if needed prepareLayout(); + final int layoutSignature = _resolvedLayoutPassSignature(constraints); + final _ResolvedLayoutPassCacheEntry? resolvedLayoutCacheEntry = + _lookupResolvedLayoutPassCache(layoutSignature); _resetBuildAndLayoutParagraphCaches(); - // Two-pass build: first lay out without right-extras placeholders to - // observe natural breaks, then re-layout with right-extras only for - // inline elements that do not fragment across lines. - _suppressAllRightExtras = true; - _forceRightExtrasOwners.clear(); - _buildAndLayoutParagraph(constraints); - // Compute baseline offsets for text-run vertical-align placeholders (top/middle/bottom) - bool needsVARebuild = - _computeTextRunBaselineOffsets() | _computeAtomicBaselineOffsets(); - - // Second pass: Only add right-extras placeholders for inline elements that - // did NOT fragment across lines in pass 1. For fragmented spans, we rely on - // per-line trailing reserves to avoid altering the chosen breaks. - _forceRightExtrasOwners.clear(); - for (final entry in _elementRanges.entries) { - final box = entry.key; - final (int sIdx, int eIdx) = entry.value; - if (eIdx <= sIdx) continue; - final styleR = box.renderStyle; - final double extraR = styleR.paddingRight.computedValue + - styleR.effectiveBorderRightWidth.computedValue + - styleR.marginRight.computedValue; - if (extraR <= 0) continue; - final rects = _paragraph!.getBoxesForRange(sIdx, eIdx); - if (rects.isEmpty) continue; - final int firstLine = _lineIndexForRect(rects.first); - final int lastLine = _lineIndexForRect(rects.last); - if (firstLine >= 0 && firstLine == lastLine) { - _forceRightExtrasOwners.add(box); - } - } - if (_forceRightExtrasOwners.isNotEmpty || needsVARebuild) { + if (resolvedLayoutCacheEntry != null) { _suppressAllRightExtras = false; + _forceRightExtrasOwners + ..clear() + ..addAll(resolvedLayoutCacheEntry.forceRightExtrasOwners); + _textRunBaselineOffsets = resolvedLayoutCacheEntry.textRunBaselineOffsets == null + ? null + : List.of(resolvedLayoutCacheEntry.textRunBaselineOffsets!); + _atomicBaselineOffsets = resolvedLayoutCacheEntry.atomicBaselineOffsets == null + ? null + : List.of(resolvedLayoutCacheEntry.atomicBaselineOffsets!); + _buildAndLayoutParagraph(constraints); + } else { + // Two-pass build: first lay out without right-extras placeholders to + // observe natural breaks, then re-layout with right-extras only for + // inline elements that do not fragment across lines. + _suppressAllRightExtras = true; + _forceRightExtrasOwners.clear(); _buildAndLayoutParagraph(constraints); + // Compute baseline offsets for text-run vertical-align placeholders (top/middle/bottom) + bool needsVARebuild = + _computeTextRunBaselineOffsets() | _computeAtomicBaselineOffsets(); + + // Second pass: Only add right-extras placeholders for inline elements that + // did NOT fragment across lines in pass 1. For fragmented spans, we rely on + // per-line trailing reserves to avoid altering the chosen breaks. + _forceRightExtrasOwners.clear(); + for (final entry in _elementRanges.entries) { + final box = entry.key; + final (int sIdx, int eIdx) = entry.value; + if (eIdx <= sIdx) continue; + final styleR = box.renderStyle; + final double extraR = styleR.paddingRight.computedValue + + styleR.effectiveBorderRightWidth.computedValue + + styleR.marginRight.computedValue; + if (extraR <= 0) continue; + final rects = _paragraph!.getBoxesForRange(sIdx, eIdx); + if (rects.isEmpty) continue; + final int firstLine = _lineIndexForRect(rects.first); + final int lastLine = _lineIndexForRect(rects.last); + if (firstLine >= 0 && firstLine == lastLine) { + _forceRightExtrasOwners.add(box); + } + } + if (_forceRightExtrasOwners.isNotEmpty || needsVARebuild) { + _suppressAllRightExtras = false; + _buildAndLayoutParagraph(constraints); + } + _storeResolvedLayoutPassCache( + layoutSignature, + forceRightExtrasOwners: _forceRightExtrasOwners, + textRunBaselineOffsets: _textRunBaselineOffsets, + atomicBaselineOffsets: _atomicBaselineOffsets, + ); // Clear offsets after they are consumed in PASS 2 _textRunBaselineOffsets = null; _atomicBaselineOffsets = null; @@ -5560,6 +5736,7 @@ class InlineFormattingContext { void dispose() { _resetBuildAndLayoutParagraphCaches(); + _resolvedLayoutPassCache.clear(); _atomicInlineItems.clear(); _items.clear(); _placeholderBoxes = const []; @@ -5782,6 +5959,18 @@ class _InlinePlaceholder { _InlinePlaceholder._(_PHKind.textRun, owner: owner); } +class _ResolvedLayoutPassCacheEntry { + const _ResolvedLayoutPassCacheEntry({ + required this.forceRightExtrasOwners, + required this.textRunBaselineOffsets, + required this.atomicBaselineOffsets, + }); + + final List forceRightExtrasOwners; + final List? textRunBaselineOffsets; + final List? atomicBaselineOffsets; +} + class _SpanPaintEntry { _SpanPaintEntry( this.box, diff --git a/webf/lib/src/rendering/text.dart b/webf/lib/src/rendering/text.dart index da1d7cb7cc..f755a76677 100644 --- a/webf/lib/src/rendering/text.dart +++ b/webf/lib/src/rendering/text.dart @@ -29,6 +29,17 @@ class RenderTextBox extends RenderBox with RenderObjectWithChildMixin _hasPendingTextLayoutUpdate = false; } + void markTextStyleNeedsLayout() { + _hasPendingTextLayoutUpdate = true; + _markAncestorSubtreeIntrinsicMeasurementUpdate(); + _markAncestorInlineCollectionNeedsUpdate(); + parent?.markNeedsLayout(); + markNeedsLayout(); + markNeedsPaint(); + _cachedSpan = null; + _textPainter = null; + } + void _markAncestorSubtreeIntrinsicMeasurementUpdate() { RenderObject? ancestor = parent; while (ancestor != null) { diff --git a/webf/lib/src/rendering/viewport.dart b/webf/lib/src/rendering/viewport.dart index 990c1ea9ad..7f2afc214c 100644 --- a/webf/lib/src/rendering/viewport.dart +++ b/webf/lib/src/rendering/viewport.dart @@ -116,7 +116,7 @@ class RenderViewportBox extends RenderBox RenderBoxModel rootRenderLayoutBox = child as RenderBoxModel; - child.layout(rootRenderLayoutBox.getConstraints().tighten(width: size.width, height: size.height)); + child.layout(rootRenderLayoutBox.getConstraints()); assert(child.parentData == childParentData); child = childParentData.nextSibling; diff --git a/webf/lib/src/rendering/widget.dart b/webf/lib/src/rendering/widget.dart index 09e8c5ffa2..954efb79c2 100644 --- a/webf/lib/src/rendering/widget.dart +++ b/webf/lib/src/rendering/widget.dart @@ -913,10 +913,29 @@ class RenderWidget extends RenderBoxModel double paddingTop = renderStyle.paddingTop.computedValue; double topInset = borderTop + paddingTop; - double? firstBaseline = - firstChild?.getDistanceToBaseline(TextBaseline.alphabetic); - double? lastBaseline = - lastChild?.getDistanceToBaseline(TextBaseline.alphabetic); + double? resolveBaseline(RenderBox? candidate, {required bool useLast}) { + if (candidate == null) return null; + + if (candidate is RenderBoxModel) { + final double? cssBaseline = useLast + ? candidate.computeCssLastBaselineOf(TextBaseline.alphabetic) + : candidate.computeCssFirstBaselineOf(TextBaseline.alphabetic); + if (cssBaseline != null) { + return cssBaseline; + } + } + + if (!candidate.attached) { + return candidate.hasSize ? candidate.size.height : null; + } + + return candidate.getDistanceToBaseline(TextBaseline.alphabetic); + } + + double? firstBaseline = resolveBaseline(firstChild, useLast: false); + double? lastBaseline = identical(lastChild, firstChild) + ? firstBaseline + : resolveBaseline(lastChild, useLast: true); if (firstBaseline != null) firstBaseline += topInset; if (lastBaseline != null) lastBaseline += topInset;