popover.dart 5.7 KB

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