// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @immutable class Pair<T> { const Pair(this.first, this.second); final T? first; final T second; @override bool operator ==(Object other) { return other is Pair<T> && other.first == first && other.second == second; } @override int get hashCode => Object.hash(first, second); @override String toString() => '($first,$second)'; } /// Widget that will layout one child in the top half of this widget's size /// and the other child in the bottom half. It will swap which child is on top /// and which is on bottom every time the widget is rendered. abstract class Swapper extends RenderObjectWidget { const Swapper({ super.key, this.stable, this.swapper }); final Widget? stable; final Widget? swapper; @override SwapperElement createElement(); @override RenderObject createRenderObject(BuildContext context) => RenderSwapper(); } class SwapperWithProperOverrides extends Swapper { const SwapperWithProperOverrides({ super.key, super.stable, super.swapper, }); @override SwapperElement createElement() => SwapperElementWithProperOverrides(this); } class SwapperWithNoOverrides extends Swapper { const SwapperWithNoOverrides({ super.key, super.stable, super.swapper, }); @override SwapperElement createElement() => SwapperElementWithNoOverrides(this); } abstract class SwapperElement extends RenderObjectElement { SwapperElement(Swapper super.widget); Element? stable; Element? swapper; bool swapperIsOnTop = true; List<dynamic> insertSlots = <dynamic>[]; List<Pair<dynamic>> moveSlots = <Pair<dynamic>>[]; List<dynamic> removeSlots = <dynamic>[]; @override Swapper get widget => super.widget as Swapper; @override RenderSwapper get renderObject => super.renderObject as RenderSwapper; @override void visitChildren(ElementVisitor visitor) { if (stable != null) { visitor(stable!); } if (swapper != null) { visitor(swapper!); } } @override void update(Swapper newWidget) { super.update(newWidget); _updateChildren(newWidget); } @override void mount(Element? parent, Object? newSlot) { super.mount(parent, newSlot); _updateChildren(widget); } void _updateChildren(Swapper widget) { stable = updateChild(stable, widget.stable, 'stable'); swapper = updateChild(swapper, widget.swapper, swapperIsOnTop); swapperIsOnTop = !swapperIsOnTop; } } class SwapperElementWithProperOverrides extends SwapperElement { SwapperElementWithProperOverrides(super.widget); @override void insertRenderObjectChild(RenderBox child, Object? slot) { insertSlots.add(slot); assert(child != null); if (slot == 'stable') { renderObject.stable = child; } else { renderObject.setSwapper(child, slot! as bool); } } @override void moveRenderObjectChild(RenderBox child, bool oldIsOnTop, bool newIsOnTop) { moveSlots.add(Pair<bool>(oldIsOnTop, newIsOnTop)); assert(oldIsOnTop == !newIsOnTop); renderObject.setSwapper(child, newIsOnTop); } @override void removeRenderObjectChild(RenderBox child, Object? slot) { removeSlots.add(slot); if (slot == 'stable') { renderObject.stable = null; } else { renderObject.setSwapper(null, slot! as bool); } } } class SwapperElementWithNoOverrides extends SwapperElement { SwapperElementWithNoOverrides(super.widget); } class RenderSwapper extends RenderBox { RenderBox? _stable; RenderBox? get stable => _stable; set stable(RenderBox? child) { if (child == _stable) { return; } if (_stable != null) { dropChild(_stable!); } _stable = child; if (child != null) { adoptChild(child); } } bool? _swapperIsOnTop; RenderBox? _swapper; RenderBox? get swapper => _swapper; void setSwapper(RenderBox? child, bool isOnTop) { if (isOnTop != _swapperIsOnTop) { _swapperIsOnTop = isOnTop; markNeedsLayout(); } if (child == _swapper) { return; } if (_swapper != null) { dropChild(_swapper!); } _swapper = child; if (child != null) { adoptChild(child); } } @override void visitChildren(RenderObjectVisitor visitor) { if (_stable != null) { visitor(_stable!); } if (_swapper != null) { visitor(_swapper!); } } @override void attach(PipelineOwner owner) { super.attach(owner); visitChildren((RenderObject child) => child.attach(owner)); } @override void detach() { super.detach(); visitChildren((RenderObject child) => child.detach()); } @override Size computeDryLayout(BoxConstraints constraints) { return constraints.biggest; } @override void performLayout() { assert(constraints.hasBoundedWidth); assert(constraints.hasTightHeight); size = constraints.biggest; const Offset topOffset = Offset.zero; final Offset bottomOffset = Offset(0, size.height / 2); final BoxConstraints childConstraints = constraints.copyWith( minHeight: constraints.minHeight / 2, maxHeight: constraints.maxHeight / 2, ); if (_stable != null) { final BoxParentData stableParentData = _stable!.parentData! as BoxParentData; _stable!.layout(childConstraints); stableParentData.offset = _swapperIsOnTop! ? bottomOffset : topOffset; } if (_swapper != null) { final BoxParentData swapperParentData = _swapper!.parentData! as BoxParentData; _swapper!.layout(childConstraints); swapperParentData.offset = _swapperIsOnTop! ? topOffset : bottomOffset; } } @override void paint(PaintingContext context, Offset offset) { visitChildren((RenderObject child) { final BoxParentData childParentData = child.parentData! as BoxParentData; context.paintChild(child, offset + childParentData.offset); }); } @override void redepthChildren() { visitChildren((RenderObject child) => redepthChild(child)); } } BoxParentData parentDataFor(RenderObject renderObject) => renderObject.parentData! as BoxParentData; void main() { testWidgets('RenderObjectElement *RenderObjectChild methods get called with correct arguments', (WidgetTester tester) async { const Key redKey = ValueKey<String>('red'); const Key blueKey = ValueKey<String>('blue'); Widget widget() { return SwapperWithProperOverrides( stable: ColoredBox( key: redKey, color: Color(nonconst(0xffff0000)), ), swapper: ColoredBox( key: blueKey, color: Color(nonconst(0xff0000ff)), ), ); } await tester.pumpWidget(widget()); final SwapperElement swapper = tester.element<SwapperElement>(find.byType(SwapperWithProperOverrides)); final RenderBox redBox = tester.renderObject<RenderBox>(find.byKey(redKey)); final RenderBox blueBox = tester.renderObject<RenderBox>(find.byKey(blueKey)); expect(swapper.insertSlots.length, 2); expect(swapper.insertSlots, contains('stable')); expect(swapper.insertSlots, contains(true)); expect(swapper.moveSlots, isEmpty); expect(swapper.removeSlots, isEmpty); expect(parentDataFor(redBox).offset, const Offset(0, 300)); expect(parentDataFor(blueBox).offset, Offset.zero); await tester.pumpWidget(widget()); expect(swapper.insertSlots.length, 2); expect(swapper.moveSlots.length, 1); expect(swapper.moveSlots, contains(const Pair<bool>(true, false))); expect(swapper.removeSlots, isEmpty); expect(parentDataFor(redBox).offset, Offset.zero); expect(parentDataFor(blueBox).offset, const Offset(0, 300)); await tester.pumpWidget(const SwapperWithProperOverrides()); expect(redBox.attached, false); expect(blueBox.attached, false); expect(swapper.insertSlots.length, 2); expect(swapper.moveSlots.length, 1); expect(swapper.removeSlots.length, 2); expect(swapper.removeSlots, contains('stable')); expect(swapper.removeSlots, contains(false)); }); testWidgets('RenderObjectElement *ChildRenderObject methods fail with deprecation message', (WidgetTester tester) async { const Key redKey = ValueKey<String>('red'); const Key blueKey = ValueKey<String>('blue'); Widget widget() { return SwapperWithNoOverrides( stable: ColoredBox( key: redKey, color: Color(nonconst(0xffff0000)), ), swapper: ColoredBox( key: blueKey, color: Color(nonconst(0xff0000ff)), ), ); } await tester.pumpWidget(widget()); final FlutterError error = tester.takeException() as FlutterError; final ErrorSummary summary = error.diagnostics.first as ErrorSummary; expect(summary.toString(), contains('deprecated')); }); }