popover.dart 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import 'package:appflowy_popover/src/layout.dart';
  2. import 'package:flutter/material.dart';
  3. import 'mask.dart';
  4. import 'mutex.dart';
  5. class PopoverController {
  6. PopoverState? _state;
  7. close() {
  8. _state?.close();
  9. }
  10. show() {
  11. _state?.showOverlay();
  12. }
  13. }
  14. class PopoverTriggerFlags {
  15. static const int none = 0x00;
  16. static const int click = 0x01;
  17. static const int hover = 0x02;
  18. }
  19. enum PopoverDirection {
  20. // Corner aligned with a corner of the SourceWidget
  21. topLeft,
  22. topRight,
  23. bottomLeft,
  24. bottomRight,
  25. center,
  26. // Edge aligned with a edge of the SourceWidget
  27. topWithLeftAligned,
  28. topWithCenterAligned,
  29. topWithRightAligned,
  30. rightWithTopAligned,
  31. rightWithCenterAligned,
  32. rightWithBottomAligned,
  33. bottomWithLeftAligned,
  34. bottomWithCenterAligned,
  35. bottomWithRightAligned,
  36. leftWithTopAligned,
  37. leftWithCenterAligned,
  38. leftWithBottomAligned,
  39. custom,
  40. }
  41. class Popover extends StatefulWidget {
  42. final PopoverController? controller;
  43. /// The offset from the [child] where the popover will be drawn
  44. final Offset? offset;
  45. /// Amount of padding between the edges of the window and the popover
  46. final EdgeInsets? windowPadding;
  47. final Decoration? maskDecoration;
  48. /// The function used to build the popover.
  49. final Widget? Function(BuildContext context) popupBuilder;
  50. /// Specify how the popover can be triggered when interacting with the child
  51. /// by supplying a bitwise-OR combination of one or more [PopoverTriggerFlags]
  52. final int triggerActions;
  53. /// If multiple popovers are exclusive,
  54. /// pass the same mutex to them.
  55. final PopoverMutex? mutex;
  56. /// The direction of the popover
  57. final PopoverDirection direction;
  58. final void Function()? onClose;
  59. final Future<bool> Function()? canClose;
  60. final bool asBarrier;
  61. /// The content area of the popover.
  62. final Widget child;
  63. const Popover({
  64. Key? key,
  65. required this.child,
  66. required this.popupBuilder,
  67. this.controller,
  68. this.offset,
  69. this.maskDecoration = const BoxDecoration(
  70. color: Color.fromARGB(0, 244, 67, 54),
  71. ),
  72. this.triggerActions = 0,
  73. this.direction = PopoverDirection.rightWithTopAligned,
  74. this.mutex,
  75. this.windowPadding,
  76. this.onClose,
  77. this.canClose,
  78. this.asBarrier = false,
  79. }) : super(key: key);
  80. @override
  81. State<Popover> createState() => PopoverState();
  82. }
  83. class PopoverState extends State<Popover> {
  84. static final RootOverlayEntry _rootEntry = RootOverlayEntry();
  85. final PopoverLink popoverLink = PopoverLink();
  86. @override
  87. void initState() {
  88. widget.controller?._state = this;
  89. super.initState();
  90. }
  91. void showOverlay() {
  92. close();
  93. if (widget.mutex != null) {
  94. widget.mutex?.state = this;
  95. }
  96. final shouldAddMask = _rootEntry.isEmpty;
  97. final newEntry = OverlayEntry(builder: (context) {
  98. final children = <Widget>[];
  99. if (shouldAddMask) {
  100. children.add(
  101. PopoverMask(
  102. decoration: widget.maskDecoration,
  103. onTap: () async {
  104. if (!(await widget.canClose?.call() ?? true)) {
  105. return;
  106. }
  107. _removeRootOverlay();
  108. },
  109. onExit: () => _removeRootOverlay(),
  110. ),
  111. );
  112. }
  113. children.add(
  114. PopoverContainer(
  115. direction: widget.direction,
  116. popoverLink: popoverLink,
  117. offset: widget.offset ?? Offset.zero,
  118. windowPadding: widget.windowPadding ?? EdgeInsets.zero,
  119. popupBuilder: widget.popupBuilder,
  120. onClose: () => close(),
  121. onCloseAll: () => _removeRootOverlay(),
  122. ),
  123. );
  124. return Stack(children: children);
  125. });
  126. _rootEntry.addEntry(context, this, newEntry, widget.asBarrier);
  127. }
  128. void close() {
  129. if (_rootEntry.contains(this)) {
  130. _rootEntry.removeEntry(this);
  131. widget.onClose?.call();
  132. }
  133. }
  134. void _removeRootOverlay() {
  135. _rootEntry.popEntry();
  136. if (widget.mutex?.state == this) {
  137. widget.mutex?.removeState();
  138. }
  139. }
  140. @override
  141. void deactivate() {
  142. close();
  143. super.deactivate();
  144. }
  145. @override
  146. Widget build(BuildContext context) {
  147. return PopoverTarget(
  148. link: popoverLink,
  149. child: _buildChild(context),
  150. );
  151. }
  152. Widget _buildChild(BuildContext context) {
  153. if (widget.triggerActions == 0) {
  154. return widget.child;
  155. }
  156. return MouseRegion(
  157. onEnter: (event) {
  158. if (widget.triggerActions & PopoverTriggerFlags.hover != 0) {
  159. showOverlay();
  160. }
  161. },
  162. child: Listener(
  163. child: widget.child,
  164. onPointerDown: (PointerDownEvent event) {
  165. if (widget.triggerActions & PopoverTriggerFlags.click != 0) {
  166. showOverlay();
  167. }
  168. },
  169. ),
  170. );
  171. }
  172. }
  173. class PopoverContainer extends StatefulWidget {
  174. final Widget? Function(BuildContext context) popupBuilder;
  175. final PopoverDirection direction;
  176. final PopoverLink popoverLink;
  177. final Offset offset;
  178. final EdgeInsets windowPadding;
  179. final void Function() onClose;
  180. final void Function() onCloseAll;
  181. const PopoverContainer({
  182. Key? key,
  183. required this.popupBuilder,
  184. required this.direction,
  185. required this.popoverLink,
  186. required this.offset,
  187. required this.windowPadding,
  188. required this.onClose,
  189. required this.onCloseAll,
  190. }) : super(key: key);
  191. @override
  192. State<StatefulWidget> createState() => PopoverContainerState();
  193. static PopoverContainerState of(BuildContext context) {
  194. if (context is StatefulElement && context.state is PopoverContainerState) {
  195. return context.state as PopoverContainerState;
  196. }
  197. final PopoverContainerState? result =
  198. context.findAncestorStateOfType<PopoverContainerState>();
  199. return result!;
  200. }
  201. }
  202. class PopoverContainerState extends State<PopoverContainer> {
  203. @override
  204. Widget build(BuildContext context) {
  205. return CustomSingleChildLayout(
  206. delegate: PopoverLayoutDelegate(
  207. direction: widget.direction,
  208. link: widget.popoverLink,
  209. offset: widget.offset,
  210. windowPadding: widget.windowPadding,
  211. ),
  212. child: widget.popupBuilder(context),
  213. );
  214. }
  215. void close() => widget.onClose();
  216. void closeAll() => widget.onCloseAll();
  217. }