Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a feature to allow WidgetSpan in annotation #70

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

chan150
Copy link

@chan150 chan150 commented Aug 6, 2024

  1. To add a support of WidgetSpan, I replace Annotation's input type from TextSpan to InlineSpan.
  2. Remove a TextSpan checker as called as _isTextSpan.
  3. When performing Layout and measure text, WidgetSpan may be skipped .

@chan150
Copy link
Author

chan150 commented Aug 6, 2024

It should be related with #25

@maRci002
Copy link
Collaborator

maRci002 commented Aug 6, 2024

What's the point of removing WidgetSpan during layout? In this scenario, the package won't function as expected:

  1. When trimMode is set to TrimMode.Line and trimLines is set to 3, if there are 3 lines of text followed by a WidgetSpan on the 4th line, the measurement will not detect any text to trim.
  2. When trimMode is set to TrimMode.Line and trimLines is set to 3, if there are 3 lines of WidgetSpan followed by 2 lines of text, the measurement will not detect any text to trim.

@chan150
Copy link
Author

chan150 commented Aug 7, 2024

What's the point of removing WidgetSpan during layout? In this scenario, the package won't function as expected:

  1. When trimMode is set to TrimMode.Line and trimLines is set to 3, if there are 3 lines of text followed by a WidgetSpan on the 4th line, the measurement will not detect any text to trim.
  2. When trimMode is set to TrimMode.Line and trimLines is set to 3, if there are 3 lines of WidgetSpan followed by 2 lines of text, the measurement will not detect any text to trim.

Hi @maRci002,

It is a scenario to be useful of the added feature.
We can attach overlayable widget inside readmore widget.

I understand that removing just WidgetSpan is too naive and there is no way to estimate the size of widget before rendering.

ReadMoreText(
  '@[{"key": {"inner":"test"}}] and @[{"key":"value"}]',
  annotations: [
    Annotation(
      regExp: RegExp('@\\[(.+?)\\]'),
      spanBuilder: ({required text, required textStyle}) {
        return WidgetSpan(
          child: Tooltip(
            message: 'It is just tooltip',
            child: Text(
              'It is a widget',
              style: TextStyle(height: 0),
            ),
          ),
        );
      },
    ),
  ],
  style: TextStyle(color: Colors.red),
)

@maRci002
Copy link
Collaborator

maRci002 commented Aug 8, 2024

I understand that removing just WidgetSpan is too naive and there is no way to estimate the size of widget before rendering.

I think instead of removing WidgetSpan during the layout phase, we should provide or calculate the WidgetSpan size.

Note: The effectiveTextStyle is already provided to Annotation.spanBuilder, and we could offer a simple function that takes a TextSpan (without any WidgetSpan children) and returns its size. For example, this works when WidgetSpan includes a ToolTip widget, but users can also return a hardcoded size. Therefore, the spanBuilder could return both InlineSpan and possible PlaceholderDimensions.

Here is an example of how PlaceholderDimensions can be used in the case of WidgetSpan. However, the trimming logic should also be updated, which results in a very difficult API.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('WidgetSpan Test'),
        ),
        body: const Center(
          child: TextPainterButton(),
        ),
      ),
    );
  }
}

class TextPainterButton extends StatelessWidget {
  const TextPainterButton({super.key});

  void _printTextLayout(BuildContext context) {
    final defaultTextStyle = DefaultTextStyle.of(context);
    var effectiveTextStyle = defaultTextStyle.style;
    if (MediaQuery.boldTextOf(context)) {
      effectiveTextStyle = effectiveTextStyle.merge(const TextStyle(fontWeight: FontWeight.bold));
    }

    final textScaler = MediaQuery.textScalerOf(context);
    final textAlign = defaultTextStyle.textAlign ?? TextAlign.start;
    final textDirection = Directionality.of(context);
    final locale = Localizations.maybeLocaleOf(context);
    final textWidthBasis = defaultTextStyle.textWidthBasis;
    final textHeightBehavior = defaultTextStyle.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context);

    final textPainter = TextPainter(
      textAlign: textAlign,
      textDirection: textDirection,
      locale: locale,
      textScaler: textScaler,
      maxLines: 1,
      textWidthBasis: textWidthBasis,
      textHeightBehavior: textHeightBehavior,
    );

    const txt = 'It is a widget';
    const widgetSpan = WidgetSpan(
      child: Tooltip(
        message: 'It is just tooltip',
        child: Text(txt),
      ),
    );

    final widgetSpanText = TextSpan(
      text: txt,
      style: effectiveTextStyle,
    );

    textPainter.text = widgetSpanText;
    textPainter.layout();

    final widgetSpanSize = textPainter.size;

    final widgetSpanPlaceHolderDimension = PlaceholderDimensions(
      size: widgetSpanSize,
      alignment: widgetSpan.alignment,
      baseline: widgetSpan.baseline,
    );

    final span = TextSpan(
      style: effectiveTextStyle.merge(const TextStyle(color: Colors.black, fontSize: 18)),
      text: 'Hello, TextPainter!',
      children: const [widgetSpan],
    );

    textPainter.text = span;
    textPainter.setPlaceholderDimensions([widgetSpanPlaceHolderDimension]);
    textPainter.layout();

    print('Text size: ${textPainter.size}');
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => _printTextLayout(context),
      child: const Text('Layout Text'),
    );
  }
}

@chan150
Copy link
Author

chan150 commented Aug 19, 2024

I think instead of removing WidgetSpan during the layout phase, we should provide or calculate the WidgetSpan size.

I provide a widget size estimator.
It will be helpful for above idea.

Size? _calculateWidgetSize(Widget widget) {
    final child = InheritedTheme.captureAll(
      context,
      MediaQuery(
        data: MediaQuery.of(context),
        child: Material(
          color: Colors.transparent,
          child: widget,
        ),
      ),
    );

    final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();
    final platformDispatcher = WidgetsBinding.instance.platformDispatcher;
    final fallBackView = platformDispatcher.views.first;
    final view = View.maybeOf(context) ?? fallBackView;
    Size logicalSize = view.physicalSize / view.devicePixelRatio; // Adapted

    final RenderView renderView = RenderView(
      view: view,
      child: RenderPositionedBox(
          alignment: Alignment.center, child: repaintBoundary),
      configuration: ViewConfiguration(
        // size: logicalSize,
        logicalConstraints: BoxConstraints(
          maxWidth: logicalSize.width,
          maxHeight: logicalSize.height,
        ),
        devicePixelRatio: 1.0,
      ),
    );

    final PipelineOwner pipelineOwner = PipelineOwner();
    final BuildOwner buildOwner =
        BuildOwner(focusManager: FocusManager(), onBuildScheduled: () {});

    pipelineOwner.rootNode = renderView;
    renderView.prepareInitialFrame();

    final RenderObjectToWidgetElement<RenderBox> rootElement =
        RenderObjectToWidgetAdapter<RenderBox>(
            container: repaintBoundary,
            child: Directionality(
              textDirection: TextDirection.ltr,
              child: child,
            )).attachToRenderTree(
      buildOwner,
    );

    buildOwner.buildScope(
      rootElement,
    );
    buildOwner.finalizeTree();

    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();

    return rootElement.size;
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants