From 13664680083eafe095a8ef9ed3ad8faff6bdee53 Mon Sep 17 00:00:00 2001 From: cgqaq Date: Wed, 4 Feb 2026 02:28:02 +0800 Subject: [PATCH 1/6] fix(webf): prevent Blink FOUC and scroll asserts Blink first-paint gate (Blink enabled): - Hide newly inserted elements until their first style-sync begins to avoid a 1-frame unstyled flash when DOM insertion UICommands arrive before style UICommands. - Mark inserted Elements/DocumentFragment children in insertAdjacentNode once the document is already rendered, then reveal them in beginBlinkStyleSync and force an adapter rebuild (BlinkFirstPaintReadyReason). - Note: this is intentionally not layout-preserving (builds as SizedBox.shrink while deferred). Overflow scrolling stability: - Keep ScrollPosition metrics updates in the layout phase (required for gesture recognizer replacement). - Use WebFScrollController/ScrollPosition idle behavior that avoids goBallistic(0) on every applyNewDimensions; only corrects when outOfRange to prevent build-scheduled-during-frame asserts from indicator/physics listeners. --- webf/lib/src/css/render_style.dart | 4 ++ webf/lib/src/dom/element.dart | 43 +++++++++++ webf/lib/src/dom/element_widget_adapter.dart | 12 ++-- webf/lib/src/launcher/view_controller.dart | 24 +++++++ webf/lib/src/rendering/overflow.dart | 6 -- .../src/widget/webf_scroll_controller.dart | 71 +++++++++++++++++++ webf/lib/widget.dart | 1 + 7 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 webf/lib/src/widget/webf_scroll_controller.dart diff --git a/webf/lib/src/css/render_style.dart b/webf/lib/src/css/render_style.dart index 7e1f32c6f8..337bd8ca8f 100644 --- a/webf/lib/src/css/render_style.dart +++ b/webf/lib/src/css/render_style.dart @@ -42,6 +42,10 @@ class UpdateTransformReason extends AdapterUpdateReason {} class UpdateChildNodeUpdateReason extends AdapterUpdateReason {} +// Used by Blink style-sync to reveal elements that were temporarily hidden to +// avoid 1-frame unstyled flickers after DOM insertion. +class BlinkFirstPaintReadyReason extends AdapterUpdateReason {} + class UpdateRenderReplacedUpdateReason extends AdapterUpdateReason {} class ToRepaintBoundaryUpdateReason extends AdapterUpdateReason {} diff --git a/webf/lib/src/dom/element.dart b/webf/lib/src/dom/element.dart index de9a1ee7d9..5d76782ee1 100644 --- a/webf/lib/src/dom/element.dart +++ b/webf/lib/src/dom/element.dart @@ -96,6 +96,49 @@ abstract class Element extends ContainerNode // Default to unknown, assign by [createElement], used by inspector. String tagName = unknown; + // --------------------------------------------------------------------------- + // Blink style-sync first-paint gate + // + // When Blink is enabled, DOM insertion UICommands can arrive one frame earlier + // than the style-sync UICommands for the inserted element, causing a brief + // unstyled flash. We gate the first paint for newly inserted elements by + // hiding them from widget building until their first Blink style-sync starts. + bool _blinkDeferFirstPaint = false; + bool _blinkHasSeenStyleSync = false; + + bool get blinkDeferFirstPaint => _blinkDeferFirstPaint; + bool get blinkHasSeenStyleSync => _blinkHasSeenStyleSync; + + void markBlinkDeferFirstPaint() { + if (_blinkHasSeenStyleSync) return; + if (_blinkDeferFirstPaint) return; + _blinkDeferFirstPaint = true; + } + + void notifyBlinkStyleSyncStarted() { + final bool wasDeferred = _blinkDeferFirstPaint; + _blinkHasSeenStyleSync = true; + if (!wasDeferred) return; + + _blinkDeferFirstPaint = false; + + // If we were previously hidden, there may be no paired render objects yet + // (because the adapter built a SizedBox.shrink). Force the adapter state to + // rebuild so the element can create its render tree in the next frame. + final reason = BlinkFirstPaintReadyReason(); + if (renderStyle.hasRenderBox()) { + renderStyle.requestWidgetToRebuild(reason); + } else { + forEachState((state) { + if (state is WebFElementWidgetState) { + state.requestForChildNodeUpdate(reason); + } else if (state is WebFReplacedElementWidgetState) { + state.requestForChildNodeUpdate(reason); + } + }); + } + } + final Set _intersectionObserverList = {}; List _thresholds = [0.0]; diff --git a/webf/lib/src/dom/element_widget_adapter.dart b/webf/lib/src/dom/element_widget_adapter.dart index 7ce7736876..ce34a757e9 100644 --- a/webf/lib/src/dom/element_widget_adapter.dart +++ b/webf/lib/src/dom/element_widget_adapter.dart @@ -221,7 +221,9 @@ class WebFElementWidgetState extends flutter.State with flutt WebFState? webFState; WebFRouterViewState? routerViewState; - if (this is WidgetElement || webFElement.renderStyle.display == CSSDisplay.none) { + if (webFElement.blinkDeferFirstPaint || + this is WidgetElement || + webFElement.renderStyle.display == CSSDisplay.none) { return flutter.SizedBox.shrink(); } @@ -316,7 +318,7 @@ class WebFElementWidgetState extends flutter.State with flutt if (overflowX == CSSOverflowType.scroll || overflowX == CSSOverflowType.auto || overflowX == CSSOverflowType.hidden) { - controllerX = _scrollControllerX ??= flutter.ScrollController(); + controllerX = _scrollControllerX ??= WebFScrollController(); final bool xScrollable = overflowX != CSSOverflowType.hidden; final bool isRTL = webFElement.renderStyle.direction == TextDirection.rtl; scrollableX = LayoutBoxWrapper( @@ -350,7 +352,7 @@ class WebFElementWidgetState extends flutter.State with flutt if (overflowY == CSSOverflowType.scroll || overflowY == CSSOverflowType.auto || overflowY == CSSOverflowType.hidden) { - final controllerY = _scrollControllerY ??= flutter.ScrollController(); + final controllerY = _scrollControllerY ??= WebFScrollController(); final bool yScrollable = overflowY != CSSOverflowType.hidden; final bool xScrollable = overflowX != CSSOverflowType.hidden; widget = LayoutBoxWrapper( @@ -369,7 +371,7 @@ class WebFElementWidgetState extends flutter.State with flutt if (scrollableX != null) { final bool isRTL = webFElement.renderStyle.direction == TextDirection.rtl; final controllerXForNested = - controllerX ?? (_scrollControllerX ??= flutter.ScrollController()); + controllerX ?? (_scrollControllerX ??= WebFScrollController()); return NestedScrollCoordinator( axis: flutter.Axis.horizontal, controller: controllerXForNested, @@ -803,7 +805,7 @@ class WebFReplacedElementWidgetState extends flutter.State{}; + // Reveal elements that were temporarily hidden to avoid 1-frame unstyled + // flickers after DOM insertion (Blink mode). + if (!hasBindingObject(selfPtr)) return; + final Node? target = getBindingObject(selfPtr); + if (target is Element) { + target.notifyBlinkStyleSyncStarted(); + } } void recordBlinkStyleSyncProperty(Pointer selfPtr, String property) { diff --git a/webf/lib/src/rendering/overflow.dart b/webf/lib/src/rendering/overflow.dart index 429a8dcede..45efc5939f 100644 --- a/webf/lib/src/rendering/overflow.dart +++ b/webf/lib/src/rendering/overflow.dart @@ -191,13 +191,7 @@ mixin RenderOverflowMixin on RenderBoxModelBase { _viewportSize = viewportSize; if (_scrollOffsetX != null) { _setUpScrollX(); - // Do not auto-jump scroll position for RTL containers. - // Per CSS/UA expectations, initial scroll position is the start edge - // of the scroll range, and user agent should not forcibly move it to - // the visual right edge for RTL. Keeping 0 preserves expected behavior - // for cases where overflow content lies entirely to the right. } - if (_scrollOffsetY != null) { _setUpScrollY(); } diff --git a/webf/lib/src/widget/webf_scroll_controller.dart b/webf/lib/src/widget/webf_scroll_controller.dart new file mode 100644 index 0000000000..2343b89f98 --- /dev/null +++ b/webf/lib/src/widget/webf_scroll_controller.dart @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024-present The OpenWebF Company. All rights reserved. + * Licensed under GNU GPL with Enterprise exception. + */ + +import 'package:flutter/widgets.dart'; + +/// A [ScrollController] used by WebF overflow scroll containers. +/// +/// This controller creates a custom [ScrollPosition] that avoids triggering a +/// ballistic activity during the layout phase when scroll metrics change but +/// the position is already in range. This prevents some third-party physics +/// implementations from calling setState during layout via +/// `createBallisticSimulation()`. +class WebFScrollController extends ScrollController { + WebFScrollController({ + super.initialScrollOffset, + super.keepScrollOffset, + super.debugLabel, + super.onAttach, + super.onDetach, + }); + + @override + ScrollPosition createScrollPosition( + ScrollPhysics physics, + ScrollContext context, + ScrollPosition? oldPosition, + ) { + return WebFScrollPositionWithSingleContext( + physics: physics, + context: context, + initialPixels: initialScrollOffset, + keepScrollOffset: keepScrollOffset, + oldPosition: oldPosition, + debugLabel: debugLabel, + ); + } +} + +class WebFScrollPositionWithSingleContext + extends ScrollPositionWithSingleContext { + WebFScrollPositionWithSingleContext({ + required super.physics, + required super.context, + double? initialPixels = 0.0, + super.keepScrollOffset, + super.oldPosition, + super.debugLabel, + }) : super(initialPixels: initialPixels); + + @override + void goIdle() { + beginActivity(_WebFIdleScrollActivity(this)); + } +} + +class _WebFIdleScrollActivity extends IdleScrollActivity { + _WebFIdleScrollActivity(super.delegate); + + @override + void applyNewDimensions() { + final ScrollActivityDelegate activityDelegate = delegate; + final ScrollMetrics? metrics = activityDelegate is ScrollMetrics + ? (activityDelegate as ScrollMetrics) + : null; + if (metrics != null && metrics.outOfRange) { + activityDelegate.goBallistic(0.0); + } + } +} diff --git a/webf/lib/widget.dart b/webf/lib/widget.dart index 933dc8ef21..61b45a6b07 100644 --- a/webf/lib/widget.dart +++ b/webf/lib/widget.dart @@ -17,3 +17,4 @@ export 'src/widget/router_view.dart'; export 'src/widget/contentful_widget_detector.dart'; export 'src/widget/nested_scroll_forwarder.dart'; export 'src/widget/ensure_visible.dart'; +export 'src/widget/webf_scroll_controller.dart'; From 71732b30920d6e915ac26fb2e85055f3e6125787 Mon Sep 17 00:00:00 2001 From: cgqaq Date: Wed, 4 Feb 2026 23:47:33 +0800 Subject: [PATCH 2/6] perf(dom): prevent style invalidation on unchanged attribute updates Avoid unnecessary style invalidation work for re-applied attributes with unchanged values, optimizing performance for frameworks like React. --- bridge/core/dom/element.cc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bridge/core/dom/element.cc b/bridge/core/dom/element.cc index fef40f2c4e..57380e8e5b 100644 --- a/bridge/core/dom/element.cc +++ b/bridge/core/dom/element.cc @@ -1363,6 +1363,14 @@ void Element::AttributeChanged(const AttributeModificationParams& params) { } const AtomicString& name = params.name; + const bool attribute_value_changed = params.old_value != params.new_value; + // Many frameworks (e.g. React) re-apply attributes even when their value + // is unchanged. Avoid forcing expensive style invalidation work when there + // is no effective attribute change. + if (!attribute_value_changed) { + return; + } + if (name == CheckedAttrName()) { // HTML boolean attributes are true by presence, even when the value is empty. checked_state_ = !params.new_value.IsNull(); From d2c887ce98390e928f0dfee0f9509c77d334a4e0 Mon Sep 17 00:00:00 2001 From: cgqaq Date: Fri, 6 Feb 2026 19:45:50 +0800 Subject: [PATCH 3/6] fix(webf): honor blink first-paint defer for WidgetElement insertAdjacentNode marks newly inserted elements with markBlinkDeferFirstPaint in Blink mode to prevent one-frame unstyled flashes before style-sync arrives.\n\nWidgetElement nodes were previously missing this protection because they render through WebFWidgetElementAdapterState.build, which only gated on display:none. As a result, widget-backed custom/native elements could still paint once before their first style-sync, leaving a FOUC gap even after the first-paint defer changes.\n\nThis commit aligns the widget adapter path with the other element adapters by adding a blinkDeferFirstPaint guard in WebFWidgetElementAdapterState.build. When defer is active, the adapter returns SizedBox.shrink() until style-sync starts and clears the defer flag, at which point the element rebuilds and paints normally.\n\nBehavior impact:\n- Blink mode: widget-backed elements now respect first-paint defer and avoid transient unstyled paint.\n- Non-Blink mode: unchanged behavior.\n- Existing display:none handling remains unchanged. --- webf/lib/src/widget/widget_element.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webf/lib/src/widget/widget_element.dart b/webf/lib/src/widget/widget_element.dart index ad0ccb75f3..01a4ab10f2 100644 --- a/webf/lib/src/widget/widget_element.dart +++ b/webf/lib/src/widget/widget_element.dart @@ -343,7 +343,7 @@ class WebFWidgetElementAdapterState extends dom.WebFElementWidgetState { Widget build(BuildContext context) { super.build(context); - if (widgetElement.renderStyle.display == CSSDisplay.none) { + if (widgetElement.blinkDeferFirstPaint || widgetElement.renderStyle.display == CSSDisplay.none) { return SizedBox.shrink(); } From 3d8709c8d277f2d88381f3dc60dbfb039f47e7a5 Mon Sep 17 00:00:00 2001 From: cgqaq Date: Fri, 6 Feb 2026 21:15:10 +0800 Subject: [PATCH 4/6] test(css): add integration tests for calc() in flex shorthand Add snapshot-based integration tests to verify that calc() expressions are accepted as the flex-basis component in the flex shorthand property. Test cases: - calc(100% - 100px) as flex-basis with percentage subtraction - calc(150px + 50px) and calc(150px - 50px) with px arithmetic - calc(50% - 10px) combined with flex-grow and flex-shrink - calc(100% - 100px) in flex-direction: column All tests include visual regression snapshots confirming that flex items are sized correctly according to their calc() basis values. Co-Authored-By: Claude Opus 4.6 --- .../flex-shorthand-calc.ts.569527c71.png | Bin 0 -> 2377 bytes .../flex-shorthand-calc.ts.72d5760d1.png | Bin 0 -> 2371 bytes .../flex-shorthand-calc.ts.e52e92a31.png | Bin 0 -> 2371 bytes .../flex-shorthand-calc.ts.f95ec0611.png | Bin 0 -> 2373 bytes .../css/css-flexbox/flex-shorthand-calc.ts | 158 ++++++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 integration_tests/snapshots/css/css-flexbox/flex-shorthand-calc.ts.569527c71.png create mode 100644 integration_tests/snapshots/css/css-flexbox/flex-shorthand-calc.ts.72d5760d1.png create mode 100644 integration_tests/snapshots/css/css-flexbox/flex-shorthand-calc.ts.e52e92a31.png create mode 100644 integration_tests/snapshots/css/css-flexbox/flex-shorthand-calc.ts.f95ec0611.png create mode 100644 integration_tests/specs/css/css-flexbox/flex-shorthand-calc.ts diff --git a/integration_tests/snapshots/css/css-flexbox/flex-shorthand-calc.ts.569527c71.png b/integration_tests/snapshots/css/css-flexbox/flex-shorthand-calc.ts.569527c71.png new file mode 100644 index 0000000000000000000000000000000000000000..adc371316253256fa27d7177a6b82cc44069409b GIT binary patch literal 2377 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_qALo-U3d6?5KRbL3(QIuPuA$$ zQ<($TAAezBc*kktFvWq{Wt18Xf}xWNVvTn%cmMEQ>Oeogl!Dg+#zicn6t#ok)z0cF yCWi8w3k-owyaKAD)MyZlrh?IoFbK;6`KeKS-wze;x(#d&F?hQAxvXVJUX< z4B-HR8jh3>1_q9|o-U3d6?5KRJILAKz{7CRT6_PwWX50R2WN6IDSCgi$`pF literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-flexbox/flex-shorthand-calc.ts.e52e92a31.png b/integration_tests/snapshots/css/css-flexbox/flex-shorthand-calc.ts.e52e92a31.png new file mode 100644 index 0000000000000000000000000000000000000000..8c4819c79573ef6f08a9ebff7f45feabc8d0db06 GIT binary patch literal 2371 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_q9|o-U3d6?5KRJILAKz{7CRT6_PwWX50R2WN6IDSCgi$`pF literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-flexbox/flex-shorthand-calc.ts.f95ec0611.png b/integration_tests/snapshots/css/css-flexbox/flex-shorthand-calc.ts.f95ec0611.png new file mode 100644 index 0000000000000000000000000000000000000000..25c90e27001b082b3a22e06b5b16f1b00f5c67ad GIT binary patch literal 2373 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_qAzo-U3d6?5KRJILAKAi#3)@(KOVrVO=QW)Y$XZKkF5Z@sQlal0&R zPkvol*z@bm3>9h@7y_Ai1yn~Vss=&nnKL!EObyoZCJs{^m|ZwWsnH-9O$DPFVYDn5 jEe=O(1ZvfWS6JsxbP0l+XkK*AE~h literal 0 HcmV?d00001 diff --git a/integration_tests/specs/css/css-flexbox/flex-shorthand-calc.ts b/integration_tests/specs/css/css-flexbox/flex-shorthand-calc.ts new file mode 100644 index 0000000000..6313b45459 --- /dev/null +++ b/integration_tests/specs/css/css-flexbox/flex-shorthand-calc.ts @@ -0,0 +1,158 @@ +describe('flex shorthand with calc()', () => { + it('should accept calc() as flex-basis in flex shorthand', async () => { + let flexbox; + let item1; + let item2; + flexbox = createElement( + 'div', + { + style: { + display: 'flex', + width: '300px', + height: '100px', + 'background-color': 'red', + 'box-sizing': 'border-box', + }, + }, + [ + (item1 = createElement('div', { + style: { + flex: '0 1 calc(100% - 100px)', + height: '100px', + 'background-color': 'green', + 'box-sizing': 'border-box', + }, + })), + (item2 = createElement('div', { + style: { + width: '100px', + height: '100px', + 'background-color': 'blue', + 'box-sizing': 'border-box', + }, + })), + ] + ); + BODY.appendChild(flexbox); + + await snapshot(); + }); + + it('should accept calc() with px values as flex-basis', async () => { + let flexbox; + let item1; + let item2; + flexbox = createElement( + 'div', + { + style: { + display: 'flex', + width: '300px', + height: '100px', + 'background-color': 'red', + 'box-sizing': 'border-box', + }, + }, + [ + (item1 = createElement('div', { + style: { + flex: '0 0 calc(150px + 50px)', + height: '100px', + 'background-color': 'green', + 'box-sizing': 'border-box', + }, + })), + (item2 = createElement('div', { + style: { + flex: '0 0 calc(150px - 50px)', + height: '100px', + 'background-color': 'blue', + 'box-sizing': 'border-box', + }, + })), + ] + ); + BODY.appendChild(flexbox); + + await snapshot(); + }); + + it('should accept calc() with grow and shrink in flex shorthand', async () => { + let flexbox; + let item1; + let item2; + flexbox = createElement( + 'div', + { + style: { + display: 'flex', + width: '300px', + height: '100px', + 'background-color': 'red', + 'box-sizing': 'border-box', + }, + }, + [ + (item1 = createElement('div', { + style: { + flex: '1 1 calc(50% - 10px)', + height: '100px', + 'background-color': 'green', + 'box-sizing': 'border-box', + }, + })), + (item2 = createElement('div', { + style: { + flex: '1 1 calc(50% - 10px)', + height: '100px', + 'background-color': 'blue', + 'box-sizing': 'border-box', + }, + })), + ] + ); + BODY.appendChild(flexbox); + + await snapshot(); + }); + + it('should layout correctly with flex calc basis in column direction', async () => { + let flexbox; + let item1; + let item2; + flexbox = createElement( + 'div', + { + style: { + display: 'flex', + 'flex-direction': 'column', + width: '100px', + height: '300px', + 'background-color': 'red', + 'box-sizing': 'border-box', + }, + }, + [ + (item1 = createElement('div', { + style: { + flex: '0 0 calc(100% - 100px)', + width: '100px', + 'background-color': 'green', + 'box-sizing': 'border-box', + }, + })), + (item2 = createElement('div', { + style: { + width: '100px', + height: '100px', + 'background-color': 'blue', + 'box-sizing': 'border-box', + }, + })), + ] + ); + BODY.appendChild(flexbox); + + await snapshot(); + }); +}); From fb083a78e66986d3b5d7aa2fcad38a4f7990c065 Mon Sep 17 00:00:00 2001 From: cgqaq Date: Fri, 6 Feb 2026 04:15:57 +0800 Subject: [PATCH 5/6] fix(css): accept calc() in flex shorthand parsing Allow calc(...) to be used as the flex-basis component when parsing the flex shorthand. Previously, _getFlexValues rejected calc() tokens and returned null, causing otherwise valid declarations like 'flex: 0 1 calc(100% - 20px)' to be dropped. Test: not run --- webf/lib/src/css/style_property.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webf/lib/src/css/style_property.dart b/webf/lib/src/css/style_property.dart index d103158f0a..0aaef433d7 100644 --- a/webf/lib/src/css/style_property.dart +++ b/webf/lib/src/css/style_property.dart @@ -1086,12 +1086,15 @@ class CSSStyleProperty { } final bool isValueVariableFunction = CSSFunction.isFunction(value, functionName: VAR); + final bool isValueCalcFunction = CSSFunction.isFunction(value, functionName: CALC); if (grow == null && (isValueVariableFunction || CSSNumber.isNumber(value))) { grow = value; } else if (shrink == null && (isValueVariableFunction || CSSNumber.isNumber(value))) { shrink = value; } else if (basis == null && ((isValueVariableFunction || + // Accept function notations like `calc(...)` and `var(...)`. + isValueCalcFunction || CSSLength.isNonNegativeLength(value) || CSSPercentage.isPercentage(value) || value == AUTO))) { From 62721baa88e8c4bedfa3676ec96f4f6d24084aad Mon Sep 17 00:00:00 2001 From: cgqaq Date: Mon, 9 Feb 2026 12:25:14 +0800 Subject: [PATCH 6/6] fix(dom): bypass stale batch style dirties in blink mode Blink-enabled integration tests were failing for dynamic dir updates (e.g. document.documentElement.dir = 'rtl') where logical margin remapping never took effect. Root cause: - Element attribute/class/id mutation paths used DebugFlags.enableCssBatchRecalc to enqueue dirty elements via markElementStyleDirty(...). - In Blink mode, Document.updateStyleIfNeeded()/flushStyle() are intentionally skipped, so that dirty queue is not consumed. - As a result, attribute-driven style changes (including inherited direction from dir attribute) could remain stale even after frames advanced. Fix: - Introduce Element._shouldBatchRecalculateStyle: DebugFlags.enableCssBatchRecalc && !ownerDocument.ownerView.enableBlink - Use this guard for id/className/attribute set/remove batching paths. - In Blink mode these paths now recalculate immediately instead of enqueueing unreachable dirty marks. Behavioral impact: - Non-Blink mode keeps existing batching behavior unchanged. - Blink mode now applies attribute/class/id style effects deterministically, including dir-driven inherited direction and logical property remapping. Validation: - npm run integration -- specs/css/css-inline-formatting/dir-dynamic-update.ts --filter "should remap marginInlineStart when documentElement.dir changes" --enable-blink - npm run integration -- specs/css/css-inline-formatting/dir-dynamic-update.ts --filter "should remap marginInlineStart when documentElement.dir changes" Co-Authored-By: Claude Opus 4.6 --- webf/lib/src/dom/element.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/webf/lib/src/dom/element.dart b/webf/lib/src/dom/element.dart index 5d76782ee1..6cdb9b31d1 100644 --- a/webf/lib/src/dom/element.dart +++ b/webf/lib/src/dom/element.dart @@ -154,7 +154,7 @@ abstract class Element extends ContainerNode // Maintain attribute presence index for [id] selectors. // Treat any non-null assignment (including empty string) as presence=true. _updateAttrPresenceIndex(_idAttr, present: id != null); - if (DebugFlags.enableCssBatchRecalc) { + if (_shouldBatchRecalculateStyle) { ownerDocument.markElementStyleDirty(this, reason: 'batch:id'); if (oldId != id) { _markHasSelectorsDirty(); @@ -181,6 +181,9 @@ abstract class Element extends ContainerNode // The attrs. final Map attributes = {}; + bool get _shouldBatchRecalculateStyle => + DebugFlags.enableCssBatchRecalc && !ownerDocument.ownerView.enableBlink; + /// The style of the element, not inline style. late CSSStyleDeclaration style; @@ -246,7 +249,7 @@ abstract class Element extends ContainerNode _updateClassIndex(oldClasses, _classList); // Maintain attribute presence index for [class] selectors. _updateAttrPresenceIndex(_classNameAttr, present: true); - if (DebugFlags.enableCssBatchRecalc) { + if (_shouldBatchRecalculateStyle) { ownerDocument.markElementStyleDirty(this, reason: 'batch:class'); if (classChanged) { _markHasSelectorsDirty(); @@ -1538,7 +1541,7 @@ abstract class Element extends ContainerNode } } else { final isNeedRecalculate = _checkRecalculateStyle([qualifiedName]); - if (DebugFlags.enableCssBatchRecalc) { + if (_shouldBatchRecalculateStyle) { ownerDocument.markElementStyleDirty(this, reason: 'batch:attr:$qualifiedName'); } else { @@ -1582,7 +1585,7 @@ abstract class Element extends ContainerNode if (hasAttribute(qualifiedName)) { attributes.remove(qualifiedName); final isNeedRecalculate = _checkRecalculateStyle([qualifiedName]); - if (DebugFlags.enableCssBatchRecalc) { + if (_shouldBatchRecalculateStyle) { ownerDocument.markElementStyleDirty(this, reason: 'batch:remove:$qualifiedName'); } else {