Skip to content

Commit 1ece492

Browse files
authored
Expression evaluation autocomplete overlay is positioned over the last . in the expression (#3449)
1 parent 9fb6a25 commit 1ece492

File tree

2 files changed

+34
-19
lines changed

2 files changed

+34
-19
lines changed

packages/devtools_app/lib/src/debugger/evaluate.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import '../globals.dart';
1313
import '../notifications.dart';
1414
import '../theme.dart';
1515
import '../ui/search.dart';
16+
import '../ui/utils.dart';
1617
import '../utils.dart';
1718
import 'debugger_controller.dart';
1819

@@ -135,14 +136,28 @@ class _ExpressionEvalFieldState extends State<ExpressionEvalField>
135136
shouldRequestFocus: false,
136137
supportClearField: true,
137138
onSelection: _onSelection,
138-
tracking: true,
139139
decoration: const InputDecoration(
140140
contentPadding: EdgeInsets.all(denseSpacing),
141141
border: OutlineInputBorder(),
142142
focusedBorder: OutlineInputBorder(borderSide: BorderSide.none),
143143
enabledBorder: OutlineInputBorder(borderSide: BorderSide.none),
144144
labelText: 'Eval',
145145
),
146+
overlayXPositionBuilder:
147+
(String inputValue, TextStyle inputStyle) {
148+
// X-coordinate is equivalent to the width of the input text
149+
// up to the last "." or the insertion point (cursor):
150+
final indexOfDot = inputValue.lastIndexOf('.');
151+
final textSegment = indexOfDot != -1
152+
? inputValue.substring(0, indexOfDot + 1)
153+
: inputValue;
154+
return calculateTextSpanWidth(
155+
TextSpan(
156+
text: textSegment,
157+
style: inputStyle,
158+
),
159+
);
160+
},
146161
),
147162
),
148163
),

packages/devtools_app/lib/src/ui/search.dart

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,13 @@ typedef ClearSearchField = Function(
556556
bool force,
557557
});
558558

559+
/// Provided by clients to specify where the autocomplete overlay should be
560+
/// positioned relative to the input text.
561+
typedef OverlayXPositionBuilder = double Function(
562+
String inputValue,
563+
TextStyle inputStyle,
564+
);
565+
559566
mixin SearchFieldMixin<T extends StatefulWidget> on State<T> {
560567
TextEditingController searchTextFieldController;
561568
FocusNode _searchFieldFocusNode;
@@ -601,9 +608,10 @@ mixin SearchFieldMixin<T extends StatefulWidget> on State<T> {
601608
/// [searchFieldKey]
602609
/// [searchFieldEnabled]
603610
/// [onSelection]
604-
/// [onHilightDropdown] use to override default highlghter.
611+
/// [onHighlightDropdown] use to override default highlghter.
605612
/// [decoration]
606-
/// [tracking] if true displays pop-up to the right of the TextField's caret.
613+
/// [overlayXPositionBuilder] callback function to determine where the
614+
/// autocomplete overlay should be positioned relative to the input text.
607615
/// [supportClearField] if true clear TextField content if pop-up not visible. If
608616
/// pop-up is visible close the pop-up on first ESCAPE.
609617
/// [keyEventsToPropogate] a set of key events that should be propogated to
@@ -617,7 +625,7 @@ mixin SearchFieldMixin<T extends StatefulWidget> on State<T> {
617625
HighlightAutoComplete onHighlightDropdown,
618626
InputDecoration decoration,
619627
String label,
620-
bool tracking = false,
628+
OverlayXPositionBuilder overlayXPositionBuilder,
621629
bool supportClearField = false,
622630
Set<LogicalKeyboardKey> keyEventsToPropogate = const {},
623631
VoidCallback onClose,
@@ -633,7 +641,7 @@ mixin SearchFieldMixin<T extends StatefulWidget> on State<T> {
633641
searchTextFieldController: searchTextFieldController,
634642
decoration: decoration,
635643
label: label,
636-
tracking: tracking,
644+
overlayXPositionBuilder: overlayXPositionBuilder,
637645
onClose: onClose,
638646
);
639647

@@ -718,6 +726,7 @@ class _SearchField extends StatelessWidget {
718726
this.tracking = false,
719727
this.decoration,
720728
this.onClose,
729+
this.overlayXPositionBuilder,
721730
});
722731

723732
final SearchControllerMixin controller;
@@ -731,30 +740,21 @@ class _SearchField extends StatelessWidget {
731740
final bool tracking;
732741
final InputDecoration decoration;
733742
final VoidCallback onClose;
743+
final OverlayXPositionBuilder overlayXPositionBuilder;
734744

735745
@override
736746
Widget build(BuildContext context) {
747+
final textStyle = Theme.of(context).textTheme.subtitle1;
737748
final searchField = TextField(
738749
key: searchFieldKey,
739750
autofocus: true,
740751
enabled: searchFieldEnabled,
741752
focusNode: searchFieldFocusNode,
742753
controller: searchTextFieldController,
754+
style: textStyle,
743755
onChanged: (value) {
744-
if (tracking) {
745-
// Use a TextPainter to calculate the width of the newly entered text.
746-
// TODO(terry): The TextPainter's TextStyle is default (same as this
747-
// TextField) consider explicitly using a TextStyle of
748-
// this TextField if the TextField needs styling.
749-
final painter = TextPainter(
750-
textDirection: TextDirection.ltr,
751-
text: TextSpan(text: value),
752-
);
753-
painter.layout();
754-
755-
// X coordinate of the pop-up, immediately to the right of the insertion
756-
// point (caret).
757-
controller.xPosition = painter.width;
756+
if (overlayXPositionBuilder != null) {
757+
controller.xPosition = overlayXPositionBuilder(value, textStyle);
758758
}
759759
controller.search = value;
760760
},

0 commit comments

Comments
 (0)