Skip to content

Commit fab56fb

Browse files
authored
feat: Control and listen to selection/caret changes in TextField (#5725)
* initial commit * support `CupertinoTextField` * Fix #2916: Downsizing `Stack` has no animation * enable `show_labels` in mkdocs configuration * improve documentation * update documentation for clarity and focus requirement * refactor: simplify animation handling in sized control * improve file picker docs * fix docs
1 parent 75402f7 commit fab56fb

File tree

16 files changed

+432
-150
lines changed

16 files changed

+432
-150
lines changed

packages/flet/lib/src/controls/base_controls.dart

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,7 @@ Widget _positionedControl(
282282
top: top,
283283
right: right,
284284
bottom: bottom,
285-
onEnd: control.getBool("on_animation_end", false)!
286-
? () {
287-
control.triggerEvent("animation_end", "position");
288-
}
289-
: null,
285+
onEnd: () => control.triggerEvent("animation_end", "position"),
290286
child: widget,
291287
);
292288
} else if (left != null || top != null || right != null || bottom != null) {
@@ -305,25 +301,34 @@ Widget _positionedControl(
305301
}
306302

307303
Widget _sizedControl(Widget widget, Control control) {
308-
final skipProps = control.internals?["skip_properties"] as List?;
309-
if (skipProps?.contains("width") == true ||
310-
skipProps?.contains("height") == true) {
304+
final skipProps = control.internals?['skip_properties'] as List?;
305+
if (skipProps != null && ['width', 'height'].any(skipProps.contains)) {
311306
return widget;
312307
}
313308

314-
var width = control.getDouble("width");
315-
var height = control.getDouble("height");
309+
final width = control.getDouble("width");
310+
final height = control.getDouble("height");
311+
final animationSize = control.getAnimation("animate_size");
316312

317-
if ((width != null || height != null)) {
318-
widget = ConstrainedBox(
319-
constraints: BoxConstraints.tightFor(width: width, height: height),
320-
child: widget,
321-
);
322-
}
323-
var animation = control.getAnimation("animate_size");
324-
if (animation != null) {
325-
return AnimatedSize(
326-
duration: animation.duration, curve: animation.curve, child: widget);
313+
final hasFixedSize = width != null || height != null;
314+
315+
if (animationSize != null) {
316+
return hasFixedSize
317+
? AnimatedContainer(
318+
duration: animationSize.duration,
319+
curve: animationSize.curve,
320+
width: width,
321+
height: height,
322+
child: widget,
323+
)
324+
: AnimatedSize(
325+
duration: animationSize.duration,
326+
curve: animationSize.curve,
327+
child: widget,
328+
);
329+
} else {
330+
return hasFixedSize
331+
? SizedBox(width: width, height: height, child: widget)
332+
: widget;
327333
}
328-
return widget;
329334
}

packages/flet/lib/src/controls/cupertino_textfield.dart

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> {
4141
late final FocusNode _shiftEnterfocusNode;
4242
String? _lastFocusValue;
4343
String? _lastBlurValue;
44+
TextSelection? _selection;
4445

4546
@override
4647
void initState() {
4748
super.initState();
4849
_controller = TextEditingController();
50+
_controller.addListener(_handleControllerChange);
4951
_shiftEnterfocusNode = FocusNode(
5052
onKeyEvent: (FocusNode node, KeyEvent evt) {
5153
if (!HardwareKeyboard.instance.isShiftPressed &&
@@ -67,6 +69,7 @@ class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> {
6769

6870
@override
6971
void dispose() {
72+
_controller.removeListener(_handleControllerChange);
7073
_controller.dispose();
7174
_shiftEnterfocusNode.removeListener(_onShiftEnterFocusChange);
7275
_shiftEnterfocusNode.dispose();
@@ -101,6 +104,25 @@ class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> {
101104
widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur");
102105
}
103106

107+
void _handleControllerChange() {
108+
final selection = _controller.selection;
109+
if (_selection == selection) return;
110+
111+
_selection = selection;
112+
113+
if (!selection.isValid ||
114+
!widget.control.getBool("on_selection_change", false)!) {
115+
return;
116+
}
117+
118+
widget.control.updateProperties({"selection": selection.toMap()});
119+
widget.control.triggerEvent("selection_change", {
120+
"selected_text":
121+
_controller.text.substring(selection.start, selection.end),
122+
"selection": selection.toMap()
123+
});
124+
}
125+
104126
@override
105127
Widget build(BuildContext context) {
106128
debugPrint("CupertinoTextField build: ${widget.control.id}");
@@ -109,7 +131,12 @@ class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> {
109131
var value = widget.control.getString("value", "")!;
110132
if (_value != value) {
111133
_value = value;
112-
_controller.text = value;
134+
_controller.value = TextEditingValue(
135+
text: value,
136+
// preserve cursor position at the end
137+
selection: TextSelection.collapsed(offset: value.length),
138+
);
139+
_selection = _controller.selection;
113140
}
114141

115142
var shiftEnter = widget.control.getBool("shift_enter", false)!;
@@ -165,6 +192,13 @@ class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> {
165192
_focusNode.unfocus();
166193
}
167194

195+
var selection = widget.control.getTextSelection("selection",
196+
minOffset: 0, maxOffset: _controller.text.length);
197+
if (selection != null && selection != _controller.selection) {
198+
_controller.selection = selection;
199+
_selection = selection;
200+
}
201+
168202
var borderRadius = widget.control.getBorderRadius("border_radius");
169203

170204
BoxBorder? border;

packages/flet/lib/src/controls/text.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,12 @@ class TextControl extends StatelessWidget {
8282

8383
TextAlign textAlign =
8484
parseTextAlign(control.getString("text_align"), TextAlign.start)!;
85-
8685
TextOverflow overflow =
8786
parseTextOverflow(control.getString("overflow"), TextOverflow.clip)!;
8887

8988
onSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
9089
control.triggerEvent("selection_change", {
91-
"text": text,
90+
"selected_text": text,
9291
"cause": cause?.name ?? "unknown",
9392
"selection": selection.toMap(),
9493
});

packages/flet/lib/src/controls/textfield.dart

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ class _TextFieldControlState extends State<TextFieldControl> {
3636
late final FocusNode _shiftEnterfocusNode;
3737
String? _lastFocusValue;
3838
String? _lastBlurValue;
39+
TextSelection? _selection;
3940

4041
@override
4142
void initState() {
4243
super.initState();
4344
_controller = TextEditingController();
45+
_controller.addListener(_handleControllerChange);
4446
_shiftEnterfocusNode = FocusNode(
4547
onKeyEvent: (FocusNode node, KeyEvent evt) {
4648
if (!HardwareKeyboard.instance.isShiftPressed &&
@@ -62,6 +64,7 @@ class _TextFieldControlState extends State<TextFieldControl> {
6264

6365
@override
6466
void dispose() {
67+
_controller.removeListener(_handleControllerChange);
6568
_controller.dispose();
6669
_shiftEnterfocusNode.removeListener(_onShiftEnterFocusChange);
6770
_shiftEnterfocusNode.dispose();
@@ -92,6 +95,25 @@ class _TextFieldControlState extends State<TextFieldControl> {
9295
widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur");
9396
}
9497

98+
void _handleControllerChange() {
99+
final selection = _controller.selection;
100+
if (_selection == selection) return;
101+
102+
_selection = selection;
103+
104+
if (!selection.isValid ||
105+
!widget.control.getBool("on_selection_change", false)!) {
106+
return;
107+
}
108+
109+
widget.control.updateProperties({"selection": selection.toMap()});
110+
widget.control.triggerEvent("selection_change", {
111+
"selected_text":
112+
_controller.text.substring(selection.start, selection.end),
113+
"selection": selection.toMap()
114+
});
115+
}
116+
95117
@override
96118
Widget build(BuildContext context) {
97119
debugPrint("TextField build: ${widget.control.id}");
@@ -103,9 +125,17 @@ class _TextFieldControlState extends State<TextFieldControl> {
103125
_value = value;
104126
_controller.value = TextEditingValue(
105127
text: value,
106-
selection: TextSelection.collapsed(
107-
offset: value.length), // preserve cursor position at the end
128+
// preserve cursor position at the end
129+
selection: TextSelection.collapsed(offset: value.length),
108130
);
131+
_selection = _controller.selection;
132+
}
133+
134+
var selection = widget.control.getTextSelection("selection",
135+
minOffset: 0, maxOffset: _controller.text.length);
136+
if (selection != null && selection != _controller.selection) {
137+
_controller.selection = selection;
138+
_selection = selection;
109139
}
110140

111141
var shiftEnter = widget.control.getBool("shift_enter", false)!;

0 commit comments

Comments
 (0)