From 2d54ae83259bb5a21a602535e493738373edd847 Mon Sep 17 00:00:00 2001 From: Jaylen Bian Date: Sun, 8 Aug 2021 22:04:49 +0800 Subject: [PATCH 1/4] [infra_ui][overlay] Implement remaining overlap behavior --- .../lib/src/flowy_overlay/flowy_overlay.dart | 1 - .../flowy_overlay/overlay_layout_delegate.dart | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart index 9fd3da09ba..b2e61501cf 100644 --- a/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart +++ b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart @@ -264,7 +264,6 @@ class FlowyOverlayState extends State { ), ), ]; - return Stack( children: children..addAll(overlays), ); diff --git a/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_layout_delegate.dart b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_layout_delegate.dart index 57b13d0584..421b746f13 100644 --- a/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_layout_delegate.dart +++ b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_layout_delegate.dart @@ -55,6 +55,24 @@ class OverlayLayoutDelegate extends SingleChildLayoutDelegate { constraints.maxHeight - anchorRect.bottom, )); break; + case AnchorDirection.topWithLeftAligned: + childConstraints = BoxConstraints.loose(Size( + constraints.maxWidth - anchorRect.left, + anchorRect.top, + )); + break; + case AnchorDirection.topWithCenterAligned: + childConstraints = BoxConstraints.loose(Size( + constraints.maxWidth, + anchorRect.top, + )); + break; + case AnchorDirection.topWithRightAligned: + childConstraints = BoxConstraints.loose(Size( + anchorRect.right, + anchorRect.top, + )); + break; case AnchorDirection.rightWithTopAligned: childConstraints = BoxConstraints.loose(Size( constraints.maxWidth - anchorRect.right, From 051240a2fbdbd85f0934379a2b36c994b8d84826 Mon Sep 17 00:00:00 2001 From: Jaylen Bian Date: Mon, 9 Aug 2021 10:48:45 +0800 Subject: [PATCH 2/4] [infra_ui][overlay] Implement list overlay --- .../packages/flowy_infra_ui/lib/basis.dart | 3 + .../lib/src/flowy_overlay/list_overlay.dart | 99 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart diff --git a/app_flowy/packages/flowy_infra_ui/lib/basis.dart b/app_flowy/packages/flowy_infra_ui/lib/basis.dart index 3758072c99..59a2bc6533 100644 --- a/app_flowy/packages/flowy_infra_ui/lib/basis.dart +++ b/app_flowy/packages/flowy_infra_ui/lib/basis.dart @@ -3,3 +3,6 @@ import 'package:flutter/material.dart'; // MARK: - Shared Builder typedef WidgetBuilder = Widget Function(); + +typedef IndexedCallback = void Function(int index); +typedef IndexedValueCallback = void Function(T value, int index); diff --git a/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart new file mode 100644 index 0000000000..61d7ec8fe4 --- /dev/null +++ b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart @@ -0,0 +1,99 @@ +import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; +import 'package:flutter/material.dart'; + +class ListOverlay extends StatelessWidget { + const ListOverlay({ + Key? key, + required this.itemBuilder, + this.itemCount, + this.controller, + this.maxWidth = double.infinity, + this.maxHeight = double.infinity, + }) : super(key: key); + + final IndexedWidgetBuilder itemBuilder; + final int? itemCount; + final ScrollController? controller; + final double maxWidth; + final double maxHeight; + + static void showWithAnchor( + BuildContext context, { + required String identifier, + required IndexedWidgetBuilder itemBuilder, + int? itemCount, + ScrollController? controller, + double maxWidth = double.infinity, + double maxHeight = double.infinity, + required BuildContext anchorContext, + AnchorDirection? anchorDirection, + FlowyOverlayDelegate? delegate, + OverlapBehaviour? overlapBehaviour, + }) { + FlowyOverlay.of(context).insertWithAnchor( + widget: ListOverlay( + itemBuilder: itemBuilder, + itemCount: itemCount, + controller: controller, + maxWidth: maxWidth, + maxHeight: maxHeight, + ), + identifier: identifier, + anchorContext: anchorContext, + anchorDirection: anchorDirection, + delegate: delegate, + overlapBehaviour: overlapBehaviour, + ); + } + + static void showWithRect( + BuildContext context, { + required BuildContext anchorContext, + required String identifier, + required IndexedWidgetBuilder itemBuilder, + int? itemCount, + ScrollController? controller, + double maxWidth = double.infinity, + double maxHeight = double.infinity, + required Offset anchorPosition, + required Size anchorSize, + AnchorDirection? anchorDirection, + FlowyOverlayDelegate? delegate, + OverlapBehaviour? overlapBehaviour, + }) { + FlowyOverlay.of(context).insertWithRect( + widget: ListOverlay( + itemBuilder: itemBuilder, + itemCount: itemCount, + controller: controller, + maxWidth: maxWidth, + maxHeight: maxHeight, + ), + identifier: identifier, + anchorPosition: anchorPosition, + anchorSize: anchorSize, + anchorDirection: anchorDirection, + delegate: delegate, + overlapBehaviour: overlapBehaviour, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints.tight(Size(maxWidth, maxHeight)), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(6)), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.1), spreadRadius: 1, blurRadius: 20.0), + ], + ), + child: ListView.builder( + shrinkWrap: true, + itemBuilder: itemBuilder, + itemCount: itemCount, + ), + ); + } +} From 82e6856f757f12d4de389727a9fec0408dd4ca37 Mon Sep 17 00:00:00 2001 From: Jaylen Bian Date: Mon, 9 Aug 2021 10:49:16 +0800 Subject: [PATCH 3/4] [infra_ui][overlay] Implement option overlay --- .../flowy_infra_ui/lib/flowy_infra_ui.dart | 2 + .../lib/flowy_infra_ui_web.dart | 2 + .../lib/src/flowy_overlay/option_overlay.dart | 97 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart diff --git a/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart b/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart index abeacbb38b..91768bd645 100644 --- a/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart +++ b/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart @@ -6,3 +6,5 @@ export 'src/keyboard/keyboard_visibility_detector.dart'; // Overlay export 'src/flowy_overlay/flowy_overlay.dart'; +export 'src/flowy_overlay/list_overlay.dart'; +export 'src/flowy_overlay/option_overlay.dart'; diff --git a/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui_web.dart b/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui_web.dart index abeacbb38b..91768bd645 100644 --- a/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui_web.dart +++ b/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui_web.dart @@ -6,3 +6,5 @@ export 'src/keyboard/keyboard_visibility_detector.dart'; // Overlay export 'src/flowy_overlay/flowy_overlay.dart'; +export 'src/flowy_overlay/list_overlay.dart'; +export 'src/flowy_overlay/option_overlay.dart'; diff --git a/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart new file mode 100644 index 0000000000..8f2feaffb4 --- /dev/null +++ b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart @@ -0,0 +1,97 @@ +import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; +import 'package:flutter/material.dart'; + +class OptionItem { + const OptionItem(this.icon, this.title); + + final Icon? icon; + final String title; +} + +class OptionOverlay extends StatelessWidget { + const OptionOverlay({ + Key? key, + required this.items, + this.onHover, + this.onTap, + }) : super(key: key); + + final List items; + final IndexedValueCallback? onHover; + final IndexedValueCallback? onTap; + + static void showWithAnchor( + BuildContext context, { + required String identifier, + required List items, + IndexedValueCallback? onHover, + IndexedValueCallback? onTap, + required BuildContext anchorContext, + AnchorDirection? anchorDirection, + FlowyOverlayDelegate? delegate, + OverlapBehaviour? overlapBehaviour, + }) { + FlowyOverlay.of(context).insertWithAnchor( + widget: OptionOverlay( + items: items, + onHover: onHover, + onTap: onTap, + ), + identifier: identifier, + anchorContext: anchorContext, + anchorDirection: anchorDirection, + delegate: delegate, + overlapBehaviour: overlapBehaviour, + ); + } + + @override + Widget build(BuildContext context) { + final List<_OptionListItem> listItems = items.map((e) => _OptionListItem(e)).toList(); + return ListOverlay( + itemBuilder: (context, index) { + return MouseRegion( + cursor: SystemMouseCursors.click, + onHover: onHover != null ? (_) => onHover!(items[index], index) : null, + child: GestureDetector( + onTap: onTap != null ? () => onTap!(items[index], index) : null, + child: listItems[index], + ), + ); + }, + itemCount: listItems.length, + ); + } +} + +class _OptionListItem extends StatelessWidget { + const _OptionListItem( + this.value, { + Key? key, + }) : super(key: key); + + final T value; + + @override + Widget build(BuildContext context) { + if (T == String || T == OptionItem) { + var children = []; + if (value is String) { + children = [ + Text(value as String), + ]; + } else if (value is OptionItem) { + final optionItem = value as OptionItem; + children = [ + if (optionItem.icon != null) optionItem.icon!, + Text(optionItem.title), + ]; + } + return Column( + mainAxisSize: MainAxisSize.min, + children: children, + ); + } + throw UnimplementedError('The type $T is not supported by option list.'); + } +} From 0edd8b86c052cebc772adc276e03ed5fa049cd91 Mon Sep 17 00:00:00 2001 From: Jaylen Bian Date: Mon, 9 Aug 2021 10:49:37 +0800 Subject: [PATCH 4/4] [infra_ui][overlay] Update overlay example project for list_overlay and option_overlay --- .../example/lib/overlay/overlay_screen.dart | 268 ++++++++++-------- 1 file changed, 157 insertions(+), 111 deletions(-) diff --git a/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart b/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart index be6c0fd115..34a28a9898 100644 --- a/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart +++ b/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart @@ -1,3 +1,4 @@ +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -48,109 +49,31 @@ class OverlayScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Overlay Demo'), - ), - body: ChangeNotifierProvider( - create: (context) => OverlayDemoConfiguration(AnchorDirection.rightWithTopAligned, OverlapBehaviour.stretch), - child: Builder(builder: (providerContext) { - return Center( - child: ConstrainedBox( - constraints: const BoxConstraints.tightFor(width: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 48.0), - ElevatedButton( - onPressed: () { - final windowSize = MediaQuery.of(context).size; - FlowyOverlay.of(context).insertCustom( - widget: Positioned( - left: windowSize.width / 2.0 - 100, - top: 200, - child: SizedBox( - width: 200, - height: 100, - child: Card( - color: Colors.green[200], - child: GestureDetector( - // ignore: avoid_print - onTapDown: (_) => print('Hello Flutter'), - child: const Center(child: FlutterLogo(size: 100)), - ), - ), - ), - ), - identifier: 'overlay_flutter_logo', - delegate: null, - ); - }, - child: const Text('Show Overlay'), - ), - const SizedBox(height: 24.0), - DropdownButton( - value: providerContext.watch().anchorDirection, - onChanged: (AnchorDirection? newValue) { - if (newValue != null) { - providerContext.read().anchorDirection = newValue; - } - }, - items: AnchorDirection.values.map((AnchorDirection classType) { - return DropdownMenuItem(value: classType, child: Text(classType.toString())); - }).toList(), - ), - const SizedBox(height: 24.0), - DropdownButton( - value: providerContext.watch().overlapBehaviour, - onChanged: (OverlapBehaviour? newValue) { - if (newValue != null) { - providerContext.read().overlapBehaviour = newValue; - } - }, - items: OverlapBehaviour.values.map((OverlapBehaviour classType) { - return DropdownMenuItem(value: classType, child: Text(classType.toString())); - }).toList(), - ), - const SizedBox(height: 24.0), - Builder(builder: (buttonContext) { - return SizedBox( - height: 100, - child: ElevatedButton( - onPressed: () { - FlowyOverlay.of(context).insertWithAnchor( - widget: SizedBox( - width: 300, - height: 50, - child: Card( - color: Colors.grey[200], - child: GestureDetector( - // ignore: avoid_print - onTapDown: (_) => print('Hello Flutter'), - child: const Center(child: FlutterLogo(size: 50)), - ), - ), - ), - identifier: 'overlay_anchored_card', - delegate: null, - anchorContext: buttonContext, - anchorDirection: providerContext.read().anchorDirection, - overlapBehaviour: providerContext.read().overlapBehaviour, - ); - }, - child: const Text('Show Anchored Overlay'), - ), - ); - }), - const SizedBox(height: 24.0), - ElevatedButton( - onPressed: () { - final windowSize = MediaQuery.of(context).size; - FlowyOverlay.of(context).insertWithRect( - widget: SizedBox( + appBar: AppBar( + title: const Text('Overlay Demo'), + ), + body: ChangeNotifierProvider( + create: (context) => OverlayDemoConfiguration(AnchorDirection.rightWithTopAligned, OverlapBehaviour.stretch), + child: Builder(builder: (providerContext) { + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 48.0), + ElevatedButton( + onPressed: () { + final windowSize = MediaQuery.of(context).size; + FlowyOverlay.of(context).insertCustom( + widget: Positioned( + left: windowSize.width / 2.0 - 100, + top: 200, + child: SizedBox( width: 200, height: 100, child: Card( - color: Colors.orange[200], + color: Colors.green[200], child: GestureDetector( // ignore: avoid_print onTapDown: (_) => print('Hello Flutter'), @@ -158,21 +81,144 @@ class OverlayScreen extends StatelessWidget { ), ), ), - identifier: 'overlay_positioned_card', - delegate: null, - anchorPosition: Offset(0, windowSize.height - 200), - anchorSize: Size.zero, + ), + identifier: 'overlay_flutter_logo', + delegate: null, + ); + }, + child: const Text('Show Overlay'), + ), + const SizedBox(height: 24.0), + DropdownButton( + value: providerContext.watch().anchorDirection, + onChanged: (AnchorDirection? newValue) { + if (newValue != null) { + providerContext.read().anchorDirection = newValue; + } + }, + items: AnchorDirection.values.map((AnchorDirection classType) { + return DropdownMenuItem(value: classType, child: Text(classType.toString())); + }).toList(), + ), + const SizedBox(height: 24.0), + DropdownButton( + value: providerContext.watch().overlapBehaviour, + onChanged: (OverlapBehaviour? newValue) { + if (newValue != null) { + providerContext.read().overlapBehaviour = newValue; + } + }, + items: OverlapBehaviour.values.map((OverlapBehaviour classType) { + return DropdownMenuItem(value: classType, child: Text(classType.toString())); + }).toList(), + ), + const SizedBox(height: 24.0), + Builder(builder: (buttonContext) { + return SizedBox( + height: 100, + child: ElevatedButton( + onPressed: () { + FlowyOverlay.of(context).insertWithAnchor( + widget: SizedBox( + width: 300, + height: 50, + child: Card( + color: Colors.grey[200], + child: GestureDetector( + // ignore: avoid_print + onTapDown: (_) => print('Hello Flutter'), + child: const Center(child: FlutterLogo(size: 50)), + ), + ), + ), + identifier: 'overlay_anchored_card', + delegate: null, + anchorContext: buttonContext, + anchorDirection: providerContext.read().anchorDirection, + overlapBehaviour: providerContext.read().overlapBehaviour, + ); + }, + child: const Text('Show Anchored Overlay'), + ), + ); + }), + const SizedBox(height: 24.0), + ElevatedButton( + onPressed: () { + final windowSize = MediaQuery.of(context).size; + FlowyOverlay.of(context).insertWithRect( + widget: SizedBox( + width: 200, + height: 100, + child: Card( + color: Colors.orange[200], + child: GestureDetector( + // ignore: avoid_print + onTapDown: (_) => print('Hello Flutter'), + child: const Center(child: FlutterLogo(size: 100)), + ), + ), + ), + identifier: 'overlay_positioned_card', + delegate: null, + anchorPosition: Offset(0, windowSize.height - 200), + anchorSize: Size.zero, + anchorDirection: providerContext.read().anchorDirection, + overlapBehaviour: providerContext.read().overlapBehaviour, + ); + }, + child: const Text('Show Positioned Overlay'), + ), + const SizedBox(height: 24.0), + Builder(builder: (buttonContext) { + return ElevatedButton( + onPressed: () { + ListOverlay.showWithAnchor( + context, + itemBuilder: (_, index) => Card( + margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), + elevation: 0, + child: Text( + 'Option $index', + style: const TextStyle(fontSize: 20.0, color: Colors.black), + ), + ), + itemCount: 10, + identifier: 'overlay_list_menu', + anchorContext: buttonContext, + anchorDirection: providerContext.read().anchorDirection, + overlapBehaviour: providerContext.read().overlapBehaviour, + maxWidth: 200.0, + maxHeight: 200.0, + ); + }, + child: const Text('Show List Overlay'), + ); + }), + const SizedBox(height: 24.0), + Builder(builder: (buttonContext) { + return ElevatedButton( + onPressed: () { + OptionOverlay.showWithAnchor( + context, + items: ['Alpha', 'Beta', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel'], + onHover: (value, index) => print('Did hover option $index, value $value'), + onTap: (value, index) => print('Did tap option $index, value $value'), + identifier: 'overlay_options', + anchorContext: buttonContext, anchorDirection: providerContext.read().anchorDirection, overlapBehaviour: providerContext.read().overlapBehaviour, ); }, - child: const Text('Show Positioned Overlay'), - ), - ], - ), + child: const Text('Show Options Overlay'), + ); + }), + ], ), - ); - }), - )); + ), + ); + }), + ), + ); } }