Reputation: 21
I am working on a Flutter app where I need to update the text selection in a SelectableText widget dynamically. Specifically, I want to highlight a portion of the text as the user scrolls with the mouse. Although I am able to update the TextSelection object when scrolling, the UI doesn't reflect the updated selection on the screen.
Here’s what I’ve tried:
I used a PointerScrollEvent to track the mouse scroll position.
I updated the TextSelection using setState.
I used a ValueNotifier to track and update the TextSelection value, which is passed to SelectableText.
However, the selection doesn’t update visually when scrolling, even though the TextSelection object is updated correctly.
Here’s the code I’ve implemented so far:
class _TextDescriptionWithOffset extends State<TextDescriptionWithOffset> {
GlobalKey _selectionWidgetKey = GlobalKey();
TextSelection _currentSelection = TextSelection.collapsed(offset: 0);
TextSpanGenerator? textSpanGenerator;
String? temp;
@override
void initState() {
super.initState();
textSpanGenerator = TextSpanGenerator(
widget.content,
widget.textObjects,
searchString: widget.searchString ?? '',
widget.textMarginWidth,
TextSpanGeneratorMode.originalText,
);
temp = textSpanGenerator!.plainText;
}
void _onPointerScroll(PointerScrollEvent event) {
final RenderBox renderBox = _selectionWidgetKey.currentContext!.findRenderObject() as RenderBox;
Offset globalPosition = renderBox.localToGlobal(event.localPosition);
TextPosition? textPosition = _getTextPosition(globalPosition);
if (textPosition != null) {
setState(() {
_currentSelection = TextSelection(
baseOffset: _currentSelection.baseOffset,
extentOffset: textPosition.offset,
);
});
}
}
TextPosition? _getTextPosition(Offset globalPosition) {
final RenderBox renderBox = _selectionWidgetKey.currentContext!.findRenderObject() as RenderBox;
final TextPainter textPainter = TextPainter(
text: TextSpan(text: temp, style: TextStyle(color: Colors.black)),
textDirection: TextDirection.ltr,
)..layout(maxWidth: renderBox.size.width);
return textPainter.getPositionForOffset(renderBox.globalToLocal(globalPosition));
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Focus(
child: MouseRegion(
onEnter: (_) => widget.onTextSelectionMode?.call(true),
onExit: (_) => widget.onTextSelectionMode?.call(false),
child: Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
_onPointerScroll(event);
}
},
child: SelectableText.rich(
TextSpan(
text: widget.content,
style: TextStyle(color: Colors.black),
),
key: _selectionWidgetKey,
strutStyle: StrutStyle.disabled,
style: TextStyle(color: Colors.black),
enableInteractiveSelection: widget.interactive,
cursorWidth: 0,
textScaler: TextScaler.linear(1.0),
textWidthBasis: TextWidthBasis.longestLine,
onSelectionChanged: (TextSelection newSelection, SelectionChangedCause? cause) {
setState(() {
_currentSelection = newSelection;
});
widget.onSelectionChanged?.call(newSelection.textInside(temp));
},
),
),
),
),
],
);
}
}
My question: How can I make the SelectableText widget update selections visually as the user scrolls?
Thank you!
Upvotes: 0
Views: 35
Reputation: 36
You can try this with StreamBuilder
import 'dart:async';
import 'package:flutter/material.dart';
class _TextDescriptionWithOffset extends State<TextDescriptionWithOffset> {
GlobalKey _selectionWidgetKey = GlobalKey();
TextSelection _currentSelection = TextSelection.collapsed(offset: 0);
TextSpanGenerator? textSpanGenerator;
String? temp;
final StreamController<TextSelection> _selectionStreamController = StreamController<TextSelection>.broadcast();
@override
void initState() {
super.initState();
textSpanGenerator = TextSpanGenerator(
widget.content,
widget.textObjects,
searchString: widget.searchString ?? '',
widget.textMarginWidth,
TextSpanGeneratorMode.originalText,
);
temp = textSpanGenerator!.plainText;
}
@override
void dispose() {
_selectionStreamController.close();
super.dispose();
}
void _onPointerScroll(PointerScrollEvent event) {
final RenderBox renderBox = _selectionWidgetKey.currentContext!.findRenderObject() as RenderBox;
Offset globalPosition = renderBox.localToGlobal(event.localPosition);
TextPosition? textPosition = _getTextPosition(globalPosition);
if (textPosition != null) {
_currentSelection = TextSelection(
baseOffset: _currentSelection.baseOffset,
extentOffset: textPosition.offset,
);
_selectionStreamController.add(_currentSelection); // Emit new selection
}
}
TextPosition? _getTextPosition(Offset globalPosition) {
final RenderBox renderBox = _selectionWidgetKey.currentContext!.findRenderObject() as RenderBox;
final TextPainter textPainter = TextPainter(
text: TextSpan(text: temp, style: TextStyle(color: Colors.black)),
textDirection: TextDirection.ltr,
)..layout(maxWidth: renderBox.size.width);
return textPainter.getPositionForOffset(renderBox.globalToLocal(globalPosition));
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Focus(
child: MouseRegion(
onEnter: (_) => widget.onTextSelectionMode?.call(true),
onExit: (_) => widget.onTextSelectionMode?.call(false),
child: Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
_onPointerScroll(event);
}
},
child: StreamBuilder<TextSelection>(
stream: _selectionStreamController.stream,
initialData: _currentSelection,
builder: (context, snapshot) {
return SelectableText.rich(
TextSpan(
text: widget.content,
style: TextStyle(color: Colors.black),
),
key: _selectionWidgetKey,
strutStyle: StrutStyle.disabled,
style: TextStyle(color: Colors.black),
enableInteractiveSelection: widget.interactive,
cursorWidth: 0,
textScaler: TextScaler.linear(1.0),
textWidthBasis: TextWidthBasis.longestLine,
onSelectionChanged: (TextSelection newSelection, SelectionChangedCause? cause) {
_currentSelection = newSelection;
_selectionStreamController.add(newSelection); // Emit new selection
widget.onSelectionChanged?.call(newSelection.textInside(temp));
},
);
},
),
),
),
),
],
);
}
}
Upvotes: 0