| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- import 'dart:math';
- import 'package:flutter/foundation.dart';
- import 'package:flutter/rendering.dart';
- import 'package:flutter/widgets.dart';
- /// Controls the location of the suggestion popup of [SuggestionPortal].
- class SuggestionPortalController extends OverlayPortalController {
- final _cursorRect = ValueNotifier<Rect>(Rect.zero);
- /// Updates the location of the suggestion popup to [rect]. If the popup is
- /// not showing, it will be shown after this call.
- void update(Rect rect) {
- _cursorRect.value = rect;
- if (!isShowing) show();
- }
- }
- /// A convenience widget to place a suggestion popup around the cursor specified
- /// by [SuggestionPortalController].
- class SuggestionPortal extends StatefulWidget {
- const SuggestionPortal({
- super.key,
- required this.controller,
- required this.overlayBuilder,
- required this.child,
- this.padding = const EdgeInsets.all(8),
- this.cursorMargin = const EdgeInsets.all(4),
- });
- final SuggestionPortalController controller;
- final WidgetBuilder overlayBuilder;
- /// The minimum space between [child] and the screen edge.
- final EdgeInsets padding;
- /// The minimum space between [child] and the cursor. Currently, only top and
- /// bottom are used.
- final EdgeInsets cursorMargin;
- final Widget child;
- @override
- State<SuggestionPortal> createState() => _SuggestionPortalState();
- }
- class _SuggestionPortalState extends State<SuggestionPortal> {
- @override
- Widget build(BuildContext context) {
- return OverlayPortal.targetsRootOverlay(
- controller: widget.controller,
- overlayChildBuilder: (context) {
- return SuggestionLayout(
- cursorRect: widget.controller._cursorRect,
- padding: widget.padding,
- cursorMargin: widget.cursorMargin,
- child: widget.overlayBuilder(context),
- );
- },
- child: widget.child,
- );
- }
- }
- /// A widget that places [child] around [cursorRect].
- class SuggestionLayout extends SingleChildRenderObjectWidget {
- SuggestionLayout({
- super.child,
- required this.cursorRect,
- required this.padding,
- required this.cursorMargin,
- });
- /// The location of the cursor relative to the top left corner of this widget.
- final ValueListenable<Rect> cursorRect;
- /// The minimum space between [child] and the edge of this widget.
- final EdgeInsets padding;
- /// The minimum space between [child] and the cursor. Currently, only top and
- /// bottom are used.
- final EdgeInsets cursorMargin;
- @override
- RenderObject createRenderObject(BuildContext context) {
- return RenderCompletionLayout(
- null,
- cursorRect: cursorRect,
- padding: padding,
- cursorMargin: cursorMargin,
- );
- }
- @override
- void updateRenderObject(
- BuildContext context,
- covariant RenderCompletionLayout renderObject,
- ) {
- renderObject.cursorRect = cursorRect;
- renderObject.padding = padding;
- renderObject.cursorMargin = cursorMargin;
- }
- }
- class RenderCompletionLayout extends RenderShiftedBox {
- RenderCompletionLayout(
- super.child, {
- required ValueListenable<Rect> cursorRect,
- required EdgeInsets padding,
- required EdgeInsets cursorMargin,
- }) : _cursorRect = cursorRect,
- _padding = padding,
- _cursorPadding = cursorMargin;
- ValueListenable<Rect> _cursorRect;
- ValueListenable<Rect> get cursorRect => _cursorRect;
- set cursorRect(ValueListenable<Rect> value) {
- if (_cursorRect == value) return;
- _cursorRect.removeListener(markNeedsLayout);
- _cursorRect = value;
- _cursorRect.addListener(markNeedsLayout);
- markNeedsLayout();
- }
- EdgeInsets _padding;
- EdgeInsets get padding => _padding;
- set padding(EdgeInsets value) {
- if (_padding == value) return;
- _padding = value;
- markNeedsLayout();
- }
- EdgeInsets _cursorPadding;
- EdgeInsets get cursorMargin => _cursorPadding;
- set cursorMargin(EdgeInsets value) {
- if (_cursorPadding == value) return;
- _cursorPadding = value;
- markNeedsLayout();
- }
- @override
- void attach(covariant PipelineOwner owner) {
- cursorRect.addListener(markNeedsLayout);
- super.attach(owner);
- }
- @override
- void detach() {
- cursorRect.removeListener(markNeedsLayout);
- super.detach();
- }
- @override
- void performLayout() {
- final child = this.child;
- if (child == null) {
- size = constraints.smallest;
- return;
- }
- size = constraints.biggest;
- // space available for the completion overlay above the cursor
- final spaceAbove = cursorRect.value.top - padding.top - cursorMargin.top;
- // space available for the completion overlay below the cursor
- final spaceBelow = size.height -
- cursorRect.value.bottom -
- padding.bottom -
- cursorMargin.bottom;
- final childConstraints = BoxConstraints(
- minWidth: 0,
- maxWidth: size.width - padding.horizontal,
- minHeight: 0,
- maxHeight: max(spaceAbove, spaceBelow),
- );
- child.layout(childConstraints, parentUsesSize: true);
- // Whether the completion overlay can be placed above the cursor.
- final fitsBelow = spaceBelow >= child.size.height;
- final childParentData = child.parentData as BoxParentData;
- childParentData.offset = Offset(
- min(
- size.width - padding.right - child.size.width,
- cursorRect.value.left,
- ),
- // Showing the completion overlay below the cursor is preferred, unless
- // there's insufficient space for it.
- fitsBelow
- ? cursorRect.value.bottom + cursorMargin.bottom
- : cursorRect.value.top - cursorMargin.top - child.size.height,
- );
- }
- }
|