suggestion.dart 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import 'dart:math';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:flutter/rendering.dart';
  4. import 'package:flutter/widgets.dart';
  5. /// Controls the location of the suggestion popup of [SuggestionPortal].
  6. class SuggestionPortalController extends OverlayPortalController {
  7. final _cursorRect = ValueNotifier<Rect>(Rect.zero);
  8. /// Updates the location of the suggestion popup to [rect]. If the popup is
  9. /// not showing, it will be shown after this call.
  10. void update(Rect rect) {
  11. _cursorRect.value = rect;
  12. if (!isShowing) show();
  13. }
  14. }
  15. /// A convenience widget to place a suggestion popup around the cursor specified
  16. /// by [SuggestionPortalController].
  17. class SuggestionPortal extends StatefulWidget {
  18. const SuggestionPortal({
  19. super.key,
  20. required this.controller,
  21. required this.overlayBuilder,
  22. required this.child,
  23. this.padding = const EdgeInsets.all(8),
  24. this.cursorMargin = const EdgeInsets.all(4),
  25. });
  26. final SuggestionPortalController controller;
  27. final WidgetBuilder overlayBuilder;
  28. /// The minimum space between [child] and the screen edge.
  29. final EdgeInsets padding;
  30. /// The minimum space between [child] and the cursor. Currently, only top and
  31. /// bottom are used.
  32. final EdgeInsets cursorMargin;
  33. final Widget child;
  34. @override
  35. State<SuggestionPortal> createState() => _SuggestionPortalState();
  36. }
  37. class _SuggestionPortalState extends State<SuggestionPortal> {
  38. @override
  39. Widget build(BuildContext context) {
  40. return OverlayPortal.targetsRootOverlay(
  41. controller: widget.controller,
  42. overlayChildBuilder: (context) {
  43. return SuggestionLayout(
  44. cursorRect: widget.controller._cursorRect,
  45. padding: widget.padding,
  46. cursorMargin: widget.cursorMargin,
  47. child: widget.overlayBuilder(context),
  48. );
  49. },
  50. child: widget.child,
  51. );
  52. }
  53. }
  54. /// A widget that places [child] around [cursorRect].
  55. class SuggestionLayout extends SingleChildRenderObjectWidget {
  56. SuggestionLayout({
  57. super.child,
  58. required this.cursorRect,
  59. required this.padding,
  60. required this.cursorMargin,
  61. });
  62. /// The location of the cursor relative to the top left corner of this widget.
  63. final ValueListenable<Rect> cursorRect;
  64. /// The minimum space between [child] and the edge of this widget.
  65. final EdgeInsets padding;
  66. /// The minimum space between [child] and the cursor. Currently, only top and
  67. /// bottom are used.
  68. final EdgeInsets cursorMargin;
  69. @override
  70. RenderObject createRenderObject(BuildContext context) {
  71. return RenderCompletionLayout(
  72. null,
  73. cursorRect: cursorRect,
  74. padding: padding,
  75. cursorMargin: cursorMargin,
  76. );
  77. }
  78. @override
  79. void updateRenderObject(
  80. BuildContext context,
  81. covariant RenderCompletionLayout renderObject,
  82. ) {
  83. renderObject.cursorRect = cursorRect;
  84. renderObject.padding = padding;
  85. renderObject.cursorMargin = cursorMargin;
  86. }
  87. }
  88. class RenderCompletionLayout extends RenderShiftedBox {
  89. RenderCompletionLayout(
  90. super.child, {
  91. required ValueListenable<Rect> cursorRect,
  92. required EdgeInsets padding,
  93. required EdgeInsets cursorMargin,
  94. }) : _cursorRect = cursorRect,
  95. _padding = padding,
  96. _cursorPadding = cursorMargin;
  97. ValueListenable<Rect> _cursorRect;
  98. ValueListenable<Rect> get cursorRect => _cursorRect;
  99. set cursorRect(ValueListenable<Rect> value) {
  100. if (_cursorRect == value) return;
  101. _cursorRect.removeListener(markNeedsLayout);
  102. _cursorRect = value;
  103. _cursorRect.addListener(markNeedsLayout);
  104. markNeedsLayout();
  105. }
  106. EdgeInsets _padding;
  107. EdgeInsets get padding => _padding;
  108. set padding(EdgeInsets value) {
  109. if (_padding == value) return;
  110. _padding = value;
  111. markNeedsLayout();
  112. }
  113. EdgeInsets _cursorPadding;
  114. EdgeInsets get cursorMargin => _cursorPadding;
  115. set cursorMargin(EdgeInsets value) {
  116. if (_cursorPadding == value) return;
  117. _cursorPadding = value;
  118. markNeedsLayout();
  119. }
  120. @override
  121. void attach(covariant PipelineOwner owner) {
  122. cursorRect.addListener(markNeedsLayout);
  123. super.attach(owner);
  124. }
  125. @override
  126. void detach() {
  127. cursorRect.removeListener(markNeedsLayout);
  128. super.detach();
  129. }
  130. @override
  131. void performLayout() {
  132. final child = this.child;
  133. if (child == null) {
  134. size = constraints.smallest;
  135. return;
  136. }
  137. size = constraints.biggest;
  138. // space available for the completion overlay above the cursor
  139. final spaceAbove = cursorRect.value.top - padding.top - cursorMargin.top;
  140. // space available for the completion overlay below the cursor
  141. final spaceBelow = size.height -
  142. cursorRect.value.bottom -
  143. padding.bottom -
  144. cursorMargin.bottom;
  145. final childConstraints = BoxConstraints(
  146. minWidth: 0,
  147. maxWidth: size.width - padding.horizontal,
  148. minHeight: 0,
  149. maxHeight: max(spaceAbove, spaceBelow),
  150. );
  151. child.layout(childConstraints, parentUsesSize: true);
  152. // Whether the completion overlay can be placed above the cursor.
  153. final fitsBelow = spaceBelow >= child.size.height;
  154. final childParentData = child.parentData as BoxParentData;
  155. childParentData.offset = Offset(
  156. min(
  157. size.width - padding.right - child.size.width,
  158. cursorRect.value.left,
  159. ),
  160. // Showing the completion overlay below the cursor is preferred, unless
  161. // there's insufficient space for it.
  162. fitsBelow
  163. ? cursorRect.value.bottom + cursorMargin.bottom
  164. : cursorRect.value.top - cursorMargin.top - child.size.height,
  165. );
  166. }
  167. }