Skip to content

Commit c1c3c04

Browse files
committed
Update
1 parent a296a3e commit c1c3c04

File tree

4 files changed

+474
-145
lines changed

4 files changed

+474
-145
lines changed

lib/src/terminal_view.dart

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class TerminalView extends StatefulWidget {
4545
this.alwaysShowCursor = false,
4646
this.deleteDetection = false,
4747
this.shortcuts,
48+
this.onKey,
4849
this.readOnly = false,
4950
this.hardwareKeyboardOnly = false,
5051
this.simulateScroll = true,
@@ -126,6 +127,10 @@ class TerminalView extends StatefulWidget {
126127
/// of the terminal If not provided, [defaultTerminalShortcuts] will be used.
127128
final Map<ShortcutActivator, Intent>? shortcuts;
128129

130+
/// Keyboard event handler of the terminal. This has higher priority than
131+
/// [shortcuts] and input handler of the terminal.
132+
final FocusOnKeyCallback? onKey;
133+
129134
/// True if no input should send to the terminal.
130135
final bool readOnly;
131136

@@ -268,7 +273,7 @@ class TerminalViewState extends State<TerminalView> {
268273
widget.terminal.keyInput(TerminalKey.enter);
269274
}
270275
},
271-
onKey: _onKeyEvent,
276+
onKey: _handleKeyEvent,
272277
readOnly: widget.readOnly,
273278
child: child,
274279
);
@@ -280,7 +285,7 @@ class TerminalViewState extends State<TerminalView> {
280285
autofocus: widget.autofocus,
281286
onInsert: _onInsert,
282287
onComposing: _onComposing,
283-
onKey: _onKeyEvent,
288+
onKey: _handleKeyEvent,
284289
);
285290
}
286291

@@ -388,7 +393,12 @@ class TerminalViewState extends State<TerminalView> {
388393
setState(() => _composingText = text);
389394
}
390395

391-
KeyEventResult _onKeyEvent(FocusNode focusNode, RawKeyEvent event) {
396+
KeyEventResult _handleKeyEvent(FocusNode focusNode, RawKeyEvent event) {
397+
final resultOverride = widget.onKey?.call(focusNode, event);
398+
if (resultOverride != null && resultOverride != KeyEventResult.ignored) {
399+
return resultOverride;
400+
}
401+
392402
// ignore: invalid_use_of_protected_member
393403
final shortcutResult = _shortcutManager.handleKeypress(
394404
focusNode.context!,

lib/suggestion.dart

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

0 commit comments

Comments
 (0)