From 20e8efe165868c385c2394d7bbb84deb334ac2f5 Mon Sep 17 00:00:00 2001 From: Nils Reichardt Date: Sat, 5 Jun 2021 17:39:30 +0200 Subject: [PATCH 01/35] docs: updated version in `README.md` (#31) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec4ef24..0d59766 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ First, you will need to add `portal` to your `pubspec.yaml`: dependencies: flutter: sdk: flutter - flutter_portal: ^0.3.0 + flutter_portal: ^0.4.0 ``` Then, run `flutter packages get` in your terminal. From f1a915257d6b24d8d6e86df700cb0d58f5bd4863 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 24 Oct 2021 15:56:48 +0000 Subject: [PATCH 02/35] Clean up project setup --- example/.gitignore | 5 +++++ pubspec.yaml | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/example/.gitignore b/example/.gitignore index 78b3c2b..282a36d 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -29,6 +29,11 @@ .pub-cache/ .pub/ /build/ +android/ +ios/ +linux/ +windows/ +macos/ # Web related lib/generated_plugin_registrant.dart diff --git a/pubspec.yaml b/pubspec.yaml index 977d73f..08e6322 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,9 +1,7 @@ name: flutter_portal version: 0.4.0 -description: Overlay/OverlayEntry, but implemented as a widget for a declarative API. +description: Overlay/OverlayEntry but implemented as a widget for a declarative API. homepage: https://github.com/rrousselGit/flutter_portal -authors: - - Remi Rousselet environment: sdk: '>=2.12.0 <3.0.0' From 862420b672ea1445032e899643ab81eb52d54e71 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 24 Oct 2021 15:59:43 +0000 Subject: [PATCH 03/35] Fix warnings --- analysis_options.yaml | 3 --- example/lib/discovery.dart | 2 +- example/pubspec.lock | 8 ++++---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 7b7f222..724bc00 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,9 +12,6 @@ analyzer: # in this file included_file_warning: ignore - # Causes false positives (https://github.com/dart-lang/sdk/issues/41571 - top_level_function_literal_block: ignore - linter: rules: # Personal preference. I don't find it more readable diff --git a/example/lib/discovery.dart b/example/lib/discovery.dart index 6d41d47..09076f1 100644 --- a/example/lib/discovery.dart +++ b/example/lib/discovery.dart @@ -86,7 +86,7 @@ class Discovery extends StatelessWidget { portal: Stack( children: [ CustomPaint( - painter: HolePainter(Theme.of(context).accentColor), + painter: HolePainter(Theme.of(context).colorScheme.secondary), child: TweenAnimationBuilder( duration: kThemeAnimationDuration, curve: Curves.easeOut, diff --git a/example/pubspec.lock b/example/pubspec.lock index 7da7af8..bb64a0b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" boolean_selector: dependency: transitive description: @@ -28,7 +28,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -87,7 +87,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: @@ -141,7 +141,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" typed_data: dependency: transitive description: From 88eec62f5a8d8a938f976c4618ebbccce008f530 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 24 Oct 2021 16:20:52 +0000 Subject: [PATCH 04/35] Adjust example to expect high-level result with backup logic --- example/lib/contextual_menu.dart | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/example/lib/contextual_menu.dart b/example/lib/contextual_menu.dart index f8d0550..fd20f0c 100644 --- a/example/lib/contextual_menu.dart +++ b/example/lib/contextual_menu.dart @@ -76,15 +76,12 @@ class Menu extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 10), - child: Card( - elevation: 8, - child: IntrinsicWidth( - child: Column( - mainAxisSize: MainAxisSize.min, - children: children, - ), + return Card( + elevation: 8, + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + children: children, ), ), ); @@ -118,8 +115,14 @@ class ModalEntry extends StatelessWidget { visible: visible, portal: menu, anchor: const Aligned( - target: Alignment.center, - source: Alignment.center, + source: Alignment.topLeft, + target: Alignment.bottomLeft, + widthFactor: 1, + backup: Aligned( + source: Alignment.bottomLeft, + target: Alignment.topLeft, + widthFactor: 1, + ), ), child: IgnorePointer( ignoring: visible, From 93435cb982e0d9c458e9b5fbd07634ce7ac4b945 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 24 Oct 2021 16:40:41 +0000 Subject: [PATCH 05/35] Fix tests --- test/anchor_test.dart | 1 + test/widget_test.dart | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 test/anchor_test.dart diff --git a/test/anchor_test.dart b/test/anchor_test.dart new file mode 100644 index 0000000..ab73b3a --- /dev/null +++ b/test/anchor_test.dart @@ -0,0 +1 @@ +void main() {} diff --git a/test/widget_test.dart b/test/widget_test.dart index 457129e..be2059c 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -15,11 +15,19 @@ import 'package:flutter/services.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_portal/src/portal.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as p; Future fetchFont() async { - final roboto = File.fromUri( - Uri.file('${Directory.current.path}/../assets/Roboto-Regular.ttf'), - ); + final Uri fontUri; + if (p.basename(Directory.current.path) == 'test') { + fontUri = + Uri.file('${Directory.current.path}/../assets/Roboto-Regular.ttf'); + } else { + // We assume the test command is executed from the project root. + fontUri = Uri.file('assets/Roboto-Regular.ttf'); + } + + final roboto = File.fromUri(fontUri); final bytes = Uint8List.fromList(await roboto.readAsBytes()); return ByteData.view(bytes.buffer); } @@ -611,7 +619,7 @@ Future main() async { exception.toString(), equals('Error: Could not find a Portal above this ' 'PortalEntry(' - "anchor: Instance of 'FullScreen', " + "anchor: Instance of 'Filled', " 'closeDuration: 0:00:05.000000, ' 'portal: Text, child: Text).\n'), ); @@ -1576,6 +1584,7 @@ class Boilerplate extends StatelessWidget { } mixin Noop {} + class TestPortal = Portal with Noop; @immutable From ae63d22436c6aff1912ce1d58561364f81bffc76 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 24 Oct 2021 17:33:00 +0000 Subject: [PATCH 06/35] Add failing tests for TDD --- lib/src/custom_follower.dart | 14 ++- test/anchor_test.dart | 204 ++++++++++++++++++++++++++++++++++- test/widget_test.dart | 2 +- 3 files changed, 210 insertions(+), 10 deletions(-) diff --git a/lib/src/custom_follower.dart b/lib/src/custom_follower.dart index 82f5626..373b168 100644 --- a/lib/src/custom_follower.dart +++ b/lib/src/custom_follower.dart @@ -80,6 +80,7 @@ class MyRenderFollowerLayer extends RenderProxyBox { /// @nodoc Anchor get anchor => _anchor; + set anchor(Anchor value) { if (_anchor != value) { _anchor = value; @@ -91,6 +92,7 @@ class MyRenderFollowerLayer extends RenderProxyBox { /// @nodoc LayerLink get link => _link; + set link(LayerLink value) { if (_link == value) { return; @@ -100,7 +102,9 @@ class MyRenderFollowerLayer extends RenderProxyBox { } OverlayLink _overlayLink; + OverlayLink get overlayLink => _overlayLink; + set overlayLink(OverlayLink value) { if (_overlayLink == value) { return; @@ -113,6 +117,7 @@ class MyRenderFollowerLayer extends RenderProxyBox { /// @nodoc Size get targetSize => _targetSize; + set targetSize(Size value) { if (_targetSize == value) { return; @@ -130,9 +135,6 @@ class MyRenderFollowerLayer extends RenderProxyBox { @override bool get alwaysNeedsCompositing => true; - @override - bool get sizedByParent => false; - @override FollowerLayer? get layer => super.layer as FollowerLayer?; @@ -157,14 +159,10 @@ class MyRenderFollowerLayer extends RenderProxyBox { ); } - @override - void performResize() { - size = constraints.biggest; - } - @override void paint(PaintingContext context, Offset offset) { final linkedOffset = anchor.getSourceOffset( + // The size is set in performLayout of the RenderProxyBoxMixin. sourceSize: size, targetRect: Rect.fromLTWH(0, 0, targetSize.width, targetSize.height), overlayRect: const Rect.fromLTRB( diff --git a/test/anchor_test.dart b/test/anchor_test.dart index ab73b3a..98bbc0b 100644 --- a/test/anchor_test.dart +++ b/test/anchor_test.dart @@ -1 +1,203 @@ -void main() {} +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/src/rendering/box.dart'; +import 'package:flutter_portal/src/anchor.dart'; +import 'package:flutter_portal/src/portal.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('$Anchor is passed proper constraints', (tester) async { + Rect? constraintsTargetRect; + BoxConstraints? constraintsOverlayConstraints; + Size? offsetSourceSize; + Rect? offsetTargetRect; + Rect? offsetOverlayRect; + final anchor = _TestAnchor( + constraints: const BoxConstraints( + minWidth: 42, + maxWidth: 42, + minHeight: 42, + maxHeight: 42, + ), + onGetSourceConstraints: (targetRect, overlayConstraints) { + constraintsTargetRect = targetRect; + constraintsOverlayConstraints = overlayConstraints; + }, + onGetSourceOffset: (sourceSize, targetRect, overlayRect) { + offsetSourceSize = sourceSize; + offsetTargetRect = targetRect; + offsetOverlayRect = overlayRect; + }, + ); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: Portal( + child: Center( + child: SizedBox( + width: 50, + height: 50, + child: PortalEntry( + anchor: anchor, + portal: const SizedBox( + width: 30, + height: 30, + ), + child: const SizedBox( + width: 20, + height: 20, + ), + ), + ), + ), + ), + ), + ), + ), + )); + + expect(constraintsTargetRect, Offset.zero & const Size(50, 50)); + expect( + constraintsOverlayConstraints, + BoxConstraints.tight(const Size(100, 100)), + ); + expect(constraintsTargetRect, offsetTargetRect); + expect(offsetSourceSize, const Size(42, 42)); + expect(offsetOverlayRect, Offset.zero & const Size(100, 100)); + }); + + testWidgets('$Aligned defers to backup if needed', (tester) async { + final backupAligned = _TestAligned( + source: Alignment.bottomLeft, + target: Alignment.topLeft, + ); + final entry = PortalEntry( + anchor: Aligned( + source: Alignment.topLeft, + target: Alignment.bottomLeft, + backup: backupAligned, + ), + portal: const SizedBox( + width: 20, + height: 20, + ), + child: const Center( + child: SizedBox( + width: 10, + height: 10, + ), + ), + ); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 50, + height: 50, + child: Portal( + child: Center( + child: entry, + ), + ), + ), + ), + ), + )); + + expect(backupAligned.offsetAccessed, false); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 50, + height: 49, + child: Portal( + child: Center( + child: entry, + ), + ), + ), + ), + ), + )); + + expect(backupAligned.offsetAccessed, true); + }); +} + +class _TestAnchor implements Anchor { + const _TestAnchor({ + required this.constraints, + required this.onGetSourceConstraints, + required this.onGetSourceOffset, + }); + + final BoxConstraints constraints; + + final void Function( + Rect targetRect, + BoxConstraints overlayConstraints, + ) onGetSourceConstraints; + final void Function( + Size sourceSize, + Rect targetRect, + Rect overlayRect, + ) onGetSourceOffset; + + @override + BoxConstraints getSourceConstraints({ + required Rect targetRect, + required BoxConstraints overlayConstraints, + }) { + onGetSourceConstraints(targetRect, overlayConstraints); + return constraints; + } + + @override + Offset getSourceOffset({ + required Size sourceSize, + required Rect targetRect, + required Rect overlayRect, + }) { + onGetSourceOffset(sourceSize, targetRect, overlayRect); + return Offset.zero; + } +} + +class _TestAligned extends Aligned { + _TestAligned({ + required Alignment source, + required Alignment target, + Offset offset = Offset.zero, + double? widthFactor, + double? heightFactor, + }) : super( + source: source, + target: target, + offset: offset, + widthFactor: widthFactor, + heightFactor: heightFactor); + + bool offsetAccessed = false; + + @override + Offset getSourceOffset({ + required Size sourceSize, + required Rect targetRect, + required Rect overlayRect, + }) { + offsetAccessed = true; + return super.getSourceOffset( + sourceSize: sourceSize, + targetRect: targetRect, + overlayRect: overlayRect, + ); + } +} diff --git a/test/widget_test.dart b/test/widget_test.dart index be2059c..9de82cd 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -12,7 +12,7 @@ import 'dart:typed_data'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_portal/src/anchor.dart'; import 'package:flutter_portal/src/portal.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; From 2dc224859ca4ea3e6b9d01725d81569d17f4e2e6 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 24 Oct 2021 17:34:34 +0000 Subject: [PATCH 07/35] Fix warnings --- test/anchor_test.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/anchor_test.dart b/test/anchor_test.dart index 98bbc0b..7d2c769 100644 --- a/test/anchor_test.dart +++ b/test/anchor_test.dart @@ -72,9 +72,11 @@ void main() { }); testWidgets('$Aligned defers to backup if needed', (tester) async { + var offsetAccessed = false; final backupAligned = _TestAligned( source: Alignment.bottomLeft, target: Alignment.topLeft, + onGetSourceOffset: () => offsetAccessed = true, ); final entry = PortalEntry( anchor: Aligned( @@ -110,7 +112,7 @@ void main() { ), )); - expect(backupAligned.offsetAccessed, false); + expect(offsetAccessed, false); await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -128,7 +130,7 @@ void main() { ), )); - expect(backupAligned.offsetAccessed, true); + expect(offsetAccessed, true); }); } @@ -172,12 +174,13 @@ class _TestAnchor implements Anchor { } class _TestAligned extends Aligned { - _TestAligned({ + const _TestAligned({ required Alignment source, required Alignment target, Offset offset = Offset.zero, double? widthFactor, double? heightFactor, + required this.onGetSourceOffset, }) : super( source: source, target: target, @@ -185,7 +188,7 @@ class _TestAligned extends Aligned { widthFactor: widthFactor, heightFactor: heightFactor); - bool offsetAccessed = false; + final VoidCallback onGetSourceOffset; @override Offset getSourceOffset({ @@ -193,7 +196,7 @@ class _TestAligned extends Aligned { required Rect targetRect, required Rect overlayRect, }) { - offsetAccessed = true; + onGetSourceOffset(); return super.getSourceOffset( sourceSize: sourceSize, targetRect: targetRect, From 0ea781cec531419f7e562415d1a59ac1fbf28b34 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 24 Oct 2021 17:51:57 +0000 Subject: [PATCH 08/35] Improve Anchor docs for better understanding --- lib/src/anchor.dart | 50 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart index 943cc3d..7529539 100644 --- a/lib/src/anchor.dart +++ b/lib/src/anchor.dart @@ -6,22 +6,50 @@ import 'package:flutter/rendering.dart'; /// /// Independent of the underlying rendering implementation. abstract class Anchor { - /// Return the layout constraints that are given to the source element given: - /// - [targetRect] the bounds of the element which the source element should - /// be anchored to. No assumptions should be made about the coordinate space. - /// - [overlayConstraints] the available space to render the source element + /// Returns the layout constraints that are given to the source element. + /// + /// The [targetRect] represents the bounds of the element which the source + /// element should be anchored to. This must be the same value that is passed + /// to [getSourceOffset]. No assumptions should be made about the coordinate + /// space, i.e. only the size of the target should be considered. + /// + /// The [overlayConstraints] represent the full available space to place the + /// source element in. This is irrespective of where the target is positioned + /// within the full available space. BoxConstraints getSourceConstraints({ required Rect targetRect, required BoxConstraints overlayConstraints, }); - /// Return the offset at which to position the source element in relation to - /// to the top-left corner of [targetRect] given: - /// - [sourceSize] the final calculated size of the source element - /// - [targetRect] the bounds of the element which the source should be - /// anchored to. Should be the same value passed in from [getSourceConstraints] - /// - [overlayRect] the bounds of the full available space to render the - /// source element + /// Returns the offset at which to position the source element in relation to + /// to the top left of the [targetRect]. + /// + /// The [sourceSize] is the final size of the source element after layout + /// based on the source constraints determined by [getSourceConstraints]. + /// + /// The [targetRect] represents the bounds of the element which the source + /// element should be anchored to. This must be the same value that is passed + /// to [getSourceConstraints]. + /// + /// The [overlayRect] represents the bounds of the full available space to + /// place the source element in. Note that this is also relative to the top + /// left of the [targetRect]. + /// This means that every offset going into or coming out of this function are + /// relative to the top-left corner of the target. + /// + /// ## Example + /// + /// In this example, our source element has a size of `Size(30, 30)` and + /// should be anchored to the bottom right of the target. + /// + /// If we assume the full available space starts at absolute `(0, 0)` and + /// spans to absolute `(100, 100)` and the target rect starts at absolute + /// `(40, 40)` and spans to absolute `(60, 60)`, the passed values will be: + /// + /// * `Rect.fromLTWH(0, 0, 20, 20)` for the [targetRect]. + /// * `Rect.fromLTWH(-40, -40, 100, 100)` for the [overlayRect]. + /// * `Size(30, 30)` for the [sourceSize]. + /// * `Offset(20, 20)` as the return value. Offset getSourceOffset({ required Size sourceSize, required Rect targetRect, From 2f6a6ff2386fc640b3221008e29f9a4d1e2c3aaf Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 24 Oct 2021 21:12:57 +0000 Subject: [PATCH 09/35] Implement root bounds --- example/lib/contextual_menu.dart | 6 +++--- lib/flutter_portal.dart | 5 +++-- lib/src/anchor.dart | 14 ++++++------- lib/src/custom_follower.dart | 35 +++++++++++++++++++------------- lib/src/portal.dart | 14 ++++++++++++- test/anchor_test.dart | 18 ++++++++-------- 6 files changed, 56 insertions(+), 36 deletions(-) diff --git a/example/lib/contextual_menu.dart b/example/lib/contextual_menu.dart index fd20f0c..e124346 100644 --- a/example/lib/contextual_menu.dart +++ b/example/lib/contextual_menu.dart @@ -40,7 +40,7 @@ class _ContextualMenuExampleState extends State { @override Widget build(BuildContext context) { return Center( - child: ModalEntry( + child: _ModalEntry( visible: _showMenu, onClose: () => setState(() => _showMenu = false), childAnchor: Alignment.topRight, @@ -88,8 +88,8 @@ class Menu extends StatelessWidget { } } -class ModalEntry extends StatelessWidget { - const ModalEntry({ +class _ModalEntry extends StatelessWidget { + const _ModalEntry({ Key? key, required this.onClose, required this.menu, diff --git a/lib/flutter_portal.dart b/lib/flutter_portal.dart index 819adda..815403b 100644 --- a/lib/flutter_portal.dart +++ b/lib/flutter_portal.dart @@ -1,2 +1,3 @@ -export 'src/anchor.dart' show Anchor, Aligned; -export 'src/portal.dart' show Portal, PortalEntry, PortalNotFoundError; +export 'package:flutter_portal/src/anchor.dart' show Anchor, Aligned; +export 'package:flutter_portal/src/portal.dart' + show Portal, PortalEntry, PortalNotFoundError; diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart index 7529539..9671243 100644 --- a/lib/src/anchor.dart +++ b/lib/src/anchor.dart @@ -31,7 +31,7 @@ abstract class Anchor { /// element should be anchored to. This must be the same value that is passed /// to [getSourceConstraints]. /// - /// The [overlayRect] represents the bounds of the full available space to + /// The [theaterRect] represents the bounds of the full available space to /// place the source element in. Note that this is also relative to the top /// left of the [targetRect]. /// This means that every offset going into or coming out of this function are @@ -47,13 +47,13 @@ abstract class Anchor { /// `(40, 40)` and spans to absolute `(60, 60)`, the passed values will be: /// /// * `Rect.fromLTWH(0, 0, 20, 20)` for the [targetRect]. - /// * `Rect.fromLTWH(-40, -40, 100, 100)` for the [overlayRect]. + /// * `Rect.fromLTWH(-40, -40, 100, 100)` for the [theaterRect]. /// * `Size(30, 30)` for the [sourceSize]. /// * `Offset(20, 20)` as the return value. Offset getSourceOffset({ required Size sourceSize, required Rect targetRect, - required Rect overlayRect, + required Rect theaterRect, }); } @@ -75,7 +75,7 @@ class Filled implements Anchor { Offset getSourceOffset({ required Size sourceSize, required Rect targetRect, - required Rect overlayRect, + required Rect theaterRect, }) { return Offset.zero; } @@ -144,7 +144,7 @@ class Aligned implements Anchor { Offset getSourceOffset({ required Size sourceSize, required Rect targetRect, - required Rect overlayRect, + required Rect theaterRect, }) { final sourceRect = (Offset.zero & sourceSize).alignedTo( targetRect, @@ -153,13 +153,13 @@ class Aligned implements Anchor { offset: offset, ); - if (!overlayRect.fullyContains(sourceRect)) { + if (!theaterRect.fullyContains(sourceRect)) { final backup = this.backup; if (backup != null) { return backup.getSourceOffset( sourceSize: sourceSize, targetRect: targetRect, - overlayRect: overlayRect, + theaterRect: theaterRect, ); } } diff --git a/lib/src/custom_follower.dart b/lib/src/custom_follower.dart index 373b168..495ca2e 100644 --- a/lib/src/custom_follower.dart +++ b/lib/src/custom_follower.dart @@ -5,9 +5,9 @@ import 'anchor.dart'; import 'portal.dart'; /// @nodoc -class MyCompositedTransformFollower extends SingleChildRenderObjectWidget { +class CustomCompositedTransformFollower extends SingleChildRenderObjectWidget { /// @nodoc - const MyCompositedTransformFollower({ + const CustomCompositedTransformFollower({ Key? key, required this.link, required this.overlayLink, @@ -29,8 +29,8 @@ class MyCompositedTransformFollower extends SingleChildRenderObjectWidget { final Size targetSize; @override - MyRenderFollowerLayer createRenderObject(BuildContext context) { - return MyRenderFollowerLayer( + RenderFollowerLayer createRenderObject(BuildContext context) { + return RenderFollowerLayer( anchor: anchor, link: link, overlayLink: overlayLink, @@ -41,7 +41,7 @@ class MyCompositedTransformFollower extends SingleChildRenderObjectWidget { @override void updateRenderObject( BuildContext context, - MyRenderFollowerLayer renderObject, + RenderFollowerLayer renderObject, ) { renderObject ..link = link @@ -62,9 +62,9 @@ class MyCompositedTransformFollower extends SingleChildRenderObjectWidget { } /// @nodoc -class MyRenderFollowerLayer extends RenderProxyBox { +class RenderFollowerLayer extends RenderProxyBox { /// @nodoc - MyRenderFollowerLayer({ + RenderFollowerLayer({ required LayerLink link, required OverlayLink overlayLink, required Size targetSize, @@ -161,17 +161,24 @@ class MyRenderFollowerLayer extends RenderProxyBox { @override void paint(PaintingContext context, Offset offset) { + assert( + overlayLink.theater != null, + 'The theater must be set in the OverlayLink when the ' + '_RenderPortalTheater is inserted as a child of the _PortalLinkScope. ' + 'Therefore, it must not be null in any child PortalEntry.', + ); + final theater = overlayLink.theater!; + + // In order to compute the theater rect, we must first offset (shift) it by + // the position of the top-left corner of the target in the coordinate space + // of the theater since we are working with it relative to the target. + final theaterShift = -localToGlobal(link.leader!.offset); + final theaterRect = theaterShift & theater.size; final linkedOffset = anchor.getSourceOffset( // The size is set in performLayout of the RenderProxyBoxMixin. sourceSize: size, targetRect: Rect.fromLTWH(0, 0, targetSize.width, targetSize.height), - overlayRect: const Rect.fromLTRB( - // We don't know where we'll end up, so we have no idea what our cull rect should be. - 0, - 0, - double.infinity, - double.infinity, - ), + theaterRect: theaterRect, ); if (layer == null) { diff --git a/lib/src/portal.dart b/lib/src/portal.dart index 09dfb92..ec7242a 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -68,6 +68,7 @@ class _PortalState extends State { class OverlayLink { _RenderPortalTheater? theater; + BoxConstraints? get constraints => theater?.constraints; final Set overlays = {}; @@ -119,7 +120,9 @@ class _RenderPortalTheater extends RenderProxyBox { } OverlayLink _overlayLink; + OverlayLink get overlayLink => _overlayLink; + set overlayLink(OverlayLink value) { if (_overlayLink != value) { assert( @@ -432,7 +435,7 @@ class _PortalEntryState extends State { overlayLink: scope._overlayLink, anchor: widget.anchor, targetSize: targetSize, - portal: MyCompositedTransformFollower( + portal: CustomCompositedTransformFollower( link: _link, overlayLink: scope._overlayLink, anchor: widget.anchor, @@ -492,6 +495,7 @@ class _PortalEntryTheater extends SingleChildRenderObjectWidget { @override SingleChildRenderObjectElement createElement() => _PortalEntryElement(this); + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -513,7 +517,9 @@ class _RenderPortalEntry extends RenderProxyBox { bool _needsAddEntryInTheater = false; OverlayLink _overlayLink; + OverlayLink get overlayLink => _overlayLink; + set overlayLink(OverlayLink value) { assert(value.theater != null); if (_overlayLink != value) { @@ -523,7 +529,9 @@ class _RenderPortalEntry extends RenderProxyBox { } Anchor _anchor; + Anchor get anchor => _anchor; + set anchor(Anchor value) { if (value != _anchor) { _anchor = value; @@ -532,7 +540,9 @@ class _RenderPortalEntry extends RenderProxyBox { } Size _targetSize; + Size get targetSize => _targetSize; + set targetSize(Size value) { if (value != _targetSize) { _targetSize = value; @@ -541,7 +551,9 @@ class _RenderPortalEntry extends RenderProxyBox { } RenderBox? _branch; + RenderBox? get branch => _branch; + set branch(RenderBox? value) { if (_branch != null) { _overlayLink.overlays.remove(branch); diff --git a/test/anchor_test.dart b/test/anchor_test.dart index 7d2c769..0a07b72 100644 --- a/test/anchor_test.dart +++ b/test/anchor_test.dart @@ -12,7 +12,7 @@ void main() { BoxConstraints? constraintsOverlayConstraints; Size? offsetSourceSize; Rect? offsetTargetRect; - Rect? offsetOverlayRect; + Rect? offsetTheaterRect; final anchor = _TestAnchor( constraints: const BoxConstraints( minWidth: 42, @@ -24,10 +24,10 @@ void main() { constraintsTargetRect = targetRect; constraintsOverlayConstraints = overlayConstraints; }, - onGetSourceOffset: (sourceSize, targetRect, overlayRect) { + onGetSourceOffset: (sourceSize, targetRect, theaterRect) { offsetSourceSize = sourceSize; offsetTargetRect = targetRect; - offsetOverlayRect = overlayRect; + offsetTheaterRect = theaterRect; }, ); @@ -68,7 +68,7 @@ void main() { ); expect(constraintsTargetRect, offsetTargetRect); expect(offsetSourceSize, const Size(42, 42)); - expect(offsetOverlayRect, Offset.zero & const Size(100, 100)); + expect(offsetTheaterRect, Offset.zero & const Size(100, 100)); }); testWidgets('$Aligned defers to backup if needed', (tester) async { @@ -150,7 +150,7 @@ class _TestAnchor implements Anchor { final void Function( Size sourceSize, Rect targetRect, - Rect overlayRect, + Rect theaterRect, ) onGetSourceOffset; @override @@ -166,9 +166,9 @@ class _TestAnchor implements Anchor { Offset getSourceOffset({ required Size sourceSize, required Rect targetRect, - required Rect overlayRect, + required Rect theaterRect, }) { - onGetSourceOffset(sourceSize, targetRect, overlayRect); + onGetSourceOffset(sourceSize, targetRect, theaterRect); return Offset.zero; } } @@ -194,13 +194,13 @@ class _TestAligned extends Aligned { Offset getSourceOffset({ required Size sourceSize, required Rect targetRect, - required Rect overlayRect, + required Rect theaterRect, }) { onGetSourceOffset(); return super.getSourceOffset( sourceSize: sourceSize, targetRect: targetRect, - overlayRect: overlayRect, + theaterRect: theaterRect, ); } } From c3fac4b06614ee83b60f80b739da43535c4cd533 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 24 Oct 2021 21:35:25 +0000 Subject: [PATCH 10/35] Finalize custom follower implementation --- lib/src/custom_follower.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/src/custom_follower.dart b/lib/src/custom_follower.dart index 495ca2e..bc6264e 100644 --- a/lib/src/custom_follower.dart +++ b/lib/src/custom_follower.dart @@ -29,8 +29,8 @@ class CustomCompositedTransformFollower extends SingleChildRenderObjectWidget { final Size targetSize; @override - RenderFollowerLayer createRenderObject(BuildContext context) { - return RenderFollowerLayer( + CustomRenderFollowerLayer createRenderObject(BuildContext context) { + return CustomRenderFollowerLayer( anchor: anchor, link: link, overlayLink: overlayLink, @@ -41,7 +41,7 @@ class CustomCompositedTransformFollower extends SingleChildRenderObjectWidget { @override void updateRenderObject( BuildContext context, - RenderFollowerLayer renderObject, + CustomRenderFollowerLayer renderObject, ) { renderObject ..link = link @@ -62,9 +62,10 @@ class CustomCompositedTransformFollower extends SingleChildRenderObjectWidget { } /// @nodoc -class RenderFollowerLayer extends RenderProxyBox { +@visibleForTesting +class CustomRenderFollowerLayer extends RenderProxyBox { /// @nodoc - RenderFollowerLayer({ + CustomRenderFollowerLayer({ required LayerLink link, required OverlayLink overlayLink, required Size targetSize, @@ -172,7 +173,12 @@ class RenderFollowerLayer extends RenderProxyBox { // In order to compute the theater rect, we must first offset (shift) it by // the position of the top-left corner of the target in the coordinate space // of the theater since we are working with it relative to the target. - final theaterShift = -localToGlobal(link.leader!.offset); + final theaterShift = -localToGlobal( + // We know that the leader is not null at this point because of our + // CompositedTransformTarget implementation that ensures the leader is set + // in the paint call of CustomRenderTargetLayer. + link.leader!.offset, + ); final theaterRect = theaterShift & theater.size; final linkedOffset = anchor.getSourceOffset( // The size is set in performLayout of the RenderProxyBoxMixin. From b629834e2f1fda6e47a8e1f22725a4407cc0d716 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 24 Oct 2021 23:47:00 +0000 Subject: [PATCH 11/35] Implement custom FollowerLayer --- example/lib/contextual_menu.dart | 63 +++++++- lib/src/custom_follower.dart | 260 +++++++++++++++++++++++++++++-- test/anchor_test.dart | 48 +++--- 3 files changed, 335 insertions(+), 36 deletions(-) diff --git a/example/lib/contextual_menu.dart b/example/lib/contextual_menu.dart index e124346..f5e74ce 100644 --- a/example/lib/contextual_menu.dart +++ b/example/lib/contextual_menu.dart @@ -3,7 +3,49 @@ import 'package:flutter_portal/flutter_portal.dart'; // a contextual menu -void main() => runApp(const MyApp()); +void main() { + runApp(MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: ColoredBox( + color: Colors.green, + child: Portal( + child: Center( + child: ColoredBox( + color: Colors.white, + child: SizedBox( + width: 50, + height: 50, + child: PortalEntry( + anchor: _TestAnchor(), + portal: const ColoredBox( + color: Colors.red, + ), + child: const Center( + child: ColoredBox( + color: Colors.black, + child: SizedBox( + width: 20, + height: 20, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + )); +} + +// void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @@ -132,3 +174,22 @@ class _ModalEntry extends StatelessWidget { ); } } + +class _TestAnchor implements Anchor { + @override + BoxConstraints getSourceConstraints({ + required Rect targetRect, + required BoxConstraints overlayConstraints, + }) { + return const BoxConstraints.tightFor(width: 42, height: 42); + } + + @override + Offset getSourceOffset({ + required Size sourceSize, + required Rect targetRect, + required Rect theaterRect, + }) { + return Offset.zero; + } +} diff --git a/lib/src/custom_follower.dart b/lib/src/custom_follower.dart index bc6264e..39f5a97 100644 --- a/lib/src/custom_follower.dart +++ b/lib/src/custom_follower.dart @@ -1,5 +1,8 @@ +import 'dart:ui'; + import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:vector_math/vector_math_64.dart'; import 'anchor.dart'; import 'portal.dart'; @@ -137,7 +140,7 @@ class CustomRenderFollowerLayer extends RenderProxyBox { bool get alwaysNeedsCompositing => true; @override - FollowerLayer? get layer => super.layer as FollowerLayer?; + _CustomFollowerLayer? get layer => super.layer as _CustomFollowerLayer?; /// @nodoc Matrix4 getCurrentTransform() { @@ -160,8 +163,15 @@ class CustomRenderFollowerLayer extends RenderProxyBox { ); } - @override - void paint(PaintingContext context, Offset offset) { + /// Returns the linked offset in relation to the leader layer. + /// + /// The [LeaderLayer] is inserted by the [CompositedTransformTarget] in + /// [PortalEntry]. + /// + /// The reason we cannot simply access the [link]'s leader in [paint] is that + /// the leader is only attached to the [LayerLink] in [LeaderLayer.attach], + /// which is called in the compositing phase which is after the paint phase. + Offset _computeLinkedOffset(Offset leaderOffset) { assert( overlayLink.theater != null, 'The theater must be set in the OverlayLink when the ' @@ -173,31 +183,33 @@ class CustomRenderFollowerLayer extends RenderProxyBox { // In order to compute the theater rect, we must first offset (shift) it by // the position of the top-left corner of the target in the coordinate space // of the theater since we are working with it relative to the target. - final theaterShift = -localToGlobal( - // We know that the leader is not null at this point because of our - // CompositedTransformTarget implementation that ensures the leader is set - // in the paint call of CustomRenderTargetLayer. - link.leader!.offset, + final theaterShift = -globalToLocal( + leaderOffset, + ancestor: theater, ); + + final targetRect = Offset.zero & targetSize; final theaterRect = theaterShift & theater.size; - final linkedOffset = anchor.getSourceOffset( + + return anchor.getSourceOffset( // The size is set in performLayout of the RenderProxyBoxMixin. sourceSize: size, - targetRect: Rect.fromLTWH(0, 0, targetSize.width, targetSize.height), + targetRect: targetRect, theaterRect: theaterRect, ); + } + @override + void paint(PaintingContext context, Offset offset) { if (layer == null) { - layer = FollowerLayer( + layer = _CustomFollowerLayer( link: link, - showWhenUnlinked: false, - linkedOffset: linkedOffset, + linkedOffsetCallback: _computeLinkedOffset, ); } else { layer! ..link = link - ..showWhenUnlinked = false - ..linkedOffset = linkedOffset; + ..linkedOffsetCallback = _computeLinkedOffset; } context.pushLayer( @@ -232,3 +244,221 @@ class CustomRenderFollowerLayer extends RenderProxyBox { properties.add(DiagnosticsProperty('targetSize', targetSize)); } } + +/// A composited layer that applies a transformation matrix to its children such +/// that they are positioned based on the position of a [LeaderLayer] and some +/// extra computation performed by a callback. +/// +/// Note that this is like [FollowerLayer] but instead of taking a +/// [FollowerLayer.linkedOffset], it takes a [linkedOffsetCallback] to compute +/// this offset. +/// +/// This custom follower layer does not do anything if unlinked (equal to +/// [FollowerLayer.unlinkedOffset] being [Offset.zero]). +/// +/// For documentation of undocumented code, see [FollowerLayer]. +class _CustomFollowerLayer extends ContainerLayer { + /// Creates a follower layer. + /// + /// The [link] property must not be null. + _CustomFollowerLayer({ + required this.link, + required this.linkedOffsetCallback, + }); + + LayerLink link; + + /// Callback that is called to compute the linked offset of the follower layer + /// based on the `leaderOffset` of the leader layer. + /// + /// This is like [FollowerLayer.linkedOffset] but as a callback. Note that + /// this has the *exact* function of [FollowerLayer.linkedOffset] and + /// therefore the leader layer offset does not need to be added to the + /// returned offset. The returned offset should only be the offset from the + /// leader layer. + /// The `leaderOffset` is only passed in case it needs to be used inside of + /// the callback for computation reasons. + Offset Function(Offset leaderOffset) linkedOffsetCallback; + + Offset? _lastOffset; + Matrix4? _lastTransform; + Matrix4? _invertedTransform; + bool _inverseDirty = true; + + Offset? _transformOffset(Offset localPosition) { + if (_inverseDirty) { + _invertedTransform = Matrix4.tryInvert(getLastTransform()!); + _inverseDirty = false; + } + if (_invertedTransform == null) { + return null; + } + final vector = Vector4(localPosition.dx, localPosition.dy, 0, 1); + final result = _invertedTransform!.transform(vector); + // We know the link leader cannot be null since we return early in + // findAnnotations otherwise. + final linkedOffset = linkedOffsetCallback(link.leader!.offset); + return Offset(result[0] - linkedOffset.dx, result[1] - linkedOffset.dy); + } + + @override + bool findAnnotations( + AnnotationResult result, Offset localPosition, + {required bool onlyFirst}) { + if (link.leader == null) { + return false; + } + final transformedOffset = _transformOffset(localPosition); + if (transformedOffset == null) { + return false; + } + return super + .findAnnotations(result, transformedOffset, onlyFirst: onlyFirst); + } + + Matrix4? getLastTransform() { + if (_lastTransform == null) { + return null; + } + final result = + Matrix4.translationValues(-_lastOffset!.dx, -_lastOffset!.dy, 0); + result.multiply(_lastTransform!); + return result; + } + + static Matrix4 _collectTransformForLayerChain(List layers) { + // Initialize our result matrix. + final result = Matrix4.identity(); + // Apply each layer to the matrix in turn, starting from the last layer, + // and providing the previous layer as the child. + for (var index = layers.length - 1; index > 0; index -= 1) { + layers[index]?.applyTransform(layers[index - 1], result); + } + return result; + } + + static Layer? _pathsToCommonAncestor( + Layer? a, + Layer? b, + List ancestorsA, + List ancestorsB, + ) { + // No common ancestor found. + if (a == null || b == null) { + return null; + } + + if (identical(a, b)) { + return a; + } + + if (a.depth < b.depth) { + ancestorsB.add(b.parent); + return _pathsToCommonAncestor(a, b.parent, ancestorsA, ancestorsB); + } else if (a.depth > b.depth) { + ancestorsA.add(a.parent); + return _pathsToCommonAncestor(a.parent, b, ancestorsA, ancestorsB); + } + + ancestorsA.add(a.parent); + ancestorsB.add(b.parent); + return _pathsToCommonAncestor(a.parent, b.parent, ancestorsA, ancestorsB); + } + + void _establishTransform() { + _lastTransform = null; + final leader = link.leader; + // Check to see if we are linked. + if (leader == null) { + return; + } + // If we're linked, check the link is valid. + assert( + leader.owner == owner, + 'Linked LeaderLayer anchor is not in the same layer tree as the FollowerLayer.', + ); + + // Stores [leader, ..., commonAncestor] after calling _pathsToCommonAncestor. + final List forwardLayers = [leader]; + // Stores [this (follower), ..., commonAncestor] after calling + // _pathsToCommonAncestor. + final List inverseLayers = [this]; + + final ancestor = _pathsToCommonAncestor( + leader, + this, + forwardLayers, + inverseLayers, + ); + assert(ancestor != null); + + final forwardTransform = _collectTransformForLayerChain(forwardLayers); + // Further transforms the coordinate system to a hypothetical child (null) + // of the leader layer, to account for the leader's additional paint offset + // and layer offset (LeaderLayer._lastOffset). + leader.applyTransform(null, forwardTransform); + final linkedOffset = linkedOffsetCallback(leader.offset); + forwardTransform.translate(linkedOffset.dx, linkedOffset.dy); + + final inverseTransform = _collectTransformForLayerChain(inverseLayers); + + if (inverseTransform.invert() == 0.0) { + // We are in a degenerate transform, so there's not much we can do. + return; + } + // Combine the matrices and store the result. + inverseTransform.multiply(forwardTransform); + _lastTransform = inverseTransform; + _inverseDirty = true; + } + + @override + bool get alwaysNeedsAddToScene => true; + + @override + void addToScene(SceneBuilder builder, [Offset layerOffset = Offset.zero]) { + if (link.leader == null) { + _lastTransform = null; + _lastOffset = null; + _inverseDirty = true; + engineLayer = null; + return; + } + _establishTransform(); + if (_lastTransform != null) { + engineLayer = builder.pushTransform( + _lastTransform!.storage, + oldLayer: engineLayer as TransformEngineLayer?, + ); + addChildrenToScene(builder); + builder.pop(); + _lastOffset = layerOffset; + } else { + _lastOffset = null; + final matrix = Matrix4.translationValues(0, 0, 0); + engineLayer = builder.pushTransform( + matrix.storage, + oldLayer: engineLayer as TransformEngineLayer?, + ); + addChildrenToScene(builder); + builder.pop(); + } + _inverseDirty = true; + } + + @override + void applyTransform(Layer? child, Matrix4 transform) { + assert(child != null); + if (_lastTransform != null) { + transform.multiply(_lastTransform!); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('link', link)); + properties.add( + TransformProperty('transform', getLastTransform(), defaultValue: null)); + } +} diff --git a/test/anchor_test.dart b/test/anchor_test.dart index 0a07b72..4ed7985 100644 --- a/test/anchor_test.dart +++ b/test/anchor_test.dart @@ -14,11 +14,9 @@ void main() { Rect? offsetTargetRect; Rect? offsetTheaterRect; final anchor = _TestAnchor( - constraints: const BoxConstraints( - minWidth: 42, - maxWidth: 42, - minHeight: 42, - maxHeight: 42, + constraints: const BoxConstraints.tightFor( + width: 42, + height: 42, ), onGetSourceConstraints: (targetRect, overlayConstraints) { constraintsTargetRect = targetRect; @@ -37,20 +35,30 @@ void main() { child: SizedBox( width: 100, height: 100, - child: Portal( - child: Center( - child: SizedBox( - width: 50, - height: 50, - child: PortalEntry( - anchor: anchor, - portal: const SizedBox( - width: 30, - height: 30, - ), - child: const SizedBox( - width: 20, - height: 20, + child: ColoredBox( + color: Colors.green, + child: Portal( + child: Center( + child: ColoredBox( + color: Colors.white, + child: SizedBox( + width: 50, + height: 50, + child: PortalEntry( + anchor: anchor, + portal: const ColoredBox( + color: Colors.red, + ), + child: const Center( + child: ColoredBox( + color: Colors.black, + child: SizedBox( + width: 20, + height: 20, + ), + ), + ), + ), ), ), ), @@ -68,7 +76,7 @@ void main() { ); expect(constraintsTargetRect, offsetTargetRect); expect(offsetSourceSize, const Size(42, 42)); - expect(offsetTheaterRect, Offset.zero & const Size(100, 100)); + expect(offsetTheaterRect, const Offset(-25, -25) & const Size(100, 100)); }); testWidgets('$Aligned defers to backup if needed', (tester) async { From 5c7833a79bcddf01de592d471570bdbe12862db0 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 24 Oct 2021 23:49:42 +0000 Subject: [PATCH 12/35] Revert from debug example --- example/lib/contextual_menu.dart | 63 +------------------------------- 1 file changed, 1 insertion(+), 62 deletions(-) diff --git a/example/lib/contextual_menu.dart b/example/lib/contextual_menu.dart index f5e74ce..e124346 100644 --- a/example/lib/contextual_menu.dart +++ b/example/lib/contextual_menu.dart @@ -3,49 +3,7 @@ import 'package:flutter_portal/flutter_portal.dart'; // a contextual menu -void main() { - runApp(MaterialApp( - home: Scaffold( - body: Center( - child: SizedBox( - width: 100, - height: 100, - child: ColoredBox( - color: Colors.green, - child: Portal( - child: Center( - child: ColoredBox( - color: Colors.white, - child: SizedBox( - width: 50, - height: 50, - child: PortalEntry( - anchor: _TestAnchor(), - portal: const ColoredBox( - color: Colors.red, - ), - child: const Center( - child: ColoredBox( - color: Colors.black, - child: SizedBox( - width: 20, - height: 20, - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - )); -} - -// void main() => runApp(const MyApp()); +void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @@ -174,22 +132,3 @@ class _ModalEntry extends StatelessWidget { ); } } - -class _TestAnchor implements Anchor { - @override - BoxConstraints getSourceConstraints({ - required Rect targetRect, - required BoxConstraints overlayConstraints, - }) { - return const BoxConstraints.tightFor(width: 42, height: 42); - } - - @override - Offset getSourceOffset({ - required Size sourceSize, - required Rect targetRect, - required Rect theaterRect, - }) { - return Offset.zero; - } -} From ab6079b723868f5b1de8e1c0b9fbd9e0d6e431f3 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Mon, 25 Oct 2021 00:01:19 +0000 Subject: [PATCH 13/35] Make tests pass --- lib/src/anchor.dart | 17 ++++++++++++++++- test/anchor_test.dart | 8 +++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart index 9671243..eb559de 100644 --- a/lib/src/anchor.dart +++ b/lib/src/anchor.dart @@ -203,5 +203,20 @@ extension _RectAnchorExt on Rect { /// If the [rect] has any part that lies outside of this parent /// false will be returned bool fullyContains(Rect rect) => - contains(rect.topLeft) && contains(rect.bottomRight); + containsIncludingBottomAndRightEdge(rect.topLeft) && + containsIncludingBottomAndRightEdge(rect.bottomRight); + + /// Whether the point specified by the given offset (which is assumed to be + /// relative to the origin) lies between the left and right and the top and + /// bottom edges of this rectangle. + /// + /// This is like [contains] but also includes the bottom edge and the right + /// edge because in the context of painting, it would make no sense to + /// consider a rect as overflowing when it lines up exactly with another rect. + bool containsIncludingBottomAndRightEdge(Offset offset) { + return offset.dx >= left && + offset.dx <= right && + offset.dy >= top && + offset.dy <= bottom; + } } diff --git a/test/anchor_test.dart b/test/anchor_test.dart index 4ed7985..134c36e 100644 --- a/test/anchor_test.dart +++ b/test/anchor_test.dart @@ -96,11 +96,9 @@ void main() { width: 20, height: 20, ), - child: const Center( - child: SizedBox( - width: 10, - height: 10, - ), + child: const SizedBox( + width: 10, + height: 10, ), ); From da7a6dc83ed25896d2380b0995e4bf5d7d940ea0 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Fri, 28 Jan 2022 16:09:36 +0000 Subject: [PATCH 14/35] Declare the main concepts --- README.md | 24 ++++++++++++++++++++++++ example/pubspec.lock | 12 ++++++------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ec4ef24..d57dfe3 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,30 @@ Center( ) ``` +## Concepts + +There are a few concepts that are useful to fully understand when using +`flutter_portal`. That is especially true if you want to support custom use +cases, which is easily possible with the abstract API provided. + +In the following, each of the abstract concepts you need to understand are +explained on a high level. You will find them both in class names (e.g. the +`Portal` widget or the `PortalTarget` widget as well as in parameter names). + +### Portal + + + +### Target + + +### Follower + + +### Anchor + + + [overlay]: https://api.flutter.dev/flutter/widgets/Overlay-class.html [overlayentry]: https://api.flutter.dev/flutter/widgets/OverlayEntry-class.html [addpostframecallback]: https://api.flutter.dev/flutter/scheduler/SchedulerBinding/addPostFrameCallback.html diff --git a/example/pubspec.lock b/example/pubspec.lock index bb64a0b..d0756ea 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,7 +21,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -80,7 +80,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" meta: dependency: transitive description: @@ -141,7 +141,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.3" typed_data: dependency: transitive description: @@ -155,7 +155,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.14.0 <3.0.0" flutter: ">=1.21.0" From 05f4406abb8beb3f970fb3a7e72264caba19e4a3 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Fri, 28 Jan 2022 16:32:48 +0000 Subject: [PATCH 15/35] Document all concepts --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index d57dfe3..bf8b659 100644 --- a/README.md +++ b/README.md @@ -259,17 +259,44 @@ explained on a high level. You will find them both in class names (e.g. the ### Portal +A portal (or the portal if you only have one) is the space used for doing all +of the portal work. On a low level, this means that you have one widget that +allows its subtree to place targets and followers that are connected. +The portal also defines the area (rectangle bounds) that are available to any +followers to be rendered onto the screen. + +In detail, you might wrap your whole `MaterialApp` in a single `Portal` widget, +which would mean that you can use the whole area of your app to render followers +attached to targets that are children of the `Portal` widget. ### Target +A target is any place within a portal that can be followed by a follower. This +allows you to attach whatever you want to overlay to a specific place in your +UI, no matter where it moves dynamically. + +On a low level, this means that you wrap the part of your UI that you want to +follow in a `PortalTarget` widget and configure it. ### Follower +A follower can only be used in combination with a target. You can use it for +anything that you want to overlay on top of your UI, attached to a target. + +Specifically, this means that you can pass one `follower` to every +`PortalTarget`, which will be displayed above your UI within the portal when +you specify so. ### Anchor +Anchors define the layout connection between targets and followers. In general, +anchors are implemented as an abstract API that provides all the information +necessary to support any positioning you want. That means that anchors can be +defined based on the attributes of the associated portal, target, and follower. +There are a few anchors that are implemented by default, e.g. `Aligned` or +`Filled`. [overlay]: https://api.flutter.dev/flutter/widgets/Overlay-class.html [overlayentry]: https://api.flutter.dev/flutter/widgets/OverlayEntry-class.html From 69d56aee1fc9fe790c9c5a2e940f494432551dfb Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Tue, 1 Feb 2022 21:11:07 +0800 Subject: [PATCH 16/35] try to fix #42 --- lib/src/portal.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/portal.dart b/lib/src/portal.dart index da70591..6c5c166 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -151,8 +151,9 @@ class _RenderPortalTheater extends RenderProxyBox { @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + final globalPosition = localToGlobal(position); // #42 for (final overlay in overlayLink.overlays) { - if (overlay.hitTest(result, position: position)) { + if (overlay.hitTest(result, position: globalPosition /* #42 */)) { return true; } } From 51fc9dd10626784a4ab2648e8da1276858782bf5 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Mon, 7 Feb 2022 09:32:47 +0000 Subject: [PATCH 17/35] Apply new namings --- example/lib/contextual_menu.dart | 2 +- example/lib/date_picker.dart | 2 +- example/lib/discovery.dart | 4 +- example/lib/medium_clap.dart | 2 +- example/lib/modal.dart | 4 +- lib/flutter_portal.dart | 2 +- lib/src/anchor.dart | 78 ++++++++++----------- lib/src/custom_follower.dart | 8 +-- lib/src/portal.dart | 73 +++++++++---------- test/anchor_test.dart | 30 ++++---- test/widget_test.dart | 116 +++++++++++++++---------------- 11 files changed, 161 insertions(+), 160 deletions(-) diff --git a/example/lib/contextual_menu.dart b/example/lib/contextual_menu.dart index e124346..063e2f9 100644 --- a/example/lib/contextual_menu.dart +++ b/example/lib/contextual_menu.dart @@ -111,7 +111,7 @@ class _ModalEntry extends StatelessWidget { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: visible ? onClose : null, - child: PortalEntry( + child: PortalTarget( visible: visible, portal: menu, anchor: const Aligned( diff --git a/example/lib/date_picker.dart b/example/lib/date_picker.dart index d7a3c82..b6b30be 100644 --- a/example/lib/date_picker.dart +++ b/example/lib/date_picker.dart @@ -21,7 +21,7 @@ class DeclarativeDatePicker extends StatelessWidget { @override Widget build(BuildContext context) { - return PortalEntry( + return PortalTarget( visible: visible, portal: Stack( children: [ diff --git a/example/lib/discovery.dart b/example/lib/discovery.dart index 09076f1..e215ece 100644 --- a/example/lib/discovery.dart +++ b/example/lib/discovery.dart @@ -76,7 +76,7 @@ class Discovery extends StatelessWidget { return Barrier( visible: visible, onClose: onClose, - child: PortalEntry( + child: PortalTarget( visible: visible, closeDuration: kThemeAnimationDuration, anchor: const Aligned( @@ -171,7 +171,7 @@ class Barrier extends StatelessWidget { @override Widget build(BuildContext context) { - return PortalEntry( + return PortalTarget( visible: visible, closeDuration: kThemeAnimationDuration, portal: GestureDetector( diff --git a/example/lib/medium_clap.dart b/example/lib/medium_clap.dart index 4f726f5..f5fc679 100644 --- a/example/lib/medium_clap.dart +++ b/example/lib/medium_clap.dart @@ -39,7 +39,7 @@ class _ClapButtonState extends State { @override Widget build(BuildContext context) { - return PortalEntry( + return PortalTarget( visible: hasClappedRecently, // aligns the top-center of `child` with the bottom-center of `portal` anchor: const Aligned( diff --git a/example/lib/modal.dart b/example/lib/modal.dart index 3606080..2c74e8f 100644 --- a/example/lib/modal.dart +++ b/example/lib/modal.dart @@ -59,7 +59,7 @@ class Modal extends StatelessWidget { return Barrier( visible: visible, onClose: onClose, - child: PortalEntry( + child: PortalTarget( visible: visible, closeDuration: kThemeAnimationDuration, portal: TweenAnimationBuilder( @@ -97,7 +97,7 @@ class Barrier extends StatelessWidget { @override Widget build(BuildContext context) { - return PortalEntry( + return PortalTarget( visible: visible, closeDuration: kThemeAnimationDuration, portal: GestureDetector( diff --git a/lib/flutter_portal.dart b/lib/flutter_portal.dart index 815403b..cfb1c59 100644 --- a/lib/flutter_portal.dart +++ b/lib/flutter_portal.dart @@ -1,3 +1,3 @@ export 'package:flutter_portal/src/anchor.dart' show Anchor, Aligned; export 'package:flutter_portal/src/portal.dart' - show Portal, PortalEntry, PortalNotFoundError; + show Portal, PortalTarget, PortalNotFoundError; diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart index eb559de..0671e65 100644 --- a/lib/src/anchor.dart +++ b/lib/src/anchor.dart @@ -1,45 +1,45 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; -/// The logic of layout and positioning of a source element in relation to a +/// The logic of layout and positioning of a follower element in relation to a /// target element. /// /// Independent of the underlying rendering implementation. abstract class Anchor { - /// Returns the layout constraints that are given to the source element. + /// Returns the layout constraints that are given to the follower element. /// - /// The [targetRect] represents the bounds of the element which the source + /// The [targetRect] represents the bounds of the element which the follower /// element should be anchored to. This must be the same value that is passed - /// to [getSourceOffset]. No assumptions should be made about the coordinate + /// to [getFollowerOffset]. No assumptions should be made about the coordinate /// space, i.e. only the size of the target should be considered. /// - /// The [overlayConstraints] represent the full available space to place the + /// The [portalConstraints] represent the full available space to place the /// source element in. This is irrespective of where the target is positioned /// within the full available space. - BoxConstraints getSourceConstraints({ + BoxConstraints getFollowerConstraints({ required Rect targetRect, - required BoxConstraints overlayConstraints, + required BoxConstraints portalConstraints, }); - /// Returns the offset at which to position the source element in relation to + /// Returns the offset at which to position the follower element in relation /// to the top left of the [targetRect]. /// - /// The [sourceSize] is the final size of the source element after layout - /// based on the source constraints determined by [getSourceConstraints]. + /// The [followerSize] is the final size of the follower element after layout + /// based on the source constraints determined by [getFollowerConstraints]. /// - /// The [targetRect] represents the bounds of the element which the source + /// The [targetRect] represents the bounds of the element which the follower /// element should be anchored to. This must be the same value that is passed - /// to [getSourceConstraints]. + /// to [getFollowerConstraints]. /// - /// The [theaterRect] represents the bounds of the full available space to - /// place the source element in. Note that this is also relative to the top + /// The [portalRect] represents the bounds of the full available space to + /// place the follower element in. Note that this is also relative to the top /// left of the [targetRect]. - /// This means that every offset going into or coming out of this function are + /// This means that every offset going into or coming out of this function is /// relative to the top-left corner of the target. /// /// ## Example /// - /// In this example, our source element has a size of `Size(30, 30)` and + /// In this example, our follower element has a size of `Size(30, 30)` and /// should be anchored to the bottom right of the target. /// /// If we assume the full available space starts at absolute `(0, 0)` and @@ -47,13 +47,13 @@ abstract class Anchor { /// `(40, 40)` and spans to absolute `(60, 60)`, the passed values will be: /// /// * `Rect.fromLTWH(0, 0, 20, 20)` for the [targetRect]. - /// * `Rect.fromLTWH(-40, -40, 100, 100)` for the [theaterRect]. - /// * `Size(30, 30)` for the [sourceSize]. + /// * `Rect.fromLTWH(-40, -40, 100, 100)` for the [portalRect]. + /// * `Size(30, 30)` for the [followerSize]. /// * `Offset(20, 20)` as the return value. - Offset getSourceOffset({ - required Size sourceSize, + Offset getFollowerOffset({ + required Size followerSize, required Rect targetRect, - required Rect theaterRect, + required Rect portalRect, }); } @@ -64,18 +64,18 @@ class Filled implements Anchor { const Filled(); @override - BoxConstraints getSourceConstraints({ + BoxConstraints getFollowerConstraints({ required Rect targetRect, - required BoxConstraints overlayConstraints, + required BoxConstraints portalConstraints, }) { - return BoxConstraints.tight(overlayConstraints.biggest); + return BoxConstraints.tight(portalConstraints.biggest); } @override - Offset getSourceOffset({ - required Size sourceSize, + Offset getFollowerOffset({ + required Size followerSize, required Rect targetRect, - required Rect theaterRect, + required Rect portalRect, }) { return Offset.zero; } @@ -126,14 +126,14 @@ class Aligned implements Anchor { final Anchor? backup; @override - BoxConstraints getSourceConstraints({ + BoxConstraints getFollowerConstraints({ required Rect targetRect, - required BoxConstraints overlayConstraints, + required BoxConstraints portalConstraints, }) { final widthFactor = this.widthFactor; final heightFactor = this.heightFactor; - return overlayConstraints.loosen().tighten( + return portalConstraints.loosen().tighten( width: widthFactor == null ? null : targetRect.width * widthFactor, height: heightFactor == null ? null : targetRect.height * heightFactor, @@ -141,25 +141,25 @@ class Aligned implements Anchor { } @override - Offset getSourceOffset({ - required Size sourceSize, + Offset getFollowerOffset({ + required Size followerSize, required Rect targetRect, - required Rect theaterRect, + required Rect portalRect, }) { - final sourceRect = (Offset.zero & sourceSize).alignedTo( + final sourceRect = (Offset.zero & followerSize).alignedTo( targetRect, sourceAlignment: source, targetAlignment: target, offset: offset, ); - if (!theaterRect.fullyContains(sourceRect)) { + if (!portalRect.fullyContains(sourceRect)) { final backup = this.backup; if (backup != null) { - return backup.getSourceOffset( - sourceSize: sourceSize, + return backup.getFollowerOffset( + followerSize: followerSize, targetRect: targetRect, - theaterRect: theaterRect, + portalRect: portalRect, ); } } @@ -185,7 +185,7 @@ class Aligned implements Anchor { int get hashCode => source.hashCode ^ target.hashCode ^ offset.hashCode; } -extension _RectAnchorExt on Rect { +extension on Rect { Rect alignedTo( Rect target, { required Alignment sourceAlignment, diff --git a/lib/src/custom_follower.dart b/lib/src/custom_follower.dart index 39f5a97..981750d 100644 --- a/lib/src/custom_follower.dart +++ b/lib/src/custom_follower.dart @@ -166,7 +166,7 @@ class CustomRenderFollowerLayer extends RenderProxyBox { /// Returns the linked offset in relation to the leader layer. /// /// The [LeaderLayer] is inserted by the [CompositedTransformTarget] in - /// [PortalEntry]. + /// [PortalTarget]. /// /// The reason we cannot simply access the [link]'s leader in [paint] is that /// the leader is only attached to the [LayerLink] in [LeaderLayer.attach], @@ -191,11 +191,11 @@ class CustomRenderFollowerLayer extends RenderProxyBox { final targetRect = Offset.zero & targetSize; final theaterRect = theaterShift & theater.size; - return anchor.getSourceOffset( + return anchor.getFollowerOffset( // The size is set in performLayout of the RenderProxyBoxMixin. - sourceSize: size, + followerSize: size, targetRect: targetRect, - theaterRect: theaterRect, + portalRect: theaterRect, ); } diff --git a/lib/src/portal.dart b/lib/src/portal.dart index ec7242a..6e40bb6 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -3,19 +3,18 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; import 'anchor.dart'; import 'custom_follower.dart'; -/// The widget where a [PortalEntry] is rendered. +/// The widget where a [PortalTarget] is rendered. /// /// [Portal] can be considered as a reimplementation of [Overlay] to allow -/// adding an [OverlayEntry] (now named [PortalEntry]) declaratively. +/// adding an [OverlayEntry] (now named [PortalTarget]) declaratively. /// -/// [Portal] widget is used in co-ordination with [PortalEntry] widget to show +/// [Portal] widget is used in co-ordination with [PortalTarget] widget to show /// some content _above_ another content. -/// This is similar to [Stack] in principle, with the difference that [PortalEntry] +/// This is similar to [Stack] in principle, with the difference that [PortalTarget] /// does not have to be a direct child of [Portal] and can instead be placed /// anywhere in the widget tree. /// @@ -174,21 +173,21 @@ class _RenderPortalTheater extends RenderProxyBox { /// A widget that renders its content in a different location of the widget tree. /// -/// In short, you can use [PortalEntry] to show dialogs, tooltips, contextual menus, ... +/// In short, you can use [PortalTarget] to show dialogs, tooltips, contextual menus, ... /// You can then control the visibility of these overlays with a simple `setState`. /// -/// The benefits of using [PortalEntry] over [Overlay]/[OverlayEntry] are multiple: -/// - [PortalEntry] is easier to manipulate +/// The benefits of using [PortalTarget] over [Overlay]/[OverlayEntry] are multiple: +/// - [PortalTarget] is easier to manipulate /// - It allows aligning your menus/tooltips next to a button easily /// - It combines nicely with state-management solutions and the "state-restoration" /// framework. For example, combined with [RestorableProperty], when the application /// is killed then re-opened, modals/menus would be restored. /// -/// For [PortalEntry] to work, make sure to insert [Portal] higher in the widget-tree. +/// For [PortalTarget] to work, make sure to insert [Portal] higher in the widget-tree. /// /// ## Contextual menu example /// -/// In this example, we will see how we can use [PortalEntry] to show a menu +/// In this example, we will see how we can use [PortalTarget] to show a menu /// after clicking on a [ElevatedButton]. /// /// First, we need to create a [StatefulWidget] that renders our [ElevatedButton]: @@ -214,10 +213,10 @@ class _RenderPortalTheater extends RenderProxyBox { /// } /// ``` /// -/// Then, we need to insert our [PortalEntry] in the widget tree. +/// Then, we need to insert our [PortalTarget] in the widget tree. /// /// We want our contextual menu to render right next to our [ElevatedButton]. -/// As such, our [PortalEntry] should be the parent of [ElevatedButton] like so: +/// As such, our [PortalTarget] should be the parent of [ElevatedButton] like so: /// /// ```dart /// Center( @@ -319,7 +318,7 @@ class _RenderPortalTheater extends RenderProxyBox { /// One final step is to close the menu when the user clicks randomly outside /// of the menu. /// -/// This can be implemented with a second [PortalEntry] combined with [GestureDetector] +/// This can be implemented with a second [PortalTarget] combined with [GestureDetector] /// like so: /// /// @@ -343,8 +342,8 @@ class _RenderPortalTheater extends RenderProxyBox { /// ), /// ) /// ``` -class PortalEntry extends StatefulWidget { - const PortalEntry({ +class PortalTarget extends StatefulWidget { + const PortalTarget({ Key? key, this.visible = true, this.anchor = const Filled(), @@ -362,7 +361,7 @@ class PortalEntry extends StatefulWidget { final Duration? closeDuration; @override - _PortalEntryState createState() => _PortalEntryState(); + _PortalTargetState createState() => _PortalTargetState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -375,13 +374,13 @@ class PortalEntry extends StatefulWidget { } } -class _PortalEntryState extends State { +class _PortalTargetState extends State { final _link = LayerLink(); late bool _visible = widget.visible; Timer? _timer; @override - void didUpdateWidget(PortalEntry oldWidget) { + void didUpdateWidget(PortalTarget oldWidget) { super.didUpdateWidget(oldWidget); if (!widget.visible) { if (!oldWidget.visible && _visible) { @@ -410,7 +409,7 @@ class _PortalEntryState extends State { } if (widget.anchor is Filled) { - return _PortalEntryTheater( + return _PortalTargetTheater( portal: _visible ? widget.portal : null, anchor: widget.anchor, targetSize: Size.zero, @@ -431,7 +430,7 @@ class _PortalEntryState extends State { builder: (context, constraints) { final targetSize = constraints.biggest; - return _PortalEntryTheater( + return _PortalTargetTheater( overlayLink: scope._overlayLink, anchor: widget.anchor, targetSize: targetSize, @@ -458,8 +457,8 @@ class _PortalEntryState extends State { } } -class _PortalEntryTheater extends SingleChildRenderObjectWidget { - const _PortalEntryTheater({ +class _PortalTargetTheater extends SingleChildRenderObjectWidget { + const _PortalTargetTheater({ Key? key, required this.portal, required this.overlayLink, @@ -475,7 +474,7 @@ class _PortalEntryTheater extends SingleChildRenderObjectWidget { @override RenderObject createRenderObject(BuildContext context) { - return _RenderPortalEntry( + return _RenderPortalTarget( overlayLink, anchor: anchor, targetSize: targetSize, @@ -485,7 +484,7 @@ class _PortalEntryTheater extends SingleChildRenderObjectWidget { @override void updateRenderObject( BuildContext context, - _RenderPortalEntry renderObject, + _RenderPortalTarget renderObject, ) { renderObject ..overlayLink = overlayLink @@ -507,10 +506,12 @@ class _PortalEntryTheater extends SingleChildRenderObjectWidget { } } -class _RenderPortalEntry extends RenderProxyBox { - _RenderPortalEntry(this._overlayLink, - {required Anchor anchor, required Size targetSize}) - : assert(_overlayLink.theater != null), +class _RenderPortalTarget extends RenderProxyBox { + _RenderPortalTarget( + this._overlayLink, { + required Anchor anchor, + required Size targetSize, + }) : assert(_overlayLink.theater != null), _anchor = anchor, _targetSize = targetSize; @@ -601,8 +602,8 @@ class _RenderPortalEntry extends RenderProxyBox { void performLayout() { super.performLayout(); if (branch != null) { - final constraints = anchor.getSourceConstraints( - overlayConstraints: overlayLink.constraints!, + final constraints = anchor.getFollowerConstraints( + portalConstraints: overlayLink.constraints!, targetRect: Offset.zero & targetSize, ); branch!.layout(constraints); @@ -650,14 +651,14 @@ class _RenderPortalEntry extends RenderProxyBox { } class _PortalEntryElement extends SingleChildRenderObjectElement { - _PortalEntryElement(_PortalEntryTheater widget) : super(widget); + _PortalEntryElement(_PortalTargetTheater widget) : super(widget); @override - _PortalEntryTheater get widget => super.widget as _PortalEntryTheater; + _PortalTargetTheater get widget => super.widget as _PortalTargetTheater; @override - _RenderPortalEntry get renderObject => - super.renderObject as _RenderPortalEntry; + _RenderPortalTarget get renderObject => + super.renderObject as _RenderPortalTarget; Element? _branch; @@ -723,11 +724,11 @@ class _PortalEntryElement extends SingleChildRenderObjectElement { } } -/// The error that will be thrown if [PortalEntry] fails to find a [Portal]. +/// The error that is thrown when a [PortalTarget] fails to find a [Portal]. class PortalNotFoundError extends Error { PortalNotFoundError._(this._portalEntry); - final PortalEntry _portalEntry; + final PortalTarget _portalEntry; @override String toString() { diff --git a/test/anchor_test.dart b/test/anchor_test.dart index 134c36e..4aa7c14 100644 --- a/test/anchor_test.dart +++ b/test/anchor_test.dart @@ -44,7 +44,7 @@ void main() { child: SizedBox( width: 50, height: 50, - child: PortalEntry( + child: PortalTarget( anchor: anchor, portal: const ColoredBox( color: Colors.red, @@ -86,7 +86,7 @@ void main() { target: Alignment.topLeft, onGetSourceOffset: () => offsetAccessed = true, ); - final entry = PortalEntry( + final entry = PortalTarget( anchor: Aligned( source: Alignment.topLeft, target: Alignment.bottomLeft, @@ -160,21 +160,21 @@ class _TestAnchor implements Anchor { ) onGetSourceOffset; @override - BoxConstraints getSourceConstraints({ + BoxConstraints getFollowerConstraints({ required Rect targetRect, - required BoxConstraints overlayConstraints, + required BoxConstraints portalConstraints, }) { - onGetSourceConstraints(targetRect, overlayConstraints); + onGetSourceConstraints(targetRect, portalConstraints); return constraints; } @override - Offset getSourceOffset({ - required Size sourceSize, + Offset getFollowerOffset({ + required Size followerSize, required Rect targetRect, - required Rect theaterRect, + required Rect portalRect, }) { - onGetSourceOffset(sourceSize, targetRect, theaterRect); + onGetSourceOffset(followerSize, targetRect, portalRect); return Offset.zero; } } @@ -197,16 +197,16 @@ class _TestAligned extends Aligned { final VoidCallback onGetSourceOffset; @override - Offset getSourceOffset({ - required Size sourceSize, + Offset getFollowerOffset({ + required Size followerSize, required Rect targetRect, - required Rect theaterRect, + required Rect portalRect, }) { onGetSourceOffset(); - return super.getSourceOffset( - sourceSize: sourceSize, + return super.getFollowerOffset( + followerSize: followerSize, targetRect: targetRect, - theaterRect: theaterRect, + portalRect: portalRect, ); } } diff --git a/test/widget_test.dart b/test/widget_test.dart index 9de82cd..0cd696f 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -45,7 +45,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( closeDuration: Duration(seconds: 5), portal: Text('portal'), child: Text('child'), @@ -59,7 +59,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( visible: false, closeDuration: Duration(seconds: 6), portal: Text('portal'), @@ -85,7 +85,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( closeDuration: Duration(seconds: 5), anchor: Aligned( target: Alignment.center, @@ -103,7 +103,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( visible: false, closeDuration: Duration(seconds: 6), anchor: Aligned.center, @@ -129,7 +129,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( closeDuration: Duration(seconds: 5), portal: Text('portal'), child: Text('child'), @@ -143,7 +143,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( visible: false, closeDuration: Duration(seconds: 5), portal: Text('portal'), @@ -164,7 +164,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( closeDuration: Duration(seconds: 5), portal: Text('portal'), child: Text('child'), @@ -178,7 +178,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( visible: false, closeDuration: Duration(seconds: 5), portal: Text('portal2'), @@ -194,7 +194,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( visible: false, closeDuration: Duration(seconds: 20), portal: Text('portal3'), @@ -216,7 +216,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( closeDuration: Duration(seconds: 5), portal: Text('portal'), child: Text('child'), @@ -230,7 +230,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( visible: false, closeDuration: Duration(seconds: 5), portal: Text('portal'), @@ -245,7 +245,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( closeDuration: Duration(seconds: 5), portal: Text('portal'), child: Text('child'), @@ -269,7 +269,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( closeDuration: Duration(seconds: 5), portal: Text('portal'), child: Text('child'), @@ -281,7 +281,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( visible: false, closeDuration: Duration(seconds: 5), portal: Text('portal'), @@ -296,7 +296,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( closeDuration: Duration(seconds: 5), portal: Text('portal'), child: Text('child'), @@ -312,7 +312,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( visible: false, closeDuration: Duration(seconds: 5), portal: Text('portal'), @@ -337,7 +337,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( portal: Text('portal'), child: Text('child'), ), @@ -350,7 +350,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( visible: false, portal: Text('portal'), child: Text('child'), @@ -367,7 +367,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( portal: Text('portal'), child: Text('child'), ), @@ -386,7 +386,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( anchor: Aligned.center, portal: Text('portal'), child: Text('child'), @@ -400,7 +400,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( visible: false, anchor: Aligned.center, portal: Text('portal'), @@ -418,7 +418,7 @@ Future main() async { await tester.pumpWidget( const Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( anchor: Aligned.center, portal: Text('portal'), child: Text('child'), @@ -461,7 +461,7 @@ Future main() async { test('PortalEntry requires portal if visible is true ', () { expect( - () => PortalEntry(child: Container()), + () => PortalTarget(child: Container()), throwsAssertionError, ); }); @@ -481,7 +481,7 @@ Future main() async { Boilerplate( child: Portal( child: Center( - child: PortalEntry( + child: PortalTarget( portal: firstPortal, child: firstChild, ), @@ -506,7 +506,7 @@ Future main() async { "portals aren't inserted if mounted is false, and visible can be changed any time", (tester) async { final portal = ValueNotifier( - PortalEntry( + PortalTarget( visible: false, portal: Builder(builder: (_) => throw Error()), child: const Text('firstChild'), @@ -528,7 +528,7 @@ Future main() async { final portalChildElement = tester.element(find.text('firstChild')); - portal.value = const PortalEntry( + portal.value = const PortalTarget( portal: Text('secondPortal'), child: Text('secondChild'), ); @@ -544,7 +544,7 @@ Future main() async { reason: 'the child state must be preserved when toggling `visible`', ); - portal.value = PortalEntry( + portal.value = PortalTarget( visible: false, portal: Builder(builder: (_) => throw Error()), child: const Text('thirdChild'), @@ -573,7 +573,7 @@ Future main() async { final portal = ValueNotifier( Center( - child: PortalEntry( + child: PortalTarget( portal: portalChild, child: Center(child: child), ), @@ -606,7 +606,7 @@ Future main() async { testWidgets('throws if no PortalEntry were found', (tester) async { await tester.pumpWidget( - const PortalEntry( + const PortalTarget( closeDuration: Duration(seconds: 5), portal: Text('portal', textDirection: TextDirection.ltr), child: Text('child', textDirection: TextDirection.ltr), @@ -634,13 +634,13 @@ Future main() async { child: ValueListenableBuilder( valueListenable: notifier, builder: (c, value, _) { - return PortalEntry( + return PortalTarget( visible: value, portal: Container( color: Colors.red.withAlpha(122), ), child: Center( - child: PortalEntry( + child: PortalTarget( visible: value, portal: Container( height: 50, @@ -686,7 +686,7 @@ Future main() async { Boilerplate( child: Portal( child: Center( - child: PortalEntry( + child: PortalTarget( portal: portal, child: child, ), @@ -730,7 +730,7 @@ Future main() async { expect(find.byWidget(first), findsOneWidget); - child.value = PortalEntry( + child.value = PortalTarget( portal: portal, child: second, ); @@ -753,7 +753,7 @@ Future main() async { await tester.pumpWidget( Boilerplate( child: Portal( - child: PortalEntry( + child: PortalTarget( portal: ElevatedButton( onPressed: () => portalClickCount++, child: const Text('portal'), @@ -783,7 +783,7 @@ Future main() async { child: Portal( // center the entry otherwise the portal is outside the screen child: Center( - child: PortalEntry( + child: PortalTarget( anchor: const Aligned( source: Alignment.bottomCenter, target: Alignment.topCenter, @@ -822,7 +822,7 @@ Future main() async { child: Portal( child: Align( alignment: Alignment.topLeft, - child: PortalEntry( + child: PortalTarget( anchor: Aligned( source: Alignment.topLeft, target: Alignment.bottomLeft), portal: SizedBox(key: portalKey, height: 42, width: 24), @@ -854,7 +854,7 @@ Future main() async { child: Portal( child: Align( alignment: Alignment.topRight, - child: PortalEntry( + child: PortalTarget( anchor: Aligned( source: Alignment.topRight, target: Alignment.bottomRight, @@ -891,7 +891,7 @@ Future main() async { child: Portal( child: Align( alignment: Alignment.bottomRight, - child: PortalEntry( + child: PortalTarget( anchor: Aligned( source: Alignment.bottomRight, target: Alignment.topRight, @@ -932,7 +932,7 @@ Future main() async { textDirection: TextDirection.ltr, child: Portal( child: Align( - child: PortalEntry( + child: PortalTarget( portal: SizedBox(key: portalKey, height: 20, width: 20), child: SizedBox(key: childKey, height: 10, width: 10), ), @@ -973,7 +973,7 @@ Future main() async { textDirection: TextDirection.ltr, child: Portal( child: Center( - child: PortalEntry( + child: PortalTarget( portal: GestureDetector( key: portalKey, onTap: () => portalClickCount++, @@ -1008,7 +1008,7 @@ Future main() async { textDirection: TextDirection.ltr, child: Portal( child: Center( - child: PortalEntry( + child: PortalTarget( anchor: const Aligned( source: Alignment.bottomCenter, target: Alignment.topCenter, @@ -1052,7 +1052,7 @@ Future main() async { textDirection: TextDirection.ltr, child: Portal( child: Center( - child: PortalEntry( + child: PortalTarget( portal: GestureDetector( key: portalKey, onTap: () => portalClickCount++, @@ -1105,7 +1105,7 @@ Future main() async { Boilerplate( child: Portal( child: Center( - child: PortalEntry( + child: PortalTarget( anchor: const Aligned( source: Alignment.bottomCenter, target: Alignment.topCenter, @@ -1142,7 +1142,7 @@ Future main() async { Boilerplate( child: Portal( child: Center( - child: PortalEntry( + child: PortalTarget( portal: Align(alignment: Alignment.topLeft, child: portal), child: child, ), @@ -1215,7 +1215,7 @@ Future main() async { await tester.pumpWidget( Portal( child: Center( - child: PortalEntry( + child: PortalTarget( portal: portal, child: const Text('child', textDirection: TextDirection.ltr), ), @@ -1235,7 +1235,7 @@ Future main() async { await tester.pumpWidget( MaterialApp( builder: (_, child) => Portal(child: child!), - home: const PortalEntry( + home: const PortalTarget( portal: Text('portal'), child: Text('child'), ), @@ -1251,7 +1251,7 @@ Future main() async { await tester.pumpWidget( CupertinoApp( builder: (_, child) => Portal(child: child!), - home: const PortalEntry( + home: const PortalTarget( portal: Text('portal'), child: Text('child'), ), @@ -1277,7 +1277,7 @@ Future main() async { builder: (c, value, _) { return LayoutBuilder( builder: (_, __) { - return PortalEntry( + return PortalTarget( portal: ValueListenableBuilder( valueListenable: entryNotifier, builder: (_, value2, __) { @@ -1318,7 +1318,7 @@ Future main() async { await tester.pumpWidget(Portal( child: LayoutBuilder( builder: (_, __) { - return const PortalEntry( + return const PortalTarget( portal: Text('portal', textDirection: TextDirection.ltr), child: Text('child', textDirection: TextDirection.ltr), ); @@ -1352,7 +1352,7 @@ Future main() async { notifier.value = LayoutBuilder( builder: (_, __) { - return const PortalEntry( + return const PortalTarget( portal: Text('portal', textDirection: TextDirection.ltr), child: Text('child2', textDirection: TextDirection.ltr), ); @@ -1455,19 +1455,19 @@ Future main() async { testWidgets('clip overflow', (tester) async {}, skip: true); testWidgets('can have multiple portals', (tester) async { - final topLeft = PortalEntry( + final topLeft = PortalTarget( portal: const Align(alignment: Alignment.topLeft), child: Container(), ); - final topRight = PortalEntry( + final topRight = PortalTarget( portal: const Align(alignment: Alignment.topRight), child: Container(), ); - final bottomRight = PortalEntry( + final bottomRight = PortalTarget( portal: const Align(alignment: Alignment.bottomRight), child: Container(), ); - final bottomLeft = PortalEntry( + final bottomLeft = PortalTarget( portal: const Align(alignment: Alignment.bottomLeft), child: Container(), ); @@ -1502,12 +1502,12 @@ Future main() async { var didClickSecond = false; await tester.pumpWidget( Portal( - child: PortalEntry( + child: PortalTarget( portal: GestureDetector( onTap: () => didClickFirst = true, child: const Text('first', textDirection: TextDirection.ltr), ), - child: PortalEntry( + child: PortalTarget( portal: Center( child: GestureDetector( onTap: () => didClickSecond = true, @@ -1543,12 +1543,12 @@ Future main() async { await tester.pumpWidget( Portal( - child: PortalEntry( + child: PortalTarget( portal: Container( margin: const EdgeInsets.all(10), color: Colors.red, ), - child: PortalEntry( + child: PortalTarget( portal: Center( child: Container( height: 30, From 3b5facbabeff07812739c9423a4be6e7c4190355 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Mon, 7 Feb 2022 09:43:10 +0000 Subject: [PATCH 18/35] Further enforce naming --- example/lib/contextual_menu.dart | 2 +- example/lib/date_picker.dart | 2 +- example/lib/discovery.dart | 4 +- example/lib/medium_clap.dart | 2 +- example/lib/modal.dart | 4 +- lib/src/anchor.dart | 2 +- lib/src/portal.dart | 34 ++++----- test/anchor_test.dart | 4 +- test/widget_test.dart | 115 ++++++++++++++++--------------- 9 files changed, 85 insertions(+), 84 deletions(-) diff --git a/example/lib/contextual_menu.dart b/example/lib/contextual_menu.dart index 063e2f9..aad4395 100644 --- a/example/lib/contextual_menu.dart +++ b/example/lib/contextual_menu.dart @@ -113,7 +113,7 @@ class _ModalEntry extends StatelessWidget { onTap: visible ? onClose : null, child: PortalTarget( visible: visible, - portal: menu, + portalFollower: menu, anchor: const Aligned( source: Alignment.topLeft, target: Alignment.bottomLeft, diff --git a/example/lib/date_picker.dart b/example/lib/date_picker.dart index b6b30be..50d7df7 100644 --- a/example/lib/date_picker.dart +++ b/example/lib/date_picker.dart @@ -23,7 +23,7 @@ class DeclarativeDatePicker extends StatelessWidget { Widget build(BuildContext context) { return PortalTarget( visible: visible, - portal: Stack( + portalFollower: Stack( children: [ const Positioned.fill( child: IgnorePointer( diff --git a/example/lib/discovery.dart b/example/lib/discovery.dart index e215ece..084a5cd 100644 --- a/example/lib/discovery.dart +++ b/example/lib/discovery.dart @@ -83,7 +83,7 @@ class Discovery extends StatelessWidget { target: Alignment.center, source: Alignment.center, ), - portal: Stack( + portalFollower: Stack( children: [ CustomPaint( painter: HolePainter(Theme.of(context).colorScheme.secondary), @@ -174,7 +174,7 @@ class Barrier extends StatelessWidget { return PortalTarget( visible: visible, closeDuration: kThemeAnimationDuration, - portal: GestureDetector( + portalFollower: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onClose, child: TweenAnimationBuilder( diff --git a/example/lib/medium_clap.dart b/example/lib/medium_clap.dart index f5fc679..76f6a6b 100644 --- a/example/lib/medium_clap.dart +++ b/example/lib/medium_clap.dart @@ -47,7 +47,7 @@ class _ClapButtonState extends State { source: Alignment.bottomCenter, ), closeDuration: kThemeChangeDuration, - portal: TweenAnimationBuilder( + portalFollower: TweenAnimationBuilder( tween: Tween(begin: 0, end: hasClappedRecently ? 1 : 0), duration: kThemeChangeDuration, builder: (context, progress, child) { diff --git a/example/lib/modal.dart b/example/lib/modal.dart index 2c74e8f..d1d37a8 100644 --- a/example/lib/modal.dart +++ b/example/lib/modal.dart @@ -62,7 +62,7 @@ class Modal extends StatelessWidget { child: PortalTarget( visible: visible, closeDuration: kThemeAnimationDuration, - portal: TweenAnimationBuilder( + portalFollower: TweenAnimationBuilder( duration: kThemeAnimationDuration, curve: Curves.easeOut, tween: Tween(begin: 0, end: visible ? 1 : 0), @@ -100,7 +100,7 @@ class Barrier extends StatelessWidget { return PortalTarget( visible: visible, closeDuration: kThemeAnimationDuration, - portal: GestureDetector( + portalFollower: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onClose, child: TweenAnimationBuilder( diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart index 0671e65..cb6a5d5 100644 --- a/lib/src/anchor.dart +++ b/lib/src/anchor.dart @@ -4,7 +4,7 @@ import 'package:flutter/rendering.dart'; /// The logic of layout and positioning of a follower element in relation to a /// target element. /// -/// Independent of the underlying rendering implementation. +/// This is independent of the underlying rendering implementation. abstract class Anchor { /// Returns the layout constraints that are given to the follower element. /// diff --git a/lib/src/portal.dart b/lib/src/portal.dart index 6e40bb6..4852e7e 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -12,11 +12,11 @@ import 'custom_follower.dart'; /// [Portal] can be considered as a reimplementation of [Overlay] to allow /// adding an [OverlayEntry] (now named [PortalTarget]) declaratively. /// -/// [Portal] widget is used in co-ordination with [PortalTarget] widget to show -/// some content _above_ another content. -/// This is similar to [Stack] in principle, with the difference that [PortalTarget] -/// does not have to be a direct child of [Portal] and can instead be placed -/// anywhere in the widget tree. +/// The [Portal] widget is used in coordination with the [PortalTarget] widget +/// to show some content _above_ other content. +/// This is similar to [Stack] in principle, with the difference that a +/// [PortalTarget] does not have to be a direct child of [Portal] and can +/// instead be placed anywhere in the widget tree. /// /// In most situations, [Portal] can be placed directly above [MaterialApp]: /// @@ -27,7 +27,7 @@ import 'custom_follower.dart'; /// ); /// ``` /// -/// This allows an overlay to renders above _everything_ including all routes. +/// This allows an overlay to render above _everything_ including all routes. /// That can be useful to show a snackbar between pages. /// /// You can optionally add a [Portal] inside your page: @@ -347,18 +347,18 @@ class PortalTarget extends StatefulWidget { Key? key, this.visible = true, this.anchor = const Filled(), - this.portal, this.closeDuration, + this.portalFollower, required this.child, - }) : assert(visible == false || portal != null), + }) : assert(visible == false || portalFollower != null), super(key: key); // ignore: diagnostic_describe_all_properties, conflicts with closeDuration final bool visible; final Anchor anchor; - final Widget? portal; - final Widget child; final Duration? closeDuration; + final Widget? portalFollower; + final Widget child; @override _PortalTargetState createState() => _PortalTargetState(); @@ -369,7 +369,7 @@ class PortalTarget extends StatefulWidget { properties ..add(DiagnosticsProperty('anchor', anchor)) ..add(DiagnosticsProperty('closeDuration', closeDuration)) - ..add(DiagnosticsProperty('portal', portal)) + ..add(DiagnosticsProperty('portal', portalFollower)) ..add(DiagnosticsProperty('child', child)); } } @@ -410,7 +410,7 @@ class _PortalTargetState extends State { if (widget.anchor is Filled) { return _PortalTargetTheater( - portal: _visible ? widget.portal : null, + portal: _visible ? widget.portalFollower : null, anchor: widget.anchor, targetSize: Size.zero, overlayLink: scope._overlayLink, @@ -439,7 +439,7 @@ class _PortalTargetState extends State { overlayLink: scope._overlayLink, anchor: widget.anchor, targetSize: targetSize, - child: widget.portal, + child: widget.portalFollower, ), child: const SizedBox.shrink(), ); @@ -643,10 +643,10 @@ class _RenderPortalTarget extends RenderProxyBox { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - .add(DiagnosticsProperty('overlayLink', overlayLink)); - properties.add(DiagnosticsProperty('anchor', anchor)); - properties.add(DiagnosticsProperty('targetSize', targetSize)); - properties.add(DiagnosticsProperty('branch', branch)); + ..add(DiagnosticsProperty('overlayLink', overlayLink)) + ..add(DiagnosticsProperty('anchor', anchor)) + ..add(DiagnosticsProperty('targetSize', targetSize)) + ..add(DiagnosticsProperty('branch', branch)); } } diff --git a/test/anchor_test.dart b/test/anchor_test.dart index 4aa7c14..613ea10 100644 --- a/test/anchor_test.dart +++ b/test/anchor_test.dart @@ -46,7 +46,7 @@ void main() { height: 50, child: PortalTarget( anchor: anchor, - portal: const ColoredBox( + portalFollower: const ColoredBox( color: Colors.red, ), child: const Center( @@ -92,7 +92,7 @@ void main() { target: Alignment.bottomLeft, backup: backupAligned, ), - portal: const SizedBox( + portalFollower: const SizedBox( width: 20, height: 20, ), diff --git a/test/widget_test.dart b/test/widget_test.dart index 0cd696f..a3e45e1 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -47,7 +47,7 @@ Future main() async { child: Portal( child: PortalTarget( closeDuration: Duration(seconds: 5), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -62,7 +62,7 @@ Future main() async { child: PortalTarget( visible: false, closeDuration: Duration(seconds: 6), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -91,7 +91,7 @@ Future main() async { target: Alignment.center, source: Alignment.center, ), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -107,7 +107,7 @@ Future main() async { visible: false, closeDuration: Duration(seconds: 6), anchor: Aligned.center, - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -131,7 +131,7 @@ Future main() async { child: Portal( child: PortalTarget( closeDuration: Duration(seconds: 5), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -146,7 +146,7 @@ Future main() async { child: PortalTarget( visible: false, closeDuration: Duration(seconds: 5), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -166,7 +166,7 @@ Future main() async { child: Portal( child: PortalTarget( closeDuration: Duration(seconds: 5), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -181,7 +181,7 @@ Future main() async { child: PortalTarget( visible: false, closeDuration: Duration(seconds: 5), - portal: Text('portal2'), + portalFollower: Text('portal2'), child: Text('child'), ), ), @@ -197,7 +197,7 @@ Future main() async { child: PortalTarget( visible: false, closeDuration: Duration(seconds: 20), - portal: Text('portal3'), + portalFollower: Text('portal3'), child: Text('child'), ), ), @@ -218,7 +218,7 @@ Future main() async { child: Portal( child: PortalTarget( closeDuration: Duration(seconds: 5), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -233,7 +233,7 @@ Future main() async { child: PortalTarget( visible: false, closeDuration: Duration(seconds: 5), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -247,7 +247,7 @@ Future main() async { child: Portal( child: PortalTarget( closeDuration: Duration(seconds: 5), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -271,7 +271,7 @@ Future main() async { child: Portal( child: PortalTarget( closeDuration: Duration(seconds: 5), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -284,7 +284,7 @@ Future main() async { child: PortalTarget( visible: false, closeDuration: Duration(seconds: 5), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -298,7 +298,7 @@ Future main() async { child: Portal( child: PortalTarget( closeDuration: Duration(seconds: 5), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -315,7 +315,7 @@ Future main() async { child: PortalTarget( visible: false, closeDuration: Duration(seconds: 5), - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -338,7 +338,7 @@ Future main() async { const Boilerplate( child: Portal( child: PortalTarget( - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -352,7 +352,7 @@ Future main() async { child: Portal( child: PortalTarget( visible: false, - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -368,7 +368,7 @@ Future main() async { const Boilerplate( child: Portal( child: PortalTarget( - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -388,7 +388,7 @@ Future main() async { child: Portal( child: PortalTarget( anchor: Aligned.center, - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -403,7 +403,7 @@ Future main() async { child: PortalTarget( visible: false, anchor: Aligned.center, - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -420,7 +420,7 @@ Future main() async { child: Portal( child: PortalTarget( anchor: Aligned.center, - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -482,7 +482,7 @@ Future main() async { child: Portal( child: Center( child: PortalTarget( - portal: firstPortal, + portalFollower: firstPortal, child: firstChild, ), ), @@ -508,7 +508,7 @@ Future main() async { final portal = ValueNotifier( PortalTarget( visible: false, - portal: Builder(builder: (_) => throw Error()), + portalFollower: Builder(builder: (_) => throw Error()), child: const Text('firstChild'), ), ); @@ -529,7 +529,7 @@ Future main() async { final portalChildElement = tester.element(find.text('firstChild')); portal.value = const PortalTarget( - portal: Text('secondPortal'), + portalFollower: Text('secondPortal'), child: Text('secondChild'), ); await tester.pump(); @@ -546,7 +546,7 @@ Future main() async { portal.value = PortalTarget( visible: false, - portal: Builder(builder: (_) => throw Error()), + portalFollower: Builder(builder: (_) => throw Error()), child: const Text('thirdChild'), ); await tester.pump(); @@ -574,7 +574,7 @@ Future main() async { final portal = ValueNotifier( Center( child: PortalTarget( - portal: portalChild, + portalFollower: portalChild, child: Center(child: child), ), ), @@ -608,7 +608,7 @@ Future main() async { await tester.pumpWidget( const PortalTarget( closeDuration: Duration(seconds: 5), - portal: Text('portal', textDirection: TextDirection.ltr), + portalFollower: Text('portal', textDirection: TextDirection.ltr), child: Text('child', textDirection: TextDirection.ltr), ), ); @@ -636,13 +636,13 @@ Future main() async { builder: (c, value, _) { return PortalTarget( visible: value, - portal: Container( + portalFollower: Container( color: Colors.red.withAlpha(122), ), child: Center( child: PortalTarget( visible: value, - portal: Container( + portalFollower: Container( height: 50, width: 50, color: Colors.blue, @@ -687,7 +687,7 @@ Future main() async { child: Portal( child: Center( child: PortalTarget( - portal: portal, + portalFollower: portal, child: child, ), ), @@ -731,7 +731,7 @@ Future main() async { expect(find.byWidget(first), findsOneWidget); child.value = PortalTarget( - portal: portal, + portalFollower: portal, child: second, ); await tester.pump(); @@ -754,7 +754,7 @@ Future main() async { Boilerplate( child: Portal( child: PortalTarget( - portal: ElevatedButton( + portalFollower: ElevatedButton( onPressed: () => portalClickCount++, child: const Text('portal'), ), @@ -788,7 +788,7 @@ Future main() async { source: Alignment.bottomCenter, target: Alignment.topCenter, ), - portal: ElevatedButton( + portalFollower: ElevatedButton( onPressed: () => portalClickCount++, child: const Text('portal'), ), @@ -825,7 +825,7 @@ Future main() async { child: PortalTarget( anchor: Aligned( source: Alignment.topLeft, target: Alignment.bottomLeft), - portal: SizedBox(key: portalKey, height: 42, width: 24), + portalFollower: SizedBox(key: portalKey, height: 42, width: 24), child: SizedBox(key: childKey, height: 10, width: 10), ), ), @@ -859,7 +859,7 @@ Future main() async { source: Alignment.topRight, target: Alignment.bottomRight, ), - portal: SizedBox(key: portalKey, height: 24, width: 42), + portalFollower: SizedBox(key: portalKey, height: 24, width: 42), child: SizedBox(key: childKey, height: 20, width: 20), ), ), @@ -896,7 +896,7 @@ Future main() async { source: Alignment.bottomRight, target: Alignment.topRight, ), - portal: SizedBox(key: portalKey, height: 20, width: 20), + portalFollower: SizedBox(key: portalKey, height: 20, width: 20), child: SizedBox(key: childKey, height: 10, width: 10), ), ), @@ -933,7 +933,7 @@ Future main() async { child: Portal( child: Align( child: PortalTarget( - portal: SizedBox(key: portalKey, height: 20, width: 20), + portalFollower: SizedBox(key: portalKey, height: 20, width: 20), child: SizedBox(key: childKey, height: 10, width: 10), ), ), @@ -974,7 +974,7 @@ Future main() async { child: Portal( child: Center( child: PortalTarget( - portal: GestureDetector( + portalFollower: GestureDetector( key: portalKey, onTap: () => portalClickCount++, child: child, @@ -1013,7 +1013,7 @@ Future main() async { source: Alignment.bottomCenter, target: Alignment.topCenter, ), - portal: GestureDetector( + portalFollower: GestureDetector( key: portalKey, onTap: () => portalClickCount++, child: child, @@ -1053,7 +1053,7 @@ Future main() async { child: Portal( child: Center( child: PortalTarget( - portal: GestureDetector( + portalFollower: GestureDetector( key: portalKey, onTap: () => portalClickCount++, child: child, @@ -1110,7 +1110,7 @@ Future main() async { source: Alignment.bottomCenter, target: Alignment.topCenter, ), - portal: portal, + portalFollower: portal, child: child, ), ), @@ -1143,7 +1143,8 @@ Future main() async { child: Portal( child: Center( child: PortalTarget( - portal: Align(alignment: Alignment.topLeft, child: portal), + portalFollower: + Align(alignment: Alignment.topLeft, child: portal), child: child, ), ), @@ -1216,7 +1217,7 @@ Future main() async { Portal( child: Center( child: PortalTarget( - portal: portal, + portalFollower: portal, child: const Text('child', textDirection: TextDirection.ltr), ), ), @@ -1236,7 +1237,7 @@ Future main() async { MaterialApp( builder: (_, child) => Portal(child: child!), home: const PortalTarget( - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -1252,7 +1253,7 @@ Future main() async { CupertinoApp( builder: (_, child) => Portal(child: child!), home: const PortalTarget( - portal: Text('portal'), + portalFollower: Text('portal'), child: Text('child'), ), ), @@ -1278,7 +1279,7 @@ Future main() async { return LayoutBuilder( builder: (_, __) { return PortalTarget( - portal: ValueListenableBuilder( + portalFollower: ValueListenableBuilder( valueListenable: entryNotifier, builder: (_, value2, __) { entryBuild(value, value2); @@ -1319,7 +1320,7 @@ Future main() async { child: LayoutBuilder( builder: (_, __) { return const PortalTarget( - portal: Text('portal', textDirection: TextDirection.ltr), + portalFollower: Text('portal', textDirection: TextDirection.ltr), child: Text('child', textDirection: TextDirection.ltr), ); }, @@ -1353,7 +1354,7 @@ Future main() async { notifier.value = LayoutBuilder( builder: (_, __) { return const PortalTarget( - portal: Text('portal', textDirection: TextDirection.ltr), + portalFollower: Text('portal', textDirection: TextDirection.ltr), child: Text('child2', textDirection: TextDirection.ltr), ); }, @@ -1456,19 +1457,19 @@ Future main() async { testWidgets('can have multiple portals', (tester) async { final topLeft = PortalTarget( - portal: const Align(alignment: Alignment.topLeft), + portalFollower: const Align(alignment: Alignment.topLeft), child: Container(), ); final topRight = PortalTarget( - portal: const Align(alignment: Alignment.topRight), + portalFollower: const Align(alignment: Alignment.topRight), child: Container(), ); final bottomRight = PortalTarget( - portal: const Align(alignment: Alignment.bottomRight), + portalFollower: const Align(alignment: Alignment.bottomRight), child: Container(), ); final bottomLeft = PortalTarget( - portal: const Align(alignment: Alignment.bottomLeft), + portalFollower: const Align(alignment: Alignment.bottomLeft), child: Container(), ); @@ -1503,12 +1504,12 @@ Future main() async { await tester.pumpWidget( Portal( child: PortalTarget( - portal: GestureDetector( + portalFollower: GestureDetector( onTap: () => didClickFirst = true, child: const Text('first', textDirection: TextDirection.ltr), ), child: PortalTarget( - portal: Center( + portalFollower: Center( child: GestureDetector( onTap: () => didClickSecond = true, child: const Text('second', textDirection: TextDirection.ltr), @@ -1544,12 +1545,12 @@ Future main() async { await tester.pumpWidget( Portal( child: PortalTarget( - portal: Container( + portalFollower: Container( margin: const EdgeInsets.all(10), color: Colors.red, ), child: PortalTarget( - portal: Center( + portalFollower: Center( child: Container( height: 30, width: 30, From 1c318c480793d44df8680f6d5052f243e6c5d8e0 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Mon, 7 Feb 2022 10:02:08 +0000 Subject: [PATCH 19/35] Introduce `PortalFollower` --- example/pubspec.lock | 17 ++++++++++++----- example/pubspec.yaml | 16 ++-------------- lib/src/portal.dart | 14 +++++++++++--- pubspec.yaml | 2 +- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index d0756ea..3e075e0 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -81,6 +81,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" meta: dependency: transitive description: @@ -94,7 +101,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" sky_engine: dependency: transitive description: flutter @@ -106,7 +113,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -141,7 +148,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.9" typed_data: dependency: transitive description: @@ -155,7 +162,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" sdks: - dart: ">=2.14.0 <3.0.0" + dart: ">=2.16.0 <3.0.0" flutter: ">=1.21.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 34a0b87..4d0d66b 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,22 +1,10 @@ name: example -description: A new Flutter project. - +description: Example app demonstrating usage of flutter_portal. publish_to: none - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.0.0+1 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.16.0 <3.0.0' dependencies: # The following adds the Cupertino Icons font to your application. diff --git a/lib/src/portal.dart b/lib/src/portal.dart index 4852e7e..3849bcb 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -7,7 +7,7 @@ import 'package:flutter/rendering.dart'; import 'anchor.dart'; import 'custom_follower.dart'; -/// The widget where a [PortalTarget] is rendered. +/// The widget where a [PortalTarget] and its [PortalFollower] are rendered. /// /// [Portal] can be considered as a reimplementation of [Overlay] to allow /// adding an [OverlayEntry] (now named [PortalTarget]) declaratively. @@ -171,6 +171,14 @@ class _RenderPortalTheater extends RenderProxyBox { } } +/// Widget that is passed to a [PortalTarget] as the follower that is overlaid +/// on top of other content in a [Portal]. +/// +/// This is just a regular [Widget] that is passed as +/// [PortalTarget.portalFollower]. The target takes care of making it a +/// follower → it is only a typedef. +typedef PortalFollower = Widget; + /// A widget that renders its content in a different location of the widget tree. /// /// In short, you can use [PortalTarget] to show dialogs, tooltips, contextual menus, ... @@ -357,7 +365,7 @@ class PortalTarget extends StatefulWidget { final bool visible; final Anchor anchor; final Duration? closeDuration; - final Widget? portalFollower; + final PortalFollower? portalFollower; final Widget child; @override @@ -369,7 +377,7 @@ class PortalTarget extends StatefulWidget { properties ..add(DiagnosticsProperty('anchor', anchor)) ..add(DiagnosticsProperty('closeDuration', closeDuration)) - ..add(DiagnosticsProperty('portal', portalFollower)) + ..add(DiagnosticsProperty('portal', portalFollower)) ..add(DiagnosticsProperty('child', child)); } } diff --git a/pubspec.yaml b/pubspec.yaml index 08e6322..bba6b1c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ description: Overlay/OverlayEntry but implemented as a widget for a declarative homepage: https://github.com/rrousselGit/flutter_portal environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.16.0 <3.0.0' flutter: '>=1.21.0' dependencies: From b88c21bd5bd00b4ee1a9d7488f8976f3742b730c Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Mon, 7 Feb 2022 10:03:28 +0000 Subject: [PATCH 20/35] Add todo for target docs --- lib/src/portal.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/portal.dart b/lib/src/portal.dart index 3849bcb..ca79957 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -179,6 +179,8 @@ class _RenderPortalTheater extends RenderProxyBox { /// follower → it is only a typedef. typedef PortalFollower = Widget; +// todo(creativecreatorormaybenot): update target docs. + /// A widget that renders its content in a different location of the widget tree. /// /// In short, you can use [PortalTarget] to show dialogs, tooltips, contextual menus, ... From ec457c5263dac46d7d802ee8918b456e50dd2666 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Mon, 7 Feb 2022 10:31:20 +0000 Subject: [PATCH 21/35] Clean up docs --- example/lib/contextual_menu.dart | 4 +- example/lib/discovery.dart | 2 +- example/lib/medium_clap.dart | 2 +- lib/src/anchor.dart | 58 ++++++++++++----------- lib/src/portal.dart | 80 +++++++++++++++++++------------- test/anchor_test.dart | 4 +- test/widget_test.dart | 16 +++---- 7 files changed, 91 insertions(+), 75 deletions(-) diff --git a/example/lib/contextual_menu.dart b/example/lib/contextual_menu.dart index aad4395..a255efa 100644 --- a/example/lib/contextual_menu.dart +++ b/example/lib/contextual_menu.dart @@ -115,11 +115,11 @@ class _ModalEntry extends StatelessWidget { visible: visible, portalFollower: menu, anchor: const Aligned( - source: Alignment.topLeft, + follower: Alignment.topLeft, target: Alignment.bottomLeft, widthFactor: 1, backup: Aligned( - source: Alignment.bottomLeft, + follower: Alignment.bottomLeft, target: Alignment.topLeft, widthFactor: 1, ), diff --git a/example/lib/discovery.dart b/example/lib/discovery.dart index 084a5cd..1ab2bb1 100644 --- a/example/lib/discovery.dart +++ b/example/lib/discovery.dart @@ -81,7 +81,7 @@ class Discovery extends StatelessWidget { closeDuration: kThemeAnimationDuration, anchor: const Aligned( target: Alignment.center, - source: Alignment.center, + follower: Alignment.center, ), portalFollower: Stack( children: [ diff --git a/example/lib/medium_clap.dart b/example/lib/medium_clap.dart index 76f6a6b..69b3a50 100644 --- a/example/lib/medium_clap.dart +++ b/example/lib/medium_clap.dart @@ -44,7 +44,7 @@ class _ClapButtonState extends State { // aligns the top-center of `child` with the bottom-center of `portal` anchor: const Aligned( target: Alignment.topCenter, - source: Alignment.bottomCenter, + follower: Alignment.bottomCenter, ), closeDuration: kThemeChangeDuration, portalFollower: TweenAnimationBuilder( diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart index cb6a5d5..54c27dd 100644 --- a/lib/src/anchor.dart +++ b/lib/src/anchor.dart @@ -14,8 +14,8 @@ abstract class Anchor { /// space, i.e. only the size of the target should be considered. /// /// The [portalConstraints] represent the full available space to place the - /// source element in. This is irrespective of where the target is positioned - /// within the full available space. + /// follower element in. This is irrespective of where the target is + /// positioned within the full available space. BoxConstraints getFollowerConstraints({ required Rect targetRect, required BoxConstraints portalConstraints, @@ -25,7 +25,7 @@ abstract class Anchor { /// to the top left of the [targetRect]. /// /// The [followerSize] is the final size of the follower element after layout - /// based on the source constraints determined by [getFollowerConstraints]. + /// based on the follower constraints determined by [getFollowerConstraints]. /// /// The [targetRect] represents the bounds of the element which the follower /// element should be anchored to. This must be the same value that is passed @@ -57,8 +57,8 @@ abstract class Anchor { }); } -/// The source element should ignore any information about the target and expand -/// to fill the bounds of the overlay +/// The follower element should ignore any information about the target and +/// expand to fill the bounds of the overlay @immutable class Filled implements Anchor { const Filled(); @@ -81,15 +81,15 @@ class Filled implements Anchor { } } -/// Align a point of the source element with a point on the target element -/// Can optionally pass a [widthFactor] or [heightFactor] so the source element -/// gets a size as a factor of the target element. +/// Align a point of the follower element with a point on the target element +/// Can optionally pass a [widthFactor] or [heightFactor] so the follower +/// element gets a size as a factor of the target element. /// Can optionally pass a [backup] which will be used if the element is going /// to be rendered off screen. @immutable class Aligned implements Anchor { const Aligned({ - required this.source, + required this.follower, required this.target, this.offset = Offset.zero, this.widthFactor, @@ -98,29 +98,31 @@ class Aligned implements Anchor { }); static const center = Aligned( - source: Alignment.center, + follower: Alignment.center, target: Alignment.center, ); - /// The reference point on the source element - final Alignment source; + /// The reference point on the follower element. + final Alignment follower; /// The reference point on the target element final Alignment target; - /// Offset to shift the source element by after all calculations are made + /// Offset to shift the follower element by after all calculations are made. final Offset offset; - /// The width to make the source element as a multiple of the width of the - /// target element. An autocomplete widget may set this to 1 so the popup - /// width matches the the text field width + /// The width to make the follower element as a multiple of the width of the + /// target element. + /// + /// An autocomplete widget may set this to 1 so the popup width matches the + /// text field width. final double? widthFactor; - /// The height to make the source element as a multiple of the height of the + /// The height to make the follower element as a multiple of the height of the /// target element. final double? heightFactor; - /// If the calculated position would render the source element out of bounds + /// If the calculated position would render the follower element out of bounds /// (for example, a tooltip would go off screen), a backup can be used. /// The offset calculations will fall back to the backup. final Anchor? backup; @@ -146,14 +148,14 @@ class Aligned implements Anchor { required Rect targetRect, required Rect portalRect, }) { - final sourceRect = (Offset.zero & followerSize).alignedTo( + final followerRect = (Offset.zero & followerSize).alignedTo( targetRect, - sourceAlignment: source, + followerAlignment: follower, targetAlignment: target, offset: offset, ); - if (!portalRect.fullyContains(sourceRect)) { + if (!portalRect.fullyContains(followerRect)) { final backup = this.backup; if (backup != null) { return backup.getFollowerOffset( @@ -164,7 +166,7 @@ class Aligned implements Anchor { } } - return sourceRect.topLeft; + return followerRect.topLeft; } @override @@ -175,28 +177,28 @@ class Aligned implements Anchor { if (other is! Aligned) { return false; } - return source == other.source && + return follower == other.follower && target == other.target && offset == other.offset && backup == other.backup; } @override - int get hashCode => source.hashCode ^ target.hashCode ^ offset.hashCode; + int get hashCode => follower.hashCode ^ target.hashCode ^ offset.hashCode; } extension on Rect { Rect alignedTo( Rect target, { - required Alignment sourceAlignment, + required Alignment followerAlignment, required Alignment targetAlignment, Offset offset = Offset.zero, }) { - final sourceOffset = targetAlignment.alongSize(target.size) - - sourceAlignment.alongSize(size) + + final followerOffset = targetAlignment.alongSize(target.size) - + followerAlignment.alongSize(size) + target.topLeft + offset; - return sourceOffset & size; + return followerOffset & size; } /// Returns true if [rect] is fully contained within this rect diff --git a/lib/src/portal.dart b/lib/src/portal.dart index ca79957..f9578f6 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -181,26 +181,37 @@ typedef PortalFollower = Widget; // todo(creativecreatorormaybenot): update target docs. -/// A widget that renders its content in a different location of the widget tree. +/// A widget that renders its follower in a different location of the widget +/// tree. /// -/// In short, you can use [PortalTarget] to show dialogs, tooltips, contextual menus, ... -/// You can then control the visibility of these overlays with a simple `setState`. +/// Its [child] is rendered in the tree as you would expect, but its +/// [portalFollower] is rendered through the ancestor [Portal] in a different +/// location of the widget tree. /// -/// The benefits of using [PortalTarget] over [Overlay]/[OverlayEntry] are multiple: +/// In short, you can use [PortalTarget] to show dialogs, tooltips, contextual +/// menus, etc. +/// You can then control the visibility of these overlays with a simple +/// `setState`. +/// +/// The benefits of using [PortalTarget]/[PortalFollower] over +/// [Overlay]/[OverlayEntry] are multiple: /// - [PortalTarget] is easier to manipulate /// - It allows aligning your menus/tooltips next to a button easily -/// - It combines nicely with state-management solutions and the "state-restoration" -/// framework. For example, combined with [RestorableProperty], when the application -/// is killed then re-opened, modals/menus would be restored. +/// - It combines nicely with state-management solutions and the +/// "state-restoration" framework. For example, combined with +/// [RestorableProperty] when the application is killed then re-opened, +/// modals/menus would be restored. /// -/// For [PortalTarget] to work, make sure to insert [Portal] higher in the widget-tree. +/// For [PortalTarget] to work, make sure to insert [Portal] higher in the +/// widget tree. /// /// ## Contextual menu example /// /// In this example, we will see how we can use [PortalTarget] to show a menu /// after clicking on a [ElevatedButton]. /// -/// First, we need to create a [StatefulWidget] that renders our [ElevatedButton]: +/// First, we need to create a [StatefulWidget] that renders our +/// [ElevatedButton]: /// /// ```dart /// class MenuExample extends StatefulWidget { @@ -226,13 +237,14 @@ typedef PortalFollower = Widget; /// Then, we need to insert our [PortalTarget] in the widget tree. /// /// We want our contextual menu to render right next to our [ElevatedButton]. -/// As such, our [PortalTarget] should be the parent of [ElevatedButton] like so: +/// As such, our [PortalTarget] should be the parent of [ElevatedButton] like +/// so: /// /// ```dart /// Center( -/// child: PortalEntry( +/// child: PortalTarget( /// visible: // -/// portal: // +/// portalFollower: // /// child: ElevatedButton( /// ... /// ), @@ -240,13 +252,13 @@ typedef PortalFollower = Widget; /// ) /// ``` /// -/// We can pass our menu to [PortalEntry]: +/// We can pass our menu as the `portalFollower` to [PortalTarget]: /// /// /// ```dart -/// PortalEntry( +/// PortalTarget( /// visible: true, -/// portal: Material( +/// portalFollower: Material( /// elevation: 8, /// child: IntrinsicWidth( /// child: Column( @@ -258,7 +270,7 @@ typedef PortalFollower = Widget; /// ), /// ), /// ), -/// child: RaiseButton(...), +/// child: ElevatedButton(...), /// ) /// ``` /// @@ -270,22 +282,24 @@ typedef PortalFollower = Widget; /// Let's fix the full-screen issue first and change our code so that our /// menu renders on the _right_ of our [ElevatedButton]. /// -/// To align our menu around our button, we can specify the `childAnchor` and -/// `portalAnchor` parameters: +/// To align our menu around our button, we can specify the `anchor` +/// parameter: /// /// ```dart /// PortalEntry( /// visible: true, -/// portalAnchor: Alignment.topLeft, -/// childAnchor: Alignment.topRight, +/// anchor: const Aligned( +/// follower: Alignment.topLeft, +/// target: Alignment.topRight, +/// ), /// portal: Material(...), -/// child: RaiseButton(...), +/// child: ElevatedButton(...), /// ) /// ``` /// /// What this code means is, this will align the top-left of our menu with the /// top-right or the [ElevatedButton]. -/// With this, our menu is no-longer full-screen and is now located to the right +/// With this, our menu is no longer full-screen and is now located to the right /// of our button. /// /// Finally, we can update our code such that the menu show only when clicking @@ -304,7 +318,7 @@ typedef PortalFollower = Widget; /// We then pass this `isMenuOpen` variable to our [PortalEntry]: /// /// ```dart -/// PortalEntry( +/// PortalTarget( /// visible: isMenuOpen, /// ... /// ) @@ -334,7 +348,7 @@ typedef PortalFollower = Widget; /// /// ```dart /// Center( -/// child: PortalEntry( +/// child: PortalTarget( /// visible: isMenuOpen, /// portal: GestureDetector( /// behavior: HitTestBehavior.opaque, @@ -344,8 +358,8 @@ typedef PortalFollower = Widget; /// }); /// }, /// ), -/// child: PortalEntry( -/// // our previous PortalEntry +/// child: PortalTarget( +/// // our previous PortalTarget /// portal: Material(...) /// child: ElevatedButton(...), /// ), @@ -503,7 +517,7 @@ class _PortalTargetTheater extends SingleChildRenderObjectWidget { } @override - SingleChildRenderObjectElement createElement() => _PortalEntryElement(this); + SingleChildRenderObjectElement createElement() => _PortalTargetElement(this); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -628,7 +642,7 @@ class _RenderPortalTarget extends RenderProxyBox { @override void applyPaintTransform(RenderObject child, Matrix4 transform) { if (child == branch) { - // ignore all transformations applied between Portal and PortalEntry + // ignore all transformations applied between Portal and PortalTarget transform.setFrom(overlayLink.theater!.getTransformTo(null)); } } @@ -660,8 +674,8 @@ class _RenderPortalTarget extends RenderProxyBox { } } -class _PortalEntryElement extends SingleChildRenderObjectElement { - _PortalEntryElement(_PortalTargetTheater widget) : super(widget); +class _PortalTargetElement extends SingleChildRenderObjectElement { + _PortalTargetElement(_PortalTargetTheater widget) : super(widget); @override _PortalTargetTheater get widget => super.widget as _PortalTargetTheater; @@ -736,14 +750,14 @@ class _PortalEntryElement extends SingleChildRenderObjectElement { /// The error that is thrown when a [PortalTarget] fails to find a [Portal]. class PortalNotFoundError extends Error { - PortalNotFoundError._(this._portalEntry); + PortalNotFoundError._(this._portalTarget); - final PortalTarget _portalEntry; + final PortalTarget _portalTarget; @override String toString() { return ''' -Error: Could not find a $T above this $_portalEntry. +Error: Could not find a $T above this $_portalTarget. '''; } } diff --git a/test/anchor_test.dart b/test/anchor_test.dart index 613ea10..993a191 100644 --- a/test/anchor_test.dart +++ b/test/anchor_test.dart @@ -88,7 +88,7 @@ void main() { ); final entry = PortalTarget( anchor: Aligned( - source: Alignment.topLeft, + follower: Alignment.topLeft, target: Alignment.bottomLeft, backup: backupAligned, ), @@ -188,7 +188,7 @@ class _TestAligned extends Aligned { double? heightFactor, required this.onGetSourceOffset, }) : super( - source: source, + follower: source, target: target, offset: offset, widthFactor: widthFactor, diff --git a/test/widget_test.dart b/test/widget_test.dart index a3e45e1..85190a9 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -89,7 +89,7 @@ Future main() async { closeDuration: Duration(seconds: 5), anchor: Aligned( target: Alignment.center, - source: Alignment.center, + follower: Alignment.center, ), portalFollower: Text('portal'), child: Text('child'), @@ -648,7 +648,7 @@ Future main() async { color: Colors.blue, ), anchor: const Aligned( - source: Alignment.bottomCenter, + follower: Alignment.bottomCenter, target: Alignment.topCenter, ), child: Container( @@ -785,7 +785,7 @@ Future main() async { child: Center( child: PortalTarget( anchor: const Aligned( - source: Alignment.bottomCenter, + follower: Alignment.bottomCenter, target: Alignment.topCenter, ), portalFollower: ElevatedButton( @@ -824,7 +824,7 @@ Future main() async { alignment: Alignment.topLeft, child: PortalTarget( anchor: Aligned( - source: Alignment.topLeft, target: Alignment.bottomLeft), + follower: Alignment.topLeft, target: Alignment.bottomLeft), portalFollower: SizedBox(key: portalKey, height: 42, width: 24), child: SizedBox(key: childKey, height: 10, width: 10), ), @@ -856,7 +856,7 @@ Future main() async { alignment: Alignment.topRight, child: PortalTarget( anchor: Aligned( - source: Alignment.topRight, + follower: Alignment.topRight, target: Alignment.bottomRight, ), portalFollower: SizedBox(key: portalKey, height: 24, width: 42), @@ -893,7 +893,7 @@ Future main() async { alignment: Alignment.bottomRight, child: PortalTarget( anchor: Aligned( - source: Alignment.bottomRight, + follower: Alignment.bottomRight, target: Alignment.topRight, ), portalFollower: SizedBox(key: portalKey, height: 20, width: 20), @@ -1010,7 +1010,7 @@ Future main() async { child: Center( child: PortalTarget( anchor: const Aligned( - source: Alignment.bottomCenter, + follower: Alignment.bottomCenter, target: Alignment.topCenter, ), portalFollower: GestureDetector( @@ -1107,7 +1107,7 @@ Future main() async { child: Center( child: PortalTarget( anchor: const Aligned( - source: Alignment.bottomCenter, + follower: Alignment.bottomCenter, target: Alignment.topCenter, ), portalFollower: portal, From 9612bb229dfc84035ccae1d8d1d1511920365ff0 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Mon, 7 Feb 2022 10:57:54 +0000 Subject: [PATCH 22/35] Make tests pass --- lib/src/portal.dart | 20 ++++++------ test/widget_test.dart | 76 ++++++++++--------------------------------- 2 files changed, 28 insertions(+), 68 deletions(-) diff --git a/lib/src/portal.dart b/lib/src/portal.dart index f9578f6..991a4ed 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -292,7 +292,7 @@ typedef PortalFollower = Widget; /// follower: Alignment.topLeft, /// target: Alignment.topRight, /// ), -/// portal: Material(...), +/// portalFollower: Material(...), /// child: ElevatedButton(...), /// ) /// ``` @@ -350,7 +350,7 @@ typedef PortalFollower = Widget; /// Center( /// child: PortalTarget( /// visible: isMenuOpen, -/// portal: GestureDetector( +/// portalFollower: GestureDetector( /// behavior: HitTestBehavior.opaque, /// onTap: () { /// setState(() { @@ -360,7 +360,7 @@ typedef PortalFollower = Widget; /// ), /// child: PortalTarget( /// // our previous PortalTarget -/// portal: Material(...) +/// portalFollower: Material(...) /// child: ElevatedButton(...), /// ), /// ), @@ -393,7 +393,7 @@ class PortalTarget extends StatefulWidget { properties ..add(DiagnosticsProperty('anchor', anchor)) ..add(DiagnosticsProperty('closeDuration', closeDuration)) - ..add(DiagnosticsProperty('portal', portalFollower)) + ..add(DiagnosticsProperty('portalFollower', portalFollower)) ..add(DiagnosticsProperty('child', child)); } } @@ -434,7 +434,7 @@ class _PortalTargetState extends State { if (widget.anchor is Filled) { return _PortalTargetTheater( - portal: _visible ? widget.portalFollower : null, + portalFollower: _visible ? widget.portalFollower : null, anchor: widget.anchor, targetSize: Size.zero, overlayLink: scope._overlayLink, @@ -458,7 +458,7 @@ class _PortalTargetState extends State { overlayLink: scope._overlayLink, anchor: widget.anchor, targetSize: targetSize, - portal: CustomCompositedTransformFollower( + portalFollower: CustomCompositedTransformFollower( link: _link, overlayLink: scope._overlayLink, anchor: widget.anchor, @@ -484,14 +484,14 @@ class _PortalTargetState extends State { class _PortalTargetTheater extends SingleChildRenderObjectWidget { const _PortalTargetTheater({ Key? key, - required this.portal, + required this.portalFollower, required this.overlayLink, required this.anchor, required this.targetSize, required Widget child, }) : super(key: key, child: child); - final Widget? portal; + final Widget? portalFollower; final Anchor anchor; final OverlayLink overlayLink; final Size targetSize; @@ -691,13 +691,13 @@ class _PortalTargetElement extends SingleChildRenderObjectElement { @override void mount(Element? parent, dynamic newSlot) { super.mount(parent, newSlot); - _branch = updateChild(_branch, widget.portal, _branchSlot); + _branch = updateChild(_branch, widget.portalFollower, _branchSlot); } @override void update(SingleChildRenderObjectWidget newWidget) { super.update(newWidget); - _branch = updateChild(_branch, widget.portal, _branchSlot); + _branch = updateChild(_branch, widget.portalFollower, _branchSlot); } @override diff --git a/test/widget_test.dart b/test/widget_test.dart index 85190a9..73ecce4 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -604,7 +604,7 @@ Future main() async { await expectLater(find.byType(Portal), matchesGoldenFile('unmounted.png')); }); - testWidgets('throws if no PortalEntry were found', (tester) async { + testWidgets('throws if no Portal was found', (tester) async { await tester.pumpWidget( const PortalTarget( closeDuration: Duration(seconds: 5), @@ -618,10 +618,10 @@ Future main() async { expect( exception.toString(), equals('Error: Could not find a Portal above this ' - 'PortalEntry(' + 'PortalTarget(' "anchor: Instance of 'Filled', " 'closeDuration: 0:00:05.000000, ' - 'portal: Text, child: Text).\n'), + 'portalFollower: Text, child: Text).\n'), ); }); @@ -1171,46 +1171,6 @@ Future main() async { expect(didClickPortal, isTrue); }); - testWidgets('PortalEntry target its generic parameter', (tester) async { - // final portalKey = UniqueKey(); - - // await tester.pumpWidget( - // TestPortal( - // child: Center( - // child: Portal( - // child: PortalEntry( - // // Fills the portal so that if it's added to TestPortal it'll be on the top-left - // // but if it's added to Portal, it'll start in the center of the screen. - // portal: Container(key: portalKey), - // child: const Text('child', textDirection: TextDirection.ltr), - // ), - // ), - // ), - // ), - // ); - - // expect(find.text('child'), findsOneWidget); - // expect( - // tester.getTopLeft(find.byKey(portalKey)), - // equals(Offset.zero), - // ); - }, skip: true); - - testWidgets( - "PortalEntry doesn't fallback to Portal if generic doesn't exists", - (tester) async { - // await tester.pumpWidget( - // Portal( - // child: PortalEntry( - // portal: const Text('portal', textDirection: TextDirection.ltr), - // child: Container(), - // ), - // ), - // ); - - // expect(tester.takeException(), isA()); - }, skip: true); - testWidgets('portals can fill the Portal', (tester) async { final portal = Container(); await tester.pumpWidget( @@ -1369,18 +1329,18 @@ Future main() async { testWidgets('handles reparenting with GlobalKey', (tester) async { // final firstPortal = UniqueKey(); // final secondPortal = UniqueKey(); - + // // final entryKey = GlobalKey(); - + // // await tester.pumpWidget( // Row( // textDirection: TextDirection.ltr, // children: [ // Portal( // key: firstPortal, - // child: PortalEntry( + // child: PortalTarget( // key: entryKey, - // portal: Container(), + // portalFollower: Container(), // child: Container(), // ), // ), @@ -1388,19 +1348,19 @@ Future main() async { // ], // ), // ); - + // // final firstPortalElement = // tester.element(find.byKey(firstPortal)) as PortalElement; // final secondPortalElement = // tester.element(find.byKey(secondPortal)) as PortalElement; - + // // expect(firstPortalElement.theater.entries.length, 1); // expect(firstPortalElement.theater.renderObject.builders.length, 1); // expect(firstPortalElement.theater.renderObject.childCount, 1); // expect(secondPortalElement.theater.entries.length, 0); // expect(secondPortalElement.theater.renderObject.builders.length, 0); // expect(secondPortalElement.theater.renderObject.childCount, 0); - + // // await tester.pumpWidget( // Row( // textDirection: TextDirection.ltr, @@ -1411,32 +1371,32 @@ Future main() async { // ), // Portal( // key: secondPortal, - // child: PortalEntry( + // child: PortalTarget( // key: entryKey, - // portal: Container(), + // portalFollower: Container(), // child: Container(), // ), // ), // ], // ), // ); - + // // expect(firstPortalElement.theater.entries.length, 0); // expect(firstPortalElement.theater.renderObject.builders.length, 0); // expect(firstPortalElement.theater.renderObject.childCount, 0); // expect(secondPortalElement.theater.entries.length, 1); // expect(secondPortalElement.theater.renderObject.builders.length, 1); // expect(secondPortalElement.theater.renderObject.childCount, 1); - + // // await tester.pumpWidget( // Row( // textDirection: TextDirection.ltr, // children: [ // Portal( // key: firstPortal, - // child: PortalEntry( + // child: PortalTarget( // key: entryKey, - // portal: Container(), + // portalFollower: Container(), // child: Container(), // ), // ), @@ -1444,14 +1404,14 @@ Future main() async { // ], // ), // ); - + // // expect(firstPortalElement.theater.entries.length, 1); // expect(firstPortalElement.theater.renderObject.builders.length, 1); // expect(firstPortalElement.theater.renderObject.childCount, 1); // expect(secondPortalElement.theater.entries.length, 0); // expect(secondPortalElement.theater.renderObject.builders.length, 0); // expect(secondPortalElement.theater.renderObject.childCount, 0); - }); + }, skip: true); testWidgets('clip overflow', (tester) async {}, skip: true); From 71d286e68ccd7e70e13f46d376d2bf6cc789ce06 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Mon, 7 Feb 2022 15:48:07 +0000 Subject: [PATCH 23/35] Add rounded corners example --- example/lib/rounded_corners.dart | 132 +++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 example/lib/rounded_corners.dart diff --git a/example/lib/rounded_corners.dart b/example/lib/rounded_corners.dart new file mode 100644 index 0000000..a66ade0 --- /dev/null +++ b/example/lib/rounded_corners.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Portal( + child: MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Example'), + ), + body: Container( + padding: const EdgeInsets.all(10), + alignment: Alignment.centerLeft, + child: const ContextualMenuExample(), + ), + ), + ), + ); + } +} + +class ContextualMenuExample extends StatefulWidget { + const ContextualMenuExample({Key? key}) : super(key: key); + + @override + _ContextualMenuExampleState createState() => _ContextualMenuExampleState(); +} + +class _ContextualMenuExampleState extends State { + bool _showMenu = false; + + @override + Widget build(BuildContext context) { + return Center( + child: _ModalEntry( + visible: _showMenu, + onClose: () => setState(() => _showMenu = false), + childAnchor: Alignment.topRight, + menuAnchor: Alignment.topLeft, + menu: const Menu( + children: [ + PopupMenuItem( + height: 42, + child: Text('first'), + ), + PopupMenuItem( + height: 42, + child: Text('second'), + ), + ], + ), + child: ElevatedButton( + onPressed: () => setState(() => _showMenu = true), + child: const Text('show menu'), + ), + ), + ); + } +} + +class Menu extends StatelessWidget { + const Menu({ + Key? key, + required this.children, + }) : super(key: key); + + final List children; + + @override + Widget build(BuildContext context) { + return Card( + elevation: 8, + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + children: children, + ), + ), + ); + } +} + +class _ModalEntry extends StatelessWidget { + const _ModalEntry({ + Key? key, + required this.onClose, + required this.menu, + required this.visible, + required this.menuAnchor, + required this.childAnchor, + required this.child, + }) : super(key: key); + + final VoidCallback onClose; + final Widget menu; + final bool visible; + final Widget child; + final Alignment menuAnchor; + final Alignment childAnchor; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: visible ? onClose : null, + child: PortalTarget( + visible: visible, + portalFollower: menu, + anchor: const Aligned( + follower: Alignment.topLeft, + target: Alignment.bottomLeft, + widthFactor: 1, + backup: Aligned( + follower: Alignment.bottomLeft, + target: Alignment.topLeft, + widthFactor: 1, + ), + ), + child: IgnorePointer( + ignoring: visible, + child: child, + ), + ), + ); + } +} From 954a8082d41e0ffb10e0dcde40d967b74e2f2001 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Mon, 7 Feb 2022 15:51:13 +0000 Subject: [PATCH 24/35] Setup popup --- example/lib/rounded_corners.dart | 38 +++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/example/lib/rounded_corners.dart b/example/lib/rounded_corners.dart index a66ade0..47084aa 100644 --- a/example/lib/rounded_corners.dart +++ b/example/lib/rounded_corners.dart @@ -17,7 +17,7 @@ class MyApp extends StatelessWidget { body: Container( padding: const EdgeInsets.all(10), alignment: Alignment.centerLeft, - child: const ContextualMenuExample(), + child: const RoundedCornersExample(), ), ), ), @@ -25,25 +25,25 @@ class MyApp extends StatelessWidget { } } -class ContextualMenuExample extends StatefulWidget { - const ContextualMenuExample({Key? key}) : super(key: key); +class RoundedCornersExample extends StatefulWidget { + const RoundedCornersExample({Key? key}) : super(key: key); @override - _ContextualMenuExampleState createState() => _ContextualMenuExampleState(); + _RoundedCornersExampleState createState() => _RoundedCornersExampleState(); } -class _ContextualMenuExampleState extends State { - bool _showMenu = false; +class _RoundedCornersExampleState extends State { + bool _showPopup = false; @override Widget build(BuildContext context) { return Center( child: _ModalEntry( - visible: _showMenu, - onClose: () => setState(() => _showMenu = false), + visible: _showPopup, + onClose: () => setState(() => _showPopup = false), childAnchor: Alignment.topRight, menuAnchor: Alignment.topLeft, - menu: const Menu( + menu: const _Popup( children: [ PopupMenuItem( height: 42, @@ -53,19 +53,31 @@ class _ContextualMenuExampleState extends State { height: 42, child: Text('second'), ), + PopupMenuItem( + height: 42, + child: Text('third'), + ), + PopupMenuItem( + height: 42, + child: Text('forth'), + ), + PopupMenuItem( + height: 42, + child: Text('fifth'), + ), ], ), child: ElevatedButton( - onPressed: () => setState(() => _showMenu = true), - child: const Text('show menu'), + onPressed: () => setState(() => _showPopup = true), + child: const Text('show popup'), ), ), ); } } -class Menu extends StatelessWidget { - const Menu({ +class _Popup extends StatelessWidget { + const _Popup({ Key? key, required this.children, }) : super(key: key); From ab00951305fd7bd8139554dde748106154983aa3 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Mon, 7 Feb 2022 15:57:23 +0000 Subject: [PATCH 25/35] Adapt example for use case --- example/lib/rounded_corners.dart | 69 +++++++++----------------------- 1 file changed, 20 insertions(+), 49 deletions(-) diff --git a/example/lib/rounded_corners.dart b/example/lib/rounded_corners.dart index 47084aa..6606f6c 100644 --- a/example/lib/rounded_corners.dart +++ b/example/lib/rounded_corners.dart @@ -14,10 +14,9 @@ class MyApp extends StatelessWidget { appBar: AppBar( title: const Text('Example'), ), - body: Container( - padding: const EdgeInsets.all(10), - alignment: Alignment.centerLeft, - child: const RoundedCornersExample(), + body: const Padding( + padding: EdgeInsets.all(16), + child: RoundedCornersExample(), ), ), ), @@ -37,40 +36,21 @@ class _RoundedCornersExampleState extends State { @override Widget build(BuildContext context) { - return Center( - child: _ModalEntry( - visible: _showPopup, - onClose: () => setState(() => _showPopup = false), - childAnchor: Alignment.topRight, - menuAnchor: Alignment.topLeft, - menu: const _Popup( - children: [ - PopupMenuItem( - height: 42, - child: Text('first'), + return _ModalEntry( + visible: _showPopup, + onClose: () => setState(() => _showPopup = false), + popup: _Popup( + children: [ + for (var i = 0; i < 12; i++) + ListTile( + onTap: () => setState(() => _showPopup = false), + title: Text('$i'), ), - PopupMenuItem( - height: 42, - child: Text('second'), - ), - PopupMenuItem( - height: 42, - child: Text('third'), - ), - PopupMenuItem( - height: 42, - child: Text('forth'), - ), - PopupMenuItem( - height: 42, - child: Text('fifth'), - ), - ], - ), - child: ElevatedButton( - onPressed: () => setState(() => _showPopup = true), - child: const Text('show popup'), - ), + ], + ), + child: ElevatedButton( + onPressed: () => setState(() => _showPopup = true), + child: const Text('show popup'), ), ); } @@ -102,19 +82,15 @@ class _ModalEntry extends StatelessWidget { const _ModalEntry({ Key? key, required this.onClose, - required this.menu, + required this.popup, required this.visible, - required this.menuAnchor, - required this.childAnchor, required this.child, }) : super(key: key); final VoidCallback onClose; - final Widget menu; + final Widget popup; final bool visible; final Widget child; - final Alignment menuAnchor; - final Alignment childAnchor; @override Widget build(BuildContext context) { @@ -123,16 +99,11 @@ class _ModalEntry extends StatelessWidget { onTap: visible ? onClose : null, child: PortalTarget( visible: visible, - portalFollower: menu, + portalFollower: popup, anchor: const Aligned( follower: Alignment.topLeft, target: Alignment.bottomLeft, widthFactor: 1, - backup: Aligned( - follower: Alignment.bottomLeft, - target: Alignment.topLeft, - widthFactor: 1, - ), ), child: IgnorePointer( ignoring: visible, From 20a33ec3a626a64c006d0b9a039fd4a4d5575b88 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Mon, 7 Feb 2022 16:58:39 +0000 Subject: [PATCH 26/35] Clip rect --- example/lib/rounded_corners.dart | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/example/lib/rounded_corners.dart b/example/lib/rounded_corners.dart index 6606f6c..67886fa 100644 --- a/example/lib/rounded_corners.dart +++ b/example/lib/rounded_corners.dart @@ -66,12 +66,20 @@ class _Popup extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - elevation: 8, - child: IntrinsicWidth( - child: Column( - mainAxisSize: MainAxisSize.min, - children: children, + return Padding( + padding: const EdgeInsets.only( + bottom: 16, + ), + child: Card( + elevation: 8, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + children: children, + ), ), ), ); From 1db3ac5c02aae1751c4eb6d39884f7ee6df45455 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 13 Feb 2022 16:04:52 +0000 Subject: [PATCH 27/35] Add examples --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index bf8b659..6fe1ffc 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,16 @@ UI, no matter where it moves dynamically. On a low level, this means that you wrap the part of your UI that you want to follow in a `PortalTarget` widget and configure it. +#### Example + +Imagine you want to display tooltips when an avatar is hovered in your app. In +that case, the avatar would be the portal **target** and could be used to anchor +the tooltip that is overlayed. + +Another example would be a dropdown menu. The widget that shows the current +selection is the *target* and when tapping on it, the dropdown options would be +overlayed through the portal as the follower. + ### Follower A follower can only be used in combination with a target. You can use it for @@ -288,6 +298,13 @@ Specifically, this means that you can pass one `follower` to every `PortalTarget`, which will be displayed above your UI within the portal when you specify so. +#### Example + +If you wanted to display an autocomplete text field using `flutter_portal`, +you would want to follow the text field to overlay your autocomplete +suggestions. The widget for the autocomplete suggestions would be the portal +**follower** in that case. + ### Anchor Anchors define the layout connection between targets and followers. In general, From 3298639794d9907966f9cb3260938848d69ba801 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 13 Feb 2022 16:54:08 +0000 Subject: [PATCH 28/35] Add ExpandedAligned --- example/lib/rounded_corners.dart | 8 +-- lib/src/anchor.dart | 96 ++++++++++++++++++++++++++------ lib/src/custom_follower.dart | 3 +- lib/src/portal.dart | 2 +- test/anchor_test.dart | 12 ++-- 5 files changed, 92 insertions(+), 29 deletions(-) diff --git a/example/lib/rounded_corners.dart b/example/lib/rounded_corners.dart index 67886fa..02991e1 100644 --- a/example/lib/rounded_corners.dart +++ b/example/lib/rounded_corners.dart @@ -76,8 +76,8 @@ class _Popup extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: IntrinsicWidth( - child: Column( - mainAxisSize: MainAxisSize.min, + child: ListView( + shrinkWrap: true, children: children, ), ), @@ -90,14 +90,14 @@ class _ModalEntry extends StatelessWidget { const _ModalEntry({ Key? key, required this.onClose, - required this.popup, required this.visible, + required this.popup, required this.child, }) : super(key: key); final VoidCallback onClose; - final Widget popup; final bool visible; + final Widget popup; final Widget child; @override diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart index 54c27dd..b5cafd4 100644 --- a/lib/src/anchor.dart +++ b/lib/src/anchor.dart @@ -1,14 +1,18 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import '../flutter_portal.dart'; + /// The logic of layout and positioning of a follower element in relation to a /// target element. /// /// This is independent of the underlying rendering implementation. abstract class Anchor { + const Anchor(); + /// Returns the layout constraints that are given to the follower element. /// - /// The [targetRect] represents the bounds of the element which the follower + /// The [targetSize] represents the bounds of the element which the follower /// element should be anchored to. This must be the same value that is passed /// to [getFollowerOffset]. No assumptions should be made about the coordinate /// space, i.e. only the size of the target should be considered. @@ -17,23 +21,23 @@ abstract class Anchor { /// follower element in. This is irrespective of where the target is /// positioned within the full available space. BoxConstraints getFollowerConstraints({ - required Rect targetRect, + required Size targetSize, required BoxConstraints portalConstraints, }); /// Returns the offset at which to position the follower element in relation - /// to the top left of the [targetRect]. + /// to the top left of the [targetSize]. /// /// The [followerSize] is the final size of the follower element after layout /// based on the follower constraints determined by [getFollowerConstraints]. /// - /// The [targetRect] represents the bounds of the element which the follower + /// The [targetSize] represents the bounds of the element which the follower /// element should be anchored to. This must be the same value that is passed /// to [getFollowerConstraints]. /// /// The [portalRect] represents the bounds of the full available space to /// place the follower element in. Note that this is also relative to the top - /// left of the [targetRect]. + /// left of the [targetSize]. /// This means that every offset going into or coming out of this function is /// relative to the top-left corner of the target. /// @@ -46,13 +50,13 @@ abstract class Anchor { /// spans to absolute `(100, 100)` and the target rect starts at absolute /// `(40, 40)` and spans to absolute `(60, 60)`, the passed values will be: /// - /// * `Rect.fromLTWH(0, 0, 20, 20)` for the [targetRect]. + /// * `Rect.fromLTWH(0, 0, 20, 20)` for the [targetSize]. /// * `Rect.fromLTWH(-40, -40, 100, 100)` for the [portalRect]. /// * `Size(30, 30)` for the [followerSize]. /// * `Offset(20, 20)` as the return value. Offset getFollowerOffset({ required Size followerSize, - required Rect targetRect, + required Size targetSize, required Rect portalRect, }); } @@ -65,7 +69,7 @@ class Filled implements Anchor { @override BoxConstraints getFollowerConstraints({ - required Rect targetRect, + required Size targetSize, required BoxConstraints portalConstraints, }) { return BoxConstraints.tight(portalConstraints.biggest); @@ -74,7 +78,7 @@ class Filled implements Anchor { @override Offset getFollowerOffset({ required Size followerSize, - required Rect targetRect, + required Size targetSize, required Rect portalRect, }) { return Offset.zero; @@ -129,27 +133,27 @@ class Aligned implements Anchor { @override BoxConstraints getFollowerConstraints({ - required Rect targetRect, + required Size targetSize, required BoxConstraints portalConstraints, }) { final widthFactor = this.widthFactor; final heightFactor = this.heightFactor; return portalConstraints.loosen().tighten( - width: widthFactor == null ? null : targetRect.width * widthFactor, + width: widthFactor == null ? null : targetSize.width * widthFactor, height: - heightFactor == null ? null : targetRect.height * heightFactor, + heightFactor == null ? null : targetSize.height * heightFactor, ); } @override Offset getFollowerOffset({ required Size followerSize, - required Rect targetRect, + required Size targetSize, required Rect portalRect, }) { final followerRect = (Offset.zero & followerSize).alignedTo( - targetRect, + Offset.zero & targetSize, followerAlignment: follower, targetAlignment: target, offset: offset, @@ -160,7 +164,7 @@ class Aligned implements Anchor { if (backup != null) { return backup.getFollowerOffset( followerSize: followerSize, - targetRect: targetRect, + targetSize: targetSize, portalRect: portalRect, ); } @@ -184,7 +188,67 @@ class Aligned implements Anchor { } @override - int get hashCode => follower.hashCode ^ target.hashCode ^ offset.hashCode; + int get hashCode => Object.hash(follower, target, offset, backup); +} + +/// An anchor implementation that expands in the specified axes. +/// +/// Expanding means giving constraints that fill the portal rect based on the +/// offset of the follower. +/// +/// This might be useful if you want to not only clip your follower widget but +/// also round corners or anything related. +/// +/// This is similar to [Aligned] in that it lets you specify how the follower +/// should be aligned to the target, but you cannot specify a backup and instead +/// the constraints simply expand in the direction specified. +/// +/// Note that this assumes that the parent [Portal] uses maximum constraints, +/// i.e. the child of the [Portal] widget uses the maximum constraints. +@immutable +class ExpandedAligned extends Anchor { + const ExpandedAligned({ + required this.follower, + required this.target, + }); + + /// The reference point on the follower element. + final Alignment follower; + + /// The reference point on the target element + final Alignment target; + + @override + Offset getFollowerOffset({ + required Size followerSize, + required Size targetSize, + required Rect portalRect, + }) { + throw UnimplementedError(); + } + + @override + BoxConstraints getFollowerConstraints({ + required Size targetSize, + required BoxConstraints portalConstraints, + }) { + final portalRect = Offset.zero & portalConstraints.biggest; + throw UnimplementedError(); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! ExpandedAligned) { + return false; + } + return follower == other.follower && target == other.target; + } + + @override + int get hashCode => Object.hash(follower, target); } extension on Rect { diff --git a/lib/src/custom_follower.dart b/lib/src/custom_follower.dart index 981750d..bf1ef71 100644 --- a/lib/src/custom_follower.dart +++ b/lib/src/custom_follower.dart @@ -188,13 +188,12 @@ class CustomRenderFollowerLayer extends RenderProxyBox { ancestor: theater, ); - final targetRect = Offset.zero & targetSize; final theaterRect = theaterShift & theater.size; return anchor.getFollowerOffset( // The size is set in performLayout of the RenderProxyBoxMixin. followerSize: size, - targetRect: targetRect, + targetSize: targetSize, portalRect: theaterRect, ); } diff --git a/lib/src/portal.dart b/lib/src/portal.dart index 991a4ed..677fd5f 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -628,7 +628,7 @@ class _RenderPortalTarget extends RenderProxyBox { if (branch != null) { final constraints = anchor.getFollowerConstraints( portalConstraints: overlayLink.constraints!, - targetRect: Offset.zero & targetSize, + targetSize: targetSize, ); branch!.layout(constraints); if (_needsAddEntryInTheater) { diff --git a/test/anchor_test.dart b/test/anchor_test.dart index 993a191..97128ac 100644 --- a/test/anchor_test.dart +++ b/test/anchor_test.dart @@ -161,20 +161,20 @@ class _TestAnchor implements Anchor { @override BoxConstraints getFollowerConstraints({ - required Rect targetRect, + required Rect targetSize, required BoxConstraints portalConstraints, }) { - onGetSourceConstraints(targetRect, portalConstraints); + onGetSourceConstraints(targetSize, portalConstraints); return constraints; } @override Offset getFollowerOffset({ required Size followerSize, - required Rect targetRect, + required Rect targetSize, required Rect portalRect, }) { - onGetSourceOffset(followerSize, targetRect, portalRect); + onGetSourceOffset(followerSize, targetSize, portalRect); return Offset.zero; } } @@ -199,13 +199,13 @@ class _TestAligned extends Aligned { @override Offset getFollowerOffset({ required Size followerSize, - required Rect targetRect, + required Rect targetSize, required Rect portalRect, }) { onGetSourceOffset(); return super.getFollowerOffset( followerSize: followerSize, - targetRect: targetRect, + targetSize: targetSize, portalRect: portalRect, ); } From 08588501861895e609871955956bd8e72b04bf72 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 13 Feb 2022 16:59:02 +0000 Subject: [PATCH 29/35] Make tests pass --- lib/src/custom_follower.dart | 5 +++++ test/anchor_test.dart | 29 +++++++++++++---------------- test/widget_test.dart | 1 - 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/src/custom_follower.dart b/lib/src/custom_follower.dart index bf1ef71..4c0f12e 100644 --- a/lib/src/custom_follower.dart +++ b/lib/src/custom_follower.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:vector_math/vector_math_64.dart'; @@ -459,5 +460,9 @@ class _CustomFollowerLayer extends ContainerLayer { properties.add(DiagnosticsProperty('link', link)); properties.add( TransformProperty('transform', getLastTransform(), defaultValue: null)); + properties.add(DiagnosticsProperty( + 'linkedOffsetCallback', + linkedOffsetCallback, + )); } } diff --git a/test/anchor_test.dart b/test/anchor_test.dart index 97128ac..4e1ffda 100644 --- a/test/anchor_test.dart +++ b/test/anchor_test.dart @@ -1,30 +1,27 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; -import 'package:flutter/src/rendering/box.dart'; import 'package:flutter_portal/src/anchor.dart'; import 'package:flutter_portal/src/portal.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('$Anchor is passed proper constraints', (tester) async { - Rect? constraintsTargetRect; + Size? constraintsTargetSize; BoxConstraints? constraintsOverlayConstraints; Size? offsetSourceSize; - Rect? offsetTargetRect; + Size? offsetTargetSize; Rect? offsetTheaterRect; final anchor = _TestAnchor( constraints: const BoxConstraints.tightFor( width: 42, height: 42, ), - onGetSourceConstraints: (targetRect, overlayConstraints) { - constraintsTargetRect = targetRect; + onGetSourceConstraints: (targetSize, overlayConstraints) { + constraintsTargetSize = targetSize; constraintsOverlayConstraints = overlayConstraints; }, - onGetSourceOffset: (sourceSize, targetRect, theaterRect) { + onGetSourceOffset: (sourceSize, targetSize, theaterRect) { offsetSourceSize = sourceSize; - offsetTargetRect = targetRect; + offsetTargetSize = targetSize; offsetTheaterRect = theaterRect; }, ); @@ -69,12 +66,12 @@ void main() { ), )); - expect(constraintsTargetRect, Offset.zero & const Size(50, 50)); + expect(constraintsTargetSize, const Size(50, 50)); expect( constraintsOverlayConstraints, BoxConstraints.tight(const Size(100, 100)), ); - expect(constraintsTargetRect, offsetTargetRect); + expect(constraintsTargetSize, offsetTargetSize); expect(offsetSourceSize, const Size(42, 42)); expect(offsetTheaterRect, const Offset(-25, -25) & const Size(100, 100)); }); @@ -150,18 +147,18 @@ class _TestAnchor implements Anchor { final BoxConstraints constraints; final void Function( - Rect targetRect, + Size targetSize, BoxConstraints overlayConstraints, ) onGetSourceConstraints; final void Function( Size sourceSize, - Rect targetRect, + Size targetSize, Rect theaterRect, ) onGetSourceOffset; @override BoxConstraints getFollowerConstraints({ - required Rect targetSize, + required Size targetSize, required BoxConstraints portalConstraints, }) { onGetSourceConstraints(targetSize, portalConstraints); @@ -171,7 +168,7 @@ class _TestAnchor implements Anchor { @override Offset getFollowerOffset({ required Size followerSize, - required Rect targetSize, + required Size targetSize, required Rect portalRect, }) { onGetSourceOffset(followerSize, targetSize, portalRect); @@ -199,7 +196,7 @@ class _TestAligned extends Aligned { @override Offset getFollowerOffset({ required Size followerSize, - required Rect targetSize, + required Size targetSize, required Rect portalRect, }) { onGetSourceOffset(); diff --git a/test/widget_test.dart b/test/widget_test.dart index 73ecce4..476535e 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,7 +5,6 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; From d7a62ea60003c4b1f684e7784b852888f14d6ec0 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 13 Feb 2022 17:22:40 +0000 Subject: [PATCH 30/35] Refactor alignedTo --- lib/src/anchor.dart | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart index b5cafd4..9bd76d7 100644 --- a/lib/src/anchor.dart +++ b/lib/src/anchor.dart @@ -152,8 +152,8 @@ class Aligned implements Anchor { required Size targetSize, required Rect portalRect, }) { - final followerRect = (Offset.zero & followerSize).alignedTo( - Offset.zero & targetSize, + final followerRect = followerSize.alignedTo( + targetSize, followerAlignment: follower, targetAlignment: target, offset: offset, @@ -208,16 +208,19 @@ class Aligned implements Anchor { @immutable class ExpandedAligned extends Anchor { const ExpandedAligned({ - required this.follower, required this.target, + this.offset = Offset.zero, }); - /// The reference point on the follower element. - final Alignment follower; - - /// The reference point on the target element + /// The reference point on the target element. + /// + /// Note that there is no alignment for the follower element since the + /// follower size is expanded. final Alignment target; + /// Offset to shift the follower element by after all calculations are made. + final Offset offset; + @override Offset getFollowerOffset({ required Size followerSize, @@ -233,6 +236,7 @@ class ExpandedAligned extends Anchor { required BoxConstraints portalConstraints, }) { final portalRect = Offset.zero & portalConstraints.biggest; + final followerOffset = target.alongSize(targetSize) + offset; throw UnimplementedError(); } @@ -244,27 +248,30 @@ class ExpandedAligned extends Anchor { if (other is! ExpandedAligned) { return false; } - return follower == other.follower && target == other.target; + return target == other.target && offset == other.offset; } @override - int get hashCode => Object.hash(follower, target); + int get hashCode => Object.hash(target, offset); } -extension on Rect { +extension on Size { + /// Returns a [Rect] that is aligned to the sizes (follower size / this and + /// the target size) along the given alignments, shifted by [offset]. Rect alignedTo( - Rect target, { + Size targetSize, { required Alignment followerAlignment, required Alignment targetAlignment, Offset offset = Offset.zero, }) { - final followerOffset = targetAlignment.alongSize(target.size) - - followerAlignment.alongSize(size) + - target.topLeft + + final followerOffset = targetAlignment.alongSize(targetSize) - + followerAlignment.alongSize(this) + offset; - return followerOffset & size; + return followerOffset & this; } +} +extension on Rect { /// Returns true if [rect] is fully contained within this rect /// If the [rect] has any part that lies outside of this parent /// false will be returned From 8953b14c71086994c464e4a7827c3519260dbfa6 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 13 Feb 2022 17:37:58 +0000 Subject: [PATCH 31/35] Revert `ExpandedAligned` --- lib/src/anchor.dart | 66 --------------------------------------------- 1 file changed, 66 deletions(-) diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart index 9bd76d7..3effc78 100644 --- a/lib/src/anchor.dart +++ b/lib/src/anchor.dart @@ -1,8 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; -import '../flutter_portal.dart'; - /// The logic of layout and positioning of a follower element in relation to a /// target element. /// @@ -191,70 +189,6 @@ class Aligned implements Anchor { int get hashCode => Object.hash(follower, target, offset, backup); } -/// An anchor implementation that expands in the specified axes. -/// -/// Expanding means giving constraints that fill the portal rect based on the -/// offset of the follower. -/// -/// This might be useful if you want to not only clip your follower widget but -/// also round corners or anything related. -/// -/// This is similar to [Aligned] in that it lets you specify how the follower -/// should be aligned to the target, but you cannot specify a backup and instead -/// the constraints simply expand in the direction specified. -/// -/// Note that this assumes that the parent [Portal] uses maximum constraints, -/// i.e. the child of the [Portal] widget uses the maximum constraints. -@immutable -class ExpandedAligned extends Anchor { - const ExpandedAligned({ - required this.target, - this.offset = Offset.zero, - }); - - /// The reference point on the target element. - /// - /// Note that there is no alignment for the follower element since the - /// follower size is expanded. - final Alignment target; - - /// Offset to shift the follower element by after all calculations are made. - final Offset offset; - - @override - Offset getFollowerOffset({ - required Size followerSize, - required Size targetSize, - required Rect portalRect, - }) { - throw UnimplementedError(); - } - - @override - BoxConstraints getFollowerConstraints({ - required Size targetSize, - required BoxConstraints portalConstraints, - }) { - final portalRect = Offset.zero & portalConstraints.biggest; - final followerOffset = target.alongSize(targetSize) + offset; - throw UnimplementedError(); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - if (other is! ExpandedAligned) { - return false; - } - return target == other.target && offset == other.offset; - } - - @override - int get hashCode => Object.hash(target, offset); -} - extension on Size { /// Returns a [Rect] that is aligned to the sizes (follower size / this and /// the target size) along the given alignments, shifted by [offset]. From b2b298d9a001155430ec55410c5213e68ac34d76 Mon Sep 17 00:00:00 2001 From: creativecreatorormaybenot Date: Sun, 13 Feb 2022 17:38:55 +0000 Subject: [PATCH 32/35] Add todo --- example/lib/rounded_corners.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/example/lib/rounded_corners.dart b/example/lib/rounded_corners.dart index 02991e1..c9ee200 100644 --- a/example/lib/rounded_corners.dart +++ b/example/lib/rounded_corners.dart @@ -108,6 +108,7 @@ class _ModalEntry extends StatelessWidget { child: PortalTarget( visible: visible, portalFollower: popup, + // todo: implement anchor that sizes the follower based on the available space within the portal at the calculated offset. anchor: const Aligned( follower: Alignment.topLeft, target: Alignment.bottomLeft, From 73ded7b9a7d31a97013fc6c021a613cc7c1d4df6 Mon Sep 17 00:00:00 2001 From: fzyzcjy Date: Mon, 21 Feb 2022 11:10:47 +0800 Subject: [PATCH 33/35] follow flutter linter and fix warnings or hints --- example/lib/discovery.dart | 2 +- lib/src/portal.dart | 1 - test/widget_test.dart | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/example/lib/discovery.dart b/example/lib/discovery.dart index 8f8e153..f54c5a6 100644 --- a/example/lib/discovery.dart +++ b/example/lib/discovery.dart @@ -84,7 +84,7 @@ class Discovery extends StatelessWidget { portal: Stack( children: [ CustomPaint( - painter: HolePainter(Theme.of(context).accentColor), + painter: HolePainter(Theme.of(context).colorScheme.secondary), child: TweenAnimationBuilder( duration: kThemeAnimationDuration, curve: Curves.easeOut, diff --git a/lib/src/portal.dart b/lib/src/portal.dart index 6c5c166..c6be852 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; import 'custom_follower.dart'; diff --git a/test/widget_test.dart b/test/widget_test.dart index f26db4a..0108873 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,16 +5,15 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_portal/src/portal.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_portal/flutter_portal.dart'; Future fetchFont() async { final roboto = File.fromUri( From ac02415bb1853cce8cc5079643624ffe70b3590e Mon Sep 17 00:00:00 2001 From: fzyzcjy Date: Mon, 21 Feb 2022 11:11:33 +0800 Subject: [PATCH 34/35] remove `top_level_function_literal_block` since flutter linter says `warning: 'top_level_function_literal_block' isn't a recognized error code. (unrecognized_error_code at [flutter_portal] analysis_options.yaml:16)` --- analysis_options.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 7b7f222..724bc00 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,9 +12,6 @@ analyzer: # in this file included_file_warning: ignore - # Causes false positives (https://github.com/dart-lang/sdk/issues/41571 - top_level_function_literal_block: ignore - linter: rules: # Personal preference. I don't find it more readable From 81614d469288aafae2350e4c4bff98994f7ebe0f Mon Sep 17 00:00:00 2001 From: fzyzcjy Date: Mon, 21 Feb 2022 11:12:34 +0800 Subject: [PATCH 35/35] remove `authors` since flutter linter says `warning: The 'authors' field is no longer used and can be removed. (deprecated_field at [flutter_portal] pubspec.yaml:5)`; and doc says `Deprecated. Use a verified publisher instead. ... The pub.dev site no longer displays package authors` --- pubspec.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 977d73f..6ef0eb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,8 +2,6 @@ name: flutter_portal version: 0.4.0 description: Overlay/OverlayEntry, but implemented as a widget for a declarative API. homepage: https://github.com/rrousselGit/flutter_portal -authors: - - Remi Rousselet environment: sdk: '>=2.12.0 <3.0.0'