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

RangeError in RenderParagraph.assembleSemanticsNode #69787

Closed
goderbauer opened this issue Nov 4, 2020 · 18 comments
Closed

RangeError in RenderParagraph.assembleSemanticsNode #69787

goderbauer opened this issue Nov 4, 2020 · 18 comments

Comments

@goderbauer
Copy link
Member

@goderbauer goderbauer commented Nov 4, 2020

See also: b/172290978

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.ltr,
      child: Center(
        child: Container(
          color: Colors.green,
          child: RichText(
            text: TextSpan(
              children: [
                TextSpan(
                  text: 'Start\n',
                  recognizer: TapGestureRecognizer()..onTap = () { },
                ),
                WidgetSpan(
                  child: ExcludeSemantics(
                    excluding: true,
                    child: Icon(
                      Icons.edit,
                      size: 16,
                      semanticLabel: 'excluded',
                    ),
                  ),
                ),
                WidgetSpan(
                  child: ExcludeSemantics(
                    excluding: false,
                    child: Icon(
                      Icons.edit,
                      size: 16,
                      semanticLabel: 'included',
                    ),
                  ),
                ),
                TextSpan(text: 'End'),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Produces the following error:

I/flutter (23037): ══╡ EXCEPTION CAUGHT BY SCHEDULER LIBRARY ╞═════════════════════════════════════════════════════════
I/flutter (23037): The following RangeError was thrown during a scheduler callback:
I/flutter (23037): RangeError (index): Invalid value: Only valid value is 0: 1
I/flutter (23037): 
I/flutter (23037): When the exception was thrown, this was the stack:
I/flutter (23037): #0      List.[] (dart:core-patch/growable_array.dart:177:60)
I/flutter (23037): #1      List.elementAt (dart:core-patch/growable_array.dart:386:16)
I/flutter (23037): #2      RenderParagraph.assembleSemanticsNode (package:flutter/src/rendering/paragraph.dart:922:52)
I/flutter (23037): #3      _SwitchableSemanticsFragment.compileChildren (package:flutter/src/rendering/object.dart:3717:13)
I/flutter (23037): #4      _RootSemanticsFragment.compileChildren (package:flutter/src/rendering/object.dart:3587:16)
I/flutter (23037): #5      RenderObject._updateSemantics (package:flutter/src/rendering/object.dart:2631:25)
I/flutter (23037): #6      PipelineOwner.flushSemantics (package:flutter/src/rendering/object.dart:1081:16)
I/flutter (23037): #7      RendererBinding.drawFrame (package:flutter/src/rendering/binding.dart:459:21)
I/flutter (23037): #8      WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:895:13)
I/flutter (23037): #9      RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:320:5)
I/flutter (23037): #10     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1117:15)
I/flutter (23037): #11     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1055:9)
I/flutter (23037): #12     SchedulerBinding.scheduleWarmUpFrame.<anonymous closure> (package:flutter/src/scheduler/binding.dart:864:7)
I/flutter (23037): (elided 11 frames from class _RawReceivePortImpl, class _Timer, dart:async, and dart:async-patch)
I/flutter (23037): ════════════════════════════════════════════════════════════════════════════════════════════════════
@goderbauer
Copy link
Member Author

@goderbauer goderbauer commented Nov 4, 2020

@goderbauer
Copy link
Member Author

@goderbauer goderbauer commented Nov 4, 2020

Looks like RenderParagraph.assembleSemantics assumes that each placeholder produces a semantics node, which is not true in the example above thanks to the ExcludeSemantics widget.

@goderbauer
Copy link
Member Author

@goderbauer goderbauer commented Nov 4, 2020

/cc @chunhtai

@mehmetf
Copy link
Member

@mehmetf mehmetf commented Nov 4, 2020

I am also trying to reduce the customer:money test case since they don't seem to be using ExcludeSemantics.

@goderbauer
Copy link
Member Author

@goderbauer goderbauer commented Nov 4, 2020

@mehmetf The following code reproduces it without ExcludeSemantics. The trick is to have a widget in the WidgetSpan that naturally doesn't contribute any semantics (like an icon without a semantics label).

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.ltr,
      child: Center(
        child: Container(
          color: Colors.green,
          child: RichText(
            text: TextSpan(
              children: [
                TextSpan(
                  text: 'Start\n',
                  recognizer: TapGestureRecognizer()..onTap = () { },
                ),
                WidgetSpan(
                  child: Icon(
                    Icons.edit,
                    size: 16,
                  ),
                ),
                WidgetSpan(
                  child: Icon(
                    Icons.edit,
                    size: 16,
                    semanticLabel: 'foo',
                  ),
                ),
                TextSpan(text: 'End'),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
@chunhtai
Copy link
Contributor

@chunhtai chunhtai commented Nov 4, 2020

This turns out to be a harder problem than I think. There is no way to tell whether a render child of renderparagrah has a semantics node or not. same thing for semantics node to know which render object creates it. It may requires some structure change. I will see if i can come up with something.

@goderbauer
Copy link
Member Author

@goderbauer goderbauer commented Nov 4, 2020

As a work-around in an app you can wrap the child of the widgetspan that's causing the problem (that's the widget span that doesn't produce any semantics information) in an empty semantics container:

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.ltr,
      child: Center(
        child: Container(
          color: Colors.green,
          child: RichText(
            text: TextSpan(
              children: [
                TextSpan(
                  text: 'Start\n',
                  recognizer: TapGestureRecognizer()..onTap = () { },
                ),
                WidgetSpan(
                  child: Semantics(      // HERE
                    container: true,     // and HERE
                    child: Icon(
                      Icons.edit,
                      size: 16,
                    ),
                  ),
                ),
                WidgetSpan(
                  child: Icon(
                    Icons.edit,
                    size: 16,
                    semanticLabel: 'foo',
                  ),
                ),
                TextSpan(text: 'End'),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
@chunhtai
Copy link
Contributor

@chunhtai chunhtai commented Nov 4, 2020

The workaround will hit another corner case. We have another issue that we only apply textscalefactor to the top most semantics node of the children.

childNode.rect.width * parentData.scale!,

it should recursively apply the scaling down it children.

@mehmetf
Copy link
Member

@mehmetf mehmetf commented Nov 4, 2020

@goderbauer I see. Yes, they do have a few widget spans that do not provide semantics. The odd thing is that the issue reproduces only in specific configurations. For instance:

I am able to reproduce it via:

return Scaffold(
      appBar: AppBar(
        title: Text('Repro'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            HtmlView(content: detailsHtml),
            HtmlView(content: detailsHtml),
            HtmlView(content: detailsHtml),
            HtmlView(content: detailsHtml),
          ],
        ),
      ),
    );

however not when I reduce the number of widgets:

return Scaffold(
      appBar: AppBar(
        title: Text('Repro'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            HtmlView(content: detailsHtml),
            HtmlView(content: detailsHtml),
          ],
        ),
      ),
    );

If HtmlView(content: detailsHtml), is building WidgetSpans that do not produce semantic nodes, then should it not throw consistently?

HtmlView is a monster of a widget which is why I have been whittling down trying to get a good repro case for this.

@mehmetf
Copy link
Member

@mehmetf mehmetf commented Nov 4, 2020

This is my approximation of what they are doing but it fails to reproduce the problem:

https://gist.github.com/mehmetf/42d11f9bb4d709e7dc699106659af66b

I can see they extend WidgetSpan and provide, for instance, separators that do not have semantics assigned to them.

@goderbauer
Copy link
Member Author

@goderbauer goderbauer commented Nov 4, 2020

@mehmetf The fact that the number of HtmlViews is relevant for whether the issue is triggered or not is very strange. I don't have an explanation for that...

The gist you provided crashes for me with a different crash ("RenderBox did not set its size during layout"). Do you think it's related to this problem? It looks like something else to me.

To trigger the bug described in the issue here in your gist, try adding a TextSpan child with a TapGestureRecognizer()..onTap = () { } recognizer to the TextSpan in line 54. In other words: In order to trigger the crash you need a WidgetSpan that doesn't contribute any semantics and it must have a sibling TextSpan with a recognizer.

@goderbauer
Copy link
Member Author

@goderbauer goderbauer commented Nov 4, 2020

The workaround will hit another corner case. We have another issue that we only apply textscalefactor to the top most semantics node of the children.

@chunhtai Can you file a separate bug for that issue?

@chunhtai
Copy link
Contributor

@chunhtai chunhtai commented Nov 5, 2020

filed #69835

@mehmetf
Copy link
Member

@mehmetf mehmetf commented Nov 5, 2020

@goderbauer @chunhtai I know range check is the wrong fix here. However, since we are unsure how to fix it properly, do we consider it a safe bandaid? or could it generate accessibility nodes that do not map to the UI?

@goderbauer
Copy link
Member Author

@goderbauer goderbauer commented Nov 5, 2020

I think @chunhtai and I have a potential fix for this. @chunhtai is implementing it.

@mehmetf
Copy link
Member

@mehmetf mehmetf commented Nov 5, 2020

Great! Let me know when the PR is ready so I can patch in google3 and try it on the internal repro I have.

@mehmetf
Copy link
Member

@mehmetf mehmetf commented Nov 5, 2020

I have reduced the internal case into something manageable. It is still quite complex but I am hoping it will give you enough of a playground to figure out whether the issue is related.

main.dart: https://gist.github.com/mehmetf/7001d39f3ddc674f5f54e9763f6199cc
html_widget.dart: https://gist.github.com/mehmetf/42d11f9bb4d709e7dc699106659af66b

Several (huh?) moments:

@goderbauer
Copy link
Member Author

@goderbauer goderbauer commented Nov 5, 2020

There has to be at least four HtmlWidget's. Remove one and the problem goes away

I found this very puzzling, BUT I think I have an explanation for it! I think the reason for you needing multiple HtmlWidgets is clipping. Each of your HtmlWidgets on their own are fine. Every WidgetSpan in the HtmlWidget produces semantics, so they shouldn't crash by themselves. However, the HtmlWidget that is at the edge of the SingleScrollView viewport will get some of its content clipped (the part that's outside of the viewport). For the clipped part, we do not calculate any semantics nodes as an optimization (after all, they will not be visible) - and suddenly because of that you now have WidgetSpans that seem to not contribute any semantics (because we proactively didn't calculate the semantics for them). RenderParagraph.assembleSemantics expects semantics for each and every WidgetSpan (as we have established before) and crashes if it doesn't get them.

I think the fix @chunhtai and I have in mind will also fix this particular situation where semantic information are missing due to clipping (@chunhtai we should add a test for this scenario as well).

Here's a more minimal repo for the clipping case:

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ClipRect(           // If you remove the clip, the crash goes away
          child: Container(
            color: Colors.green,
            height: 100,
            width: 100,
            child: OverflowBox(
              alignment: Alignment.topLeft,
              maxWidth: double.infinity,
              child: RichText(
                text: TextSpan(
                  children: [
                    WidgetSpan(
                      child: Icon(
                        Icons.edit,
                        size: 16,
                        semanticLabel: 'not clipped',
                      ),
                    ),
                    TextSpan(
                      text: 'next WS is clipped',
                      recognizer: TapGestureRecognizer()..onTap = () { },
                    ),
                    WidgetSpan(
                      child: Icon(
                        Icons.edit,
                        size: 16,
                        semanticLabel: 'clipped',
                      ),
                    ),
                  ],
                ),
              ),
            )
          ),
        )
      ),
    );
  }
}

Notice how the RichText by itself is totally fine (every WidgetSpan contributes semantics) and if you remove the ClipRect this will not crash. However, if we clip the second WidgetSpan off we are left with a WidgetSpan that doesn't have any semantics node associated with it and we crash.

This is likely also going to be the reason why the work-around proposed in #69787 (comment) will not work (sorry about that). Even if we wrap the WidgetSpans in extra Semantics containers, they can still be clipped off triggering the bug again.

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

Successfully merging a pull request may close this issue.

4 participants
You can’t perform that action at this time.