Darya
Darya

Reputation: 21

How to update text selection dynamically in SelectableText widget in Flutter?

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:

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

Answers (1)

Soufiane Bamou
Soufiane Bamou

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

Related Questions