Unverified Commit 98369bdd authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Introduce methods for computing the baseline location of a RenderBox without...

Introduce methods for computing the baseline location of a RenderBox without affecting the current layout (#144655)

Extracted from https://github.com/flutter/flutter/pull/138369

Introduces `RenderBox.{compute,get}DryBaseline` for computing the baseline location in `RenderBox.computeDryLayout`.
parent f704560c
...@@ -1316,7 +1316,10 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin ...@@ -1316,7 +1316,10 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
@override @override
double computeDistanceToActualBaseline(TextBaseline baseline) { double computeDistanceToActualBaseline(TextBaseline baseline) {
return _boxParentData(input!).offset.dy + (input?.computeDistanceToActualBaseline(baseline) ?? 0.0); final RenderBox? input = this.input;
return input == null
? 0.0
: _boxParentData(input).offset.dy + (input.computeDistanceToActualBaseline(baseline) ?? 0.0);
} }
// Records where the label was painted. // Records where the label was painted.
......
...@@ -1249,10 +1249,12 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ ...@@ -1249,10 +1249,12 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
} }
@override @override
double computeDistanceToActualBaseline(TextBaseline baseline) { double? computeDistanceToActualBaseline(TextBaseline baseline) {
assert(title != null); assert(title != null);
final BoxParentData parentData = title!.parentData! as BoxParentData; final BoxParentData parentData = title!.parentData! as BoxParentData;
return parentData.offset.dy + title!.getDistanceToActualBaseline(baseline)!; final BaselineOffset offset = BaselineOffset(title!.getDistanceToActualBaseline(baseline))
+ parentData.offset.dy;
return offset.offset;
} }
static double? _boxBaseline(RenderBox box, TextBaseline baseline) { static double? _boxBaseline(RenderBox box, TextBaseline baseline) {
......
...@@ -1175,11 +1175,13 @@ class _SelectToggleButtonRenderObject extends RenderShiftedBox { ...@@ -1175,11 +1175,13 @@ class _SelectToggleButtonRenderObject extends RenderShiftedBox {
} }
@override @override
double computeDistanceToActualBaseline(TextBaseline baseline) { double? computeDistanceToActualBaseline(TextBaseline baseline) {
// The baseline of this widget is the baseline of its child // The baseline of this widget is the baseline of its child
return direction == Axis.horizontal final BaselineOffset childOffset = BaselineOffset(child?.computeDistanceToActualBaseline(baseline));
? child!.computeDistanceToActualBaseline(baseline)! + borderSide.width return switch (direction) {
: child!.computeDistanceToActualBaseline(baseline)! + leadingBorderSide.width; Axis.horizontal => childOffset + borderSide.width,
Axis.vertical => childOffset + leadingBorderSide.width,
}.offset;
} }
@override @override
......
This diff is collapsed.
...@@ -2213,16 +2213,16 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge ...@@ -2213,16 +2213,16 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
@protected @protected
void debugAssertDoesMeetConstraints(); void debugAssertDoesMeetConstraints();
/// When true, debugAssertDoesMeetConstraints() is currently /// When true, a debug method ([debugAssertDoesMeetConstraints], for instance)
/// executing asserts for verifying the consistent behavior of /// is currently executing asserts for verifying the consistent behavior of
/// intrinsic dimensions methods. /// intrinsic dimensions methods.
/// ///
/// This should only be set by debugAssertDoesMeetConstraints() /// This is typically set by framework debug methods. It is read by tests to
/// implementations. It is used by tests to selectively ignore /// selectively ignore custom layout callbacks. It should not be set outside of
/// custom layout callbacks. It should not be set outside of /// intrinsic-checking debug methods, and should not be checked in release mode
/// debugAssertDoesMeetConstraints(), and should not be checked in /// (where it will always be false).
/// release mode (where it will always be false).
static bool debugCheckingIntrinsics = false; static bool debugCheckingIntrinsics = false;
bool _debugSubtreeRelayoutRootAlreadyMarkedNeedsLayout() { bool _debugSubtreeRelayoutRootAlreadyMarkedNeedsLayout() {
if (_relayoutBoundary == null) { if (_relayoutBoundary == null) {
// We don't know where our relayout boundary is yet. // We don't know where our relayout boundary is yet.
......
...@@ -700,8 +700,9 @@ class RenderIndexedStack extends RenderStack { ...@@ -700,8 +700,9 @@ class RenderIndexedStack extends RenderStack {
@override @override
void visitChildrenForSemantics(RenderObjectVisitor visitor) { void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (index != null && firstChild != null) { final RenderBox? displayedChild = _childAtIndex();
visitor(_childAtIndex()); if (displayedChild != null) {
visitor(displayedChild);
} }
} }
...@@ -715,45 +716,55 @@ class RenderIndexedStack extends RenderStack { ...@@ -715,45 +716,55 @@ class RenderIndexedStack extends RenderStack {
} }
} }
RenderBox _childAtIndex() { RenderBox? _childAtIndex() {
assert(index != null); final int? index = this.index;
if (index == null) {
return null;
}
RenderBox? child = firstChild; RenderBox? child = firstChild;
int i = 0; for (int i = 0; i < index && child != null; i += 1) {
while (child != null && i < index!) { child = childAfter(child);
final StackParentData childParentData = child.parentData! as StackParentData; }
child = childParentData.nextSibling; assert(firstChild == null || child != null);
i += 1; return child;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
final RenderBox? displayedChild = _childAtIndex();
if (displayedChild == null) {
return null;
} }
assert(i == index); final StackParentData childParentData = displayedChild.parentData! as StackParentData;
assert(child != null); final BaselineOffset offset = BaselineOffset(displayedChild.getDistanceToActualBaseline(baseline)) + childParentData.offset.dy;
return child!; return offset.offset;
} }
@override @override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
if (firstChild == null || index == null) { final RenderBox? displayedChild = _childAtIndex();
if (displayedChild == null) {
return false; return false;
} }
final RenderBox child = _childAtIndex(); final StackParentData childParentData = displayedChild.parentData! as StackParentData;
final StackParentData childParentData = child.parentData! as StackParentData;
return result.addWithPaintOffset( return result.addWithPaintOffset(
offset: childParentData.offset, offset: childParentData.offset,
position: position, position: position,
hitTest: (BoxHitTestResult result, Offset transformed) { hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset); assert(transformed == position - childParentData.offset);
return child.hitTest(result, position: transformed); return displayedChild.hitTest(result, position: transformed);
}, },
); );
} }
@override @override
void paintStack(PaintingContext context, Offset offset) { void paintStack(PaintingContext context, Offset offset) {
if (firstChild == null || index == null) { final RenderBox? displayedChild = _childAtIndex();
if (displayedChild == null) {
return; return;
} }
final RenderBox child = _childAtIndex(); final StackParentData childParentData = displayedChild.parentData! as StackParentData;
final StackParentData childParentData = child.parentData! as StackParentData; context.paintChild(displayedChild, childParentData.offset + offset);
context.paintChild(child, childParentData.offset + offset);
} }
@override @override
......
...@@ -10,11 +10,6 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -10,11 +10,6 @@ import 'package:flutter_test/flutter_test.dart';
const List<String> menuItems = <String>['one', 'two', 'three', 'four']; const List<String> menuItems = <String>['one', 'two', 'three', 'four'];
void onChanged<T>(T _) { } void onChanged<T>(T _) { }
final Type dropdownButtonType = DropdownButton<String>(
onChanged: (_) { },
items: const <DropdownMenuItem<String>>[],
).runtimeType;
Finder _iconRichText(Key iconKey) { Finder _iconRichText(Key iconKey) {
return find.descendant( return find.descendant(
of: find.byKey(iconKey), of: find.byKey(iconKey),
...@@ -566,8 +561,7 @@ void main() { ...@@ -566,8 +561,7 @@ void main() {
} }
}); });
testWidgets('DropdownButtonFormField with isDense:true does not clip large scale text', testWidgets('DropdownButtonFormField with isDense:true does not clip large scale text', (WidgetTester tester) async {
(WidgetTester tester) async {
final Key buttonKey = UniqueKey(); final Key buttonKey = UniqueKey();
const String value = 'two'; const String value = 'two';
...@@ -588,9 +582,11 @@ void main() { ...@@ -588,9 +582,11 @@ void main() {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
key: ValueKey<String>(item), key: ValueKey<String>(item),
value: item, value: item,
child: Text(item, child: Text(
key: ValueKey<String>('${item}Text'), item,
style: const TextStyle(fontSize: 20.0)), key: ValueKey<String>('${item}Text'),
style: const TextStyle(fontSize: 20.0),
),
); );
}).toList(), }).toList(),
), ),
...@@ -601,8 +597,7 @@ void main() { ...@@ -601,8 +597,7 @@ void main() {
), ),
); );
final RenderBox box = final RenderBox box = tester.renderObject<RenderBox>(find.byType(DropdownButton<String>));
tester.renderObject<RenderBox>(find.byType(dropdownButtonType));
expect(box.size.height, 64.0); expect(box.size.height, 64.0);
}); });
...@@ -633,7 +628,7 @@ void main() { ...@@ -633,7 +628,7 @@ void main() {
), ),
); );
final RenderBox box = tester.renderObject<RenderBox>(find.byType(dropdownButtonType)); final RenderBox box = tester.renderObject<RenderBox>(find.byType(DropdownButton<String>));
expect(box.size.height, 48.0); expect(box.size.height, 48.0);
}); });
...@@ -1077,7 +1072,7 @@ void main() { ...@@ -1077,7 +1072,7 @@ void main() {
expect(find.text(currentValue), findsOneWidget); expect(find.text(currentValue), findsOneWidget);
// Tap the DropdownButtonFormField widget // Tap the DropdownButtonFormField widget
await tester.tap(find.byType(dropdownButtonType)); await tester.tap(find.byType(DropdownButton<String>));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Tap the first dropdown menu item. // Tap the first dropdown menu item.
......
...@@ -1274,7 +1274,7 @@ void main() { ...@@ -1274,7 +1274,7 @@ void main() {
}); });
testWidgets('Material2 - ToggleButtons text baseline alignment', (WidgetTester tester) async { testWidgets('Material2 - ToggleButtons text baseline alignment', (WidgetTester tester) async {
// The point size of the fonts must be a multiple of 4 until // The font size must be a multiple of 4 until
// https://github.com/flutter/flutter/issues/122066 is resolved. // https://github.com/flutter/flutter/issues/122066 is resolved.
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
......
...@@ -1227,6 +1227,23 @@ void main() { ...@@ -1227,6 +1227,23 @@ void main() {
); );
layout(goodRoot, onErrors: () { assert(false); }); layout(goodRoot, onErrors: () { assert(false); });
}); });
group('BaselineOffset', () {
test('minOf', () {
expect(BaselineOffset.noBaseline.minOf(BaselineOffset.noBaseline), BaselineOffset.noBaseline);
expect(BaselineOffset.noBaseline.minOf(const BaselineOffset(1)), const BaselineOffset(1));
expect(const BaselineOffset(1).minOf(BaselineOffset.noBaseline), const BaselineOffset(1));
expect(const BaselineOffset(2).minOf(const BaselineOffset(1)), const BaselineOffset(1));
expect(const BaselineOffset(1).minOf(const BaselineOffset(2)), const BaselineOffset(1));
});
test('+', () {
expect(BaselineOffset.noBaseline + 2, BaselineOffset.noBaseline);
expect(const BaselineOffset(1) + 2, const BaselineOffset(3));
});
});
} }
class _DummyHitTestTarget implements HitTestTarget { class _DummyHitTestTarget implements HitTestTarget {
......
...@@ -37,6 +37,34 @@ class RenderTestBox extends RenderBox { ...@@ -37,6 +37,34 @@ class RenderTestBox extends RenderBox {
} }
} }
class RenderDryBaselineTestBox extends RenderTestBox {
double? baselineOverride;
@override
double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
calls += 1;
return baselineOverride ?? constraints.biggest.height / 2.0;
}
}
class RenderBadDryBaselineTestBox extends RenderTestBox {
@override
double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
return size.height / 2.0;
}
}
class RenderCannotComputeDryBaselineTestBox extends RenderTestBox {
bool shouldAssert = true;
@override
double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
if (shouldAssert) {
assert(debugCannotComputeDryLayout(reason: 'no dry baseline for you'));
}
return null;
}
}
void main() { void main() {
TestRenderingFlutterBinding.ensureInitialized(); TestRenderingFlutterBinding.ensureInitialized();
...@@ -134,4 +162,99 @@ void main() { ...@@ -134,4 +162,99 @@ void main() {
expect(test.calls, 3); // Use the cached data if the layout constraints do not change. expect(test.calls, 3); // Use the cached data if the layout constraints do not change.
}); });
group('Dry baseline', () {
test('computeDryBaseline results are cached and shared with computeDistanceToActualBaseline', () {
const double viewHeight = 200.0;
const BoxConstraints constraints = BoxConstraints.tightFor(width: 200.0, height: viewHeight);
final RenderDryBaselineTestBox test = RenderDryBaselineTestBox();
final RenderBox baseline = RenderBaseline(
baseline: 0.0,
baselineType: TextBaseline.alphabetic,
child: test,
);
final RenderConstrainedBox root = RenderConstrainedBox(
additionalConstraints: constraints,
child: baseline,
);
layout(RenderPositionedBox(child: root));
expect(test.calls, 1);
// The baseline widget loosens the input constraints when passing on to child.
expect(test.getDryBaseline(constraints.loosen(), TextBaseline.alphabetic), test.boxSize.height / 2);
// There's cache for the constraints so this should be 1, but we always evaluate
// computeDryBaseline in debug mode in case it asserts even if the baseline
// cache hits.
expect(test.calls, 2);
const BoxConstraints newConstraints = BoxConstraints.tightFor(width: 10.0, height: 10.0);
expect(test.getDryBaseline(newConstraints.loosen(), TextBaseline.alphabetic), 5.0);
// Should be 3 but there's an additional computeDryBaseline call in getDryBaseline,
// in an assert.
expect(test.calls, 4);
root.additionalConstraints = newConstraints;
pumpFrame();
expect(test.calls, 4);
});
test('Asserts when a RenderBox cannot compute dry baseline', () {
final RenderCannotComputeDryBaselineTestBox test = RenderCannotComputeDryBaselineTestBox();
layout(RenderBaseline(baseline: 0.0, baselineType: TextBaseline.alphabetic, child: test));
final BoxConstraints incomingConstraints = test.constraints;
assert(incomingConstraints != const BoxConstraints());
expect(
() => test.getDryBaseline(const BoxConstraints(), TextBaseline.alphabetic),
throwsA(isA<AssertionError>().having((AssertionError e) => e.message, 'message', contains('no dry baseline for you'))),
);
// Still throws when there is cache.
expect(
() => test.getDryBaseline(incomingConstraints, TextBaseline.alphabetic),
throwsA(isA<AssertionError>().having((AssertionError e) => e.message, 'message', contains('no dry baseline for you'))),
);
});
test('Cactches inconsistencies between computeDryBaseline and computeDistanceToActualBaseline', () {
final RenderDryBaselineTestBox test = RenderDryBaselineTestBox();
layout(test, phase: EnginePhase.composite);
FlutterErrorDetails? error;
test.markNeedsLayout();
test.baselineOverride = 123;
pumpFrame(phase: EnginePhase.composite, onErrors: () {
error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails();
});
expect(error?.exceptionAsString(), contains('differs from the baseline location computed by computeDryBaseline'));
});
test('Accessing RenderBox.size in computeDryBaseline is not allowed', () {
final RenderBadDryBaselineTestBox test = RenderBadDryBaselineTestBox();
FlutterErrorDetails? error;
layout(test, phase: EnginePhase.composite, onErrors: () {
error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails();
});
expect(error?.exceptionAsString(), contains('RenderBox.size accessed in RenderBadDryBaselineTestBox.computeDryBaseline.'));
});
test('debug baseline checks do not freak out when debugCannotComputeDryLayout is called', () {
FlutterErrorDetails? error;
void onErrors() {
error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails();
}
final RenderCannotComputeDryBaselineTestBox test = RenderCannotComputeDryBaselineTestBox();
layout(test, phase: EnginePhase.composite, onErrors: onErrors);
expect(error, isNull);
test.shouldAssert = false;
test.markNeedsLayout();
pumpFrame(phase: EnginePhase.composite, onErrors: onErrors);
expect(error?.exceptionAsString(), contains('differs from the baseline location computed by computeDryBaseline'));
});
});
} }
...@@ -119,6 +119,7 @@ void main() { ...@@ -119,6 +119,7 @@ void main() {
visitedChildren.add(child); visitedChildren.add(child);
} }
layout(stack);
stack.visitChildrenForSemantics(visitor); stack.visitChildrenForSemantics(visitor);
expect(visitedChildren, hasLength(1)); expect(visitedChildren, hasLength(1));
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
...@@ -44,6 +45,8 @@ void main() { ...@@ -44,6 +45,8 @@ void main() {
}); });
testWidgets('Chip caches baseline', (WidgetTester tester) async { testWidgets('Chip caches baseline', (WidgetTester tester) async {
final bool checkIntrinsicSizes = debugCheckIntrinsicSizes;
debugCheckIntrinsicSizes = false;
int calls = 0; int calls = 0;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -53,6 +56,7 @@ void main() { ...@@ -53,6 +56,7 @@ void main() {
baselineType: TextBaseline.alphabetic, baselineType: TextBaseline.alphabetic,
child: Chip( child: Chip(
label: BaselineDetector(() { label: BaselineDetector(() {
assert(!debugCheckIntrinsicSizes);
calls += 1; calls += 1;
}), }),
), ),
...@@ -66,9 +70,12 @@ void main() { ...@@ -66,9 +70,12 @@ void main() {
tester.renderObject<RenderBaselineDetector>(find.byType(BaselineDetector)).dirty(); tester.renderObject<RenderBaselineDetector>(find.byType(BaselineDetector)).dirty();
await tester.pump(); await tester.pump();
expect(calls, 2); expect(calls, 2);
debugCheckIntrinsicSizes = checkIntrinsicSizes;
}); });
testWidgets('ListTile caches baseline', (WidgetTester tester) async { testWidgets('ListTile caches baseline', (WidgetTester tester) async {
final bool checkIntrinsicSizes = debugCheckIntrinsicSizes;
debugCheckIntrinsicSizes = false;
int calls = 0; int calls = 0;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -78,6 +85,7 @@ void main() { ...@@ -78,6 +85,7 @@ void main() {
baselineType: TextBaseline.alphabetic, baselineType: TextBaseline.alphabetic,
child: ListTile( child: ListTile(
title: BaselineDetector(() { title: BaselineDetector(() {
assert(!debugCheckIntrinsicSizes);
calls += 1; calls += 1;
}), }),
), ),
...@@ -91,6 +99,7 @@ void main() { ...@@ -91,6 +99,7 @@ void main() {
tester.renderObject<RenderBaselineDetector>(find.byType(BaselineDetector)).dirty(); tester.renderObject<RenderBaselineDetector>(find.byType(BaselineDetector)).dirty();
await tester.pump(); await tester.pump();
expect(calls, 2); expect(calls, 2);
debugCheckIntrinsicSizes = checkIntrinsicSizes;
}); });
testWidgets("LayoutBuilder returns child's baseline", (WidgetTester tester) async { testWidgets("LayoutBuilder returns child's baseline", (WidgetTester tester) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment