Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bridge/core/dom/element.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
158 changes: 158 additions & 0 deletions integration_tests/specs/css/css-flexbox/flex-shorthand-calc.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
4 changes: 4 additions & 0 deletions webf/lib/src/css/render_style.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
3 changes: 3 additions & 0 deletions webf/lib/src/css/style_property.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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))) {
Expand Down
54 changes: 50 additions & 4 deletions webf/lib/src/dom/element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<IntersectionObserver> _intersectionObserverList = {};
List<double> _thresholds = [0.0];

Expand All @@ -111,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();
Expand All @@ -138,6 +181,9 @@ abstract class Element extends ContainerNode
// The attrs.
final Map<String, String> attributes = <String, String>{};

bool get _shouldBatchRecalculateStyle =>
DebugFlags.enableCssBatchRecalc && !ownerDocument.ownerView.enableBlink;

/// The style of the element, not inline style.
late CSSStyleDeclaration style;

Expand Down Expand Up @@ -203,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();
Expand Down Expand Up @@ -1495,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 {
Expand Down Expand Up @@ -1539,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 {
Expand Down
12 changes: 7 additions & 5 deletions webf/lib/src/dom/element_widget_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ class WebFElementWidgetState extends flutter.State<WebFElementWidget> 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();
}

Expand Down Expand Up @@ -316,7 +318,7 @@ class WebFElementWidgetState extends flutter.State<WebFElementWidget> 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(
Expand Down Expand Up @@ -350,7 +352,7 @@ class WebFElementWidgetState extends flutter.State<WebFElementWidget> 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(
Expand All @@ -369,7 +371,7 @@ class WebFElementWidgetState extends flutter.State<WebFElementWidget> 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,
Expand Down Expand Up @@ -803,7 +805,7 @@ class WebFReplacedElementWidgetState extends flutter.State<WebFReplacedElementWi
flutter.Widget build(flutter.BuildContext context) {
super.build(context);

if (webFElement.renderStyle.display == CSSDisplay.none) {
if (webFElement.blinkDeferFirstPaint || webFElement.renderStyle.display == CSSDisplay.none) {
return flutter.SizedBox.shrink();
}

Expand Down
24 changes: 24 additions & 0 deletions webf/lib/src/launcher/view_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,23 @@ class WebFViewController with Diagnosticable implements WidgetsBindingObserver {
return;
}

// In Blink mode, style-sync UICommands may arrive a frame later than DOM
// insertion commands. Hide newly inserted elements until their first style
// sync starts to avoid a 1-frame unstyled flash.
final bool shouldDeferBlinkFirstPaint =
enableBlink && (document.documentElement?.renderStyle.hasRenderBox() ?? false);
if (shouldDeferBlinkFirstPaint) {
if (newNode is Element) {
newNode.markBlinkDeferFirstPaint();
} else if (newNode is DocumentFragment) {
for (final Node child in newNode.childNodes) {
if (child is Element) {
child.markBlinkDeferFirstPaint();
}
}
}
}

Node? targetParentNode = target.parentNode;

switch (position) {
Expand Down Expand Up @@ -960,6 +977,13 @@ class WebFViewController with Diagnosticable implements WidgetsBindingObserver {
void beginBlinkStyleSync(Pointer selfPtr) {
if (!enableBlink) return;
_blinkStyleSyncUpdatedProperties[selfPtr.address] = <String>{};
// 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<Node>(selfPtr);
if (target is Element) {
target.notifyBlinkStyleSyncStarted();
}
}

void recordBlinkStyleSyncProperty(Pointer selfPtr, String property) {
Expand Down
6 changes: 0 additions & 6 deletions webf/lib/src/rendering/overflow.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Loading
Loading