Show tooltip when TextSpan is hovered over

555

I didn't find a direct way to do this.

But here is a possible solution using Hover on the whole RichText and then identifying which TextSpan is the target, and whether or not it has a tooltip.

enter image description here

Though, it's not an easy ride. Buckle up!

I tried to keep my application as simple as possible:

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'TextSpan Hover Demo',
      home: HomePage(),
    ),
  );
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _textKey = GlobalKey();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        padding: EdgeInsets.all(16.0),
        child: RichTextTooltipDetector(
          textKey: _textKey,
          child: RichText(
            key: _textKey,
            text: TextSpan(
              text: 'Hello ',
              children: <TextSpan>[
                TextSpan(
                  text: 'bold',
                  style: TextStyle(fontWeight: FontWeight.bold),
                  semanticsLabel: 'Tooltip: Yeah! It works',
                ),
                TextSpan(text: ' world!'),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

This RichTextTooltipDetector is where I handle the Tooltips as OverlayEntries.

My RichTextTooltipDetector is just a MouseRegion on which I will handle the hover events. This event gives me the local position of the mouse. This position, together with the RichText GlobalKey, is all we need to identify whether we have a tooltip to show or not:

  1. From the RichText Global Key, I get the RenderParagraph
  2. From this RenderParagraph and the localPositionof the PointerHoverEvent, I get the `InlineSpan, if any
  3. When I defined the TextSpan, I highjacked the semanticsLabel. This let me know easily if I need to display a Tooltip or not.

The rest is just basic OverlayEntry management:

  1. Creating an OverlayEntry based on the BuildContext, an offset, and the Tooltip text
  2. Displaying the OverlayEntry with Overlay.of(context).insert(_tooltipOverlay);
  3. After 1 second, hiding the OverlayEntry with _tooltipOverlay?.remove();
import 'dart:async';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class _RichTextTooltipDetectorState extends State<RichTextTooltipDetector> {
  OverlayEntry _tooltipOverlay;
  Timer _timer;

  RenderParagraph get _renderParagraph =>
      widget.textKey.currentContext?.findRenderObject() as RenderParagraph;

  InlineSpan _span(PointerHoverEvent event, RenderParagraph paragraph) {
    final textPosition = paragraph.getPositionForOffset(event.localPosition);
    return paragraph.text.getSpanForPosition(textPosition);
  }

  String _tooltipText(PointerHoverEvent event, RenderParagraph paragraph) {
    final span = _span(event, paragraph);
    return span is TextSpan &&
            span.semanticsLabel != null &&
            span.semanticsLabel.startsWith('Tooltip: ')
        ? span.semanticsLabel.split('Tooltip: ')[1]
        : '';
  }

  OverlayEntry _createTooltip(
      BuildContext context, String text, Offset offset) {
    return OverlayEntry(
      builder: (context) => Positioned(
        left: offset.dx,
        top: offset.dy,
        child: Material(
          elevation: 4.0,
          child: Container(
            decoration: BoxDecoration(
              color: Colors.white70,
              border: Border.all(color: Colors.black87, width: 3.0),
            ),
            padding: EdgeInsets.all(8.0),
            child: Text(text),
          ),
        ),
      ),
    );
  }

  void _showTooltip() {
    Overlay.of(context).insert(_tooltipOverlay);
    _timer = Timer(Duration(seconds: 1), () => _hideTooltip());
  }

  void _hideTooltip() {
    _timer?.cancel();
    _tooltipOverlay?.remove();
    _tooltipOverlay = null;
    _timer = null;
  }

  void _handleHover(BuildContext context, PointerHoverEvent event) {
    _hideTooltip();
    final paragraph = _renderParagraph;
    if (event is! PointerHoverEvent || paragraph == null) return;
    final tooltipText = _tooltipText(event, paragraph);
    if (tooltipText.isNotEmpty) {
      RenderBox renderBox = context.findRenderObject();
      var offset = renderBox.localToGlobal(event.localPosition);
      _tooltipOverlay = _createTooltip(context, tooltipText, offset);
      _showTooltip();
    }
  }

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onHover: (event) => _handleHover(context, event),
      child: widget.child,
    );
  }
}
Share:
555
Camelid
Author by

Camelid

Updated on December 28, 2022

Comments

  • Camelid
    Camelid over 1 year

    How do I show a tooltip when a particular TextSpan is hovered over? I found Flutter: How to display Tooltip for TextSpan inside RichText but that is for when a TextSpan is tapped on, not hovered over with the mouse, and it uses a SnackBar, whereas I want a tooltip.

    Unfortunately I cannot just wrap the TextSpan in a ToolTip because my use case is in an override of TextEditingController.buildTextSpan where I'm returning a TextSpan(children: childSpans), which means I have to use a subclass of TextSpan.

  • Camelid
    Camelid about 3 years
    Thank you for the very detailed response! However, my specific use case is for showing a tooltip when users hover up different spans of text in a TextField. I tried several different ways to adapt your approach for my specific use case, but because I am new to Flutter I was unable to get it to work. How should I adapt your approach for a TextField? Thanks again!
  • Thierry
    Thierry about 3 years
    Maybe you could update your question with a Minimal Code Sample. It always attracts more targeted answers.