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
@override
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.
......
......@@ -1249,10 +1249,12 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
}
@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
double? computeDistanceToActualBaseline(TextBaseline baseline) {
assert(title != null);
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) {
......
......@@ -1175,11 +1175,13 @@ class _SelectToggleButtonRenderObject extends RenderShiftedBox {
}
@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
double? computeDistanceToActualBaseline(TextBaseline baseline) {
// The baseline of this widget is the baseline of its child
return direction == Axis.horizontal
? child!.computeDistanceToActualBaseline(baseline)! + borderSide.width
: child!.computeDistanceToActualBaseline(baseline)! + leadingBorderSide.width;
final BaselineOffset childOffset = BaselineOffset(child?.computeDistanceToActualBaseline(baseline));
return switch (direction) {
Axis.horizontal => childOffset + borderSide.width,
Axis.vertical => childOffset + leadingBorderSide.width,
}.offset;
}
@override
......
This diff is collapsed.
......@@ -2213,16 +2213,16 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
@protected
void debugAssertDoesMeetConstraints();
/// When true, debugAssertDoesMeetConstraints() is currently
/// executing asserts for verifying the consistent behavior of
/// When true, a debug method ([debugAssertDoesMeetConstraints], for instance)
/// is currently executing asserts for verifying the consistent behavior of
/// intrinsic dimensions methods.
///
/// This should only be set by debugAssertDoesMeetConstraints()
/// implementations. It is used by tests to selectively ignore
/// custom layout callbacks. It should not be set outside of
/// debugAssertDoesMeetConstraints(), and should not be checked in
/// release mode (where it will always be false).
/// This is typically set by framework debug methods. It is read by tests to
/// selectively ignore custom layout callbacks. It should not be set outside of
/// intrinsic-checking debug methods, and should not be checked in release mode
/// (where it will always be false).
static bool debugCheckingIntrinsics = false;
bool _debugSubtreeRelayoutRootAlreadyMarkedNeedsLayout() {
if (_relayoutBoundary == null) {
// We don't know where our relayout boundary is yet.
......
......@@ -700,8 +700,9 @@ class RenderIndexedStack extends RenderStack {
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (index != null && firstChild != null) {
visitor(_childAtIndex());
final RenderBox? displayedChild = _childAtIndex();
if (displayedChild != null) {
visitor(displayedChild);
}
}
......@@ -715,45 +716,55 @@ class RenderIndexedStack extends RenderStack {
}
}
RenderBox _childAtIndex() {
assert(index != null);
RenderBox? _childAtIndex() {
final int? index = this.index;
if (index == null) {
return null;
}
RenderBox? child = firstChild;
int i = 0;
while (child != null && i < index!) {
final StackParentData childParentData = child.parentData! as StackParentData;
child = childParentData.nextSibling;
i += 1;
for (int i = 0; i < index && child != null; i += 1) {
child = childAfter(child);
}
assert(firstChild == null || child != null);
return child;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
final RenderBox? displayedChild = _childAtIndex();
if (displayedChild == null) {
return null;
}
assert(i == index);
assert(child != null);
return child!;
final StackParentData childParentData = displayedChild.parentData! as StackParentData;
final BaselineOffset offset = BaselineOffset(displayedChild.getDistanceToActualBaseline(baseline)) + childParentData.offset.dy;
return offset.offset;
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
if (firstChild == null || index == null) {
final RenderBox? displayedChild = _childAtIndex();
if (displayedChild == null) {
return false;
}
final RenderBox child = _childAtIndex();
final StackParentData childParentData = child.parentData! as StackParentData;
final StackParentData childParentData = displayedChild.parentData! as StackParentData;
return result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return child.hitTest(result, position: transformed);
return displayedChild.hitTest(result, position: transformed);
},
);
}
@override
void paintStack(PaintingContext context, Offset offset) {
if (firstChild == null || index == null) {
final RenderBox? displayedChild = _childAtIndex();
if (displayedChild == null) {
return;
}
final RenderBox child = _childAtIndex();
final StackParentData childParentData = child.parentData! as StackParentData;
context.paintChild(child, childParentData.offset + offset);
final StackParentData childParentData = displayedChild.parentData! as StackParentData;
context.paintChild(displayedChild, childParentData.offset + offset);
}
@override
......
......@@ -10,11 +10,6 @@ import 'package:flutter_test/flutter_test.dart';
const List<String> menuItems = <String>['one', 'two', 'three', 'four'];
void onChanged<T>(T _) { }
final Type dropdownButtonType = DropdownButton<String>(
onChanged: (_) { },
items: const <DropdownMenuItem<String>>[],
).runtimeType;
Finder _iconRichText(Key iconKey) {
return find.descendant(
of: find.byKey(iconKey),
......@@ -566,8 +561,7 @@ void main() {
}
});
testWidgets('DropdownButtonFormField with isDense:true does not clip large scale text',
(WidgetTester tester) async {
testWidgets('DropdownButtonFormField with isDense:true does not clip large scale text', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
const String value = 'two';
......@@ -588,9 +582,11 @@ void main() {
return DropdownMenuItem<String>(
key: ValueKey<String>(item),
value: item,
child: Text(item,
key: ValueKey<String>('${item}Text'),
style: const TextStyle(fontSize: 20.0)),
child: Text(
item,
key: ValueKey<String>('${item}Text'),
style: const TextStyle(fontSize: 20.0),
),
);
}).toList(),
),
......@@ -601,8 +597,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, 64.0);
});
......@@ -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);
});
......@@ -1077,7 +1072,7 @@ void main() {
expect(find.text(currentValue), findsOneWidget);
// Tap the DropdownButtonFormField widget
await tester.tap(find.byType(dropdownButtonType));
await tester.tap(find.byType(DropdownButton<String>));
await tester.pumpAndSettle();
// Tap the first dropdown menu item.
......
......@@ -1274,7 +1274,7 @@ void main() {
});
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.
await tester.pumpWidget(
boilerplate(
......
......@@ -1227,6 +1227,23 @@ void main() {
);
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 {
......
......@@ -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() {
TestRenderingFlutterBinding.ensureInitialized();
......@@ -134,4 +162,99 @@ void main() {
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() {
visitedChildren.add(child);
}
layout(stack);
stack.visitChildrenForSemantics(visitor);
expect(visitedChildren, hasLength(1));
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
......@@ -44,6 +45,8 @@ void main() {
});
testWidgets('Chip caches baseline', (WidgetTester tester) async {
final bool checkIntrinsicSizes = debugCheckIntrinsicSizes;
debugCheckIntrinsicSizes = false;
int calls = 0;
await tester.pumpWidget(
MaterialApp(
......@@ -53,6 +56,7 @@ void main() {
baselineType: TextBaseline.alphabetic,
child: Chip(
label: BaselineDetector(() {
assert(!debugCheckIntrinsicSizes);
calls += 1;
}),
),
......@@ -66,9 +70,12 @@ void main() {
tester.renderObject<RenderBaselineDetector>(find.byType(BaselineDetector)).dirty();
await tester.pump();
expect(calls, 2);
debugCheckIntrinsicSizes = checkIntrinsicSizes;
});
testWidgets('ListTile caches baseline', (WidgetTester tester) async {
final bool checkIntrinsicSizes = debugCheckIntrinsicSizes;
debugCheckIntrinsicSizes = false;
int calls = 0;
await tester.pumpWidget(
MaterialApp(
......@@ -78,6 +85,7 @@ void main() {
baselineType: TextBaseline.alphabetic,
child: ListTile(
title: BaselineDetector(() {
assert(!debugCheckIntrinsicSizes);
calls += 1;
}),
),
......@@ -91,6 +99,7 @@ void main() {
tester.renderObject<RenderBaselineDetector>(find.byType(BaselineDetector)).dirty();
await tester.pump();
expect(calls, 2);
debugCheckIntrinsicSizes = checkIntrinsicSizes;
});
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