superlistapp / super_editor

A Flutter toolkit for building document editors and readers
https://superlist.com/SuperEditor/
MIT License
1.64k stars 239 forks source link

[BUG] - A RenderFlex expected a child of type RenderBox but received a child of type _RenderSliverHybridStack. #2266

Open llfbandit opened 3 weeks ago

llfbandit commented 3 weeks ago

Package Version 0.3.0-dev.4

0.3.0-dev.2 is OK. The issue has raised since SliverHybridStack introduction.

I use SuperEditor with many other components in screens. This version restricts to use it as main component.

Minimal Reproduction Code

// While out of real implementation, this code is enough to reproduce the issue.
SingleChildScrollView(child: SizedBox(height: 200, child: SuperEditor(...));
Error log and stack trace ════════ Exception caught by widgets library ═══════════════════════════════════ The following assertion was thrown building _FocusInheritedScope: A RenderFlex expected a child of type RenderBox but received a child of type _RenderSliverHybridStack. RenderObjects expect specific types of children because they coordinate with their children during layout and paint. For example, a RenderSliver cannot be the child of a RenderBox because a RenderSliver does not understand the RenderBox layout protocol. The RenderFlex that expected a RenderBox child was created by: Column ← _Decorator ← InputDecorator ← Column ← AppRichTextEditorInternal ← FutureBuilder ← AppRichTextEditor ← _NewsDescription ← Column ← Padding ← ConstrainedBox ← LayoutBuilder ← ⋯ The _RenderSliverHybridStack that did not match the expected child type was created by: SliverHybridStack ← _FocusInheritedScope ← Focus ← SuperEditorHardwareKeyHandler ← DocumentScrollable ← DocumentScaffold ← EditorSelectionAndFocusPolicy ← SuperEditorFocusDebugVisuals ← Builder ← SuperEditor ← Column ← _Decorator ← ⋯ When the exception was thrown, this was the stack: #0 ContainerRenderObjectMixin.debugValidateChild. (package:flutter/src/rendering/object.dart:4170:9) object.dart:4170 #1 ContainerRenderObjectMixin.debugValidateChild (package:flutter/src/rendering/object.dart:4197:6) object.dart:4197 #2 MultiChildRenderObjectElement.insertRenderObjectChild (package:flutter/src/widgets/framework.dart:6842:25) framework.dart:6842 #3 RenderObjectElement.attachRenderObject (package:flutter/src/widgets/framework.dart:6602:35) framework.dart:6602 #4 RenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6467:5) framework.dart:6467 #5 MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6911:11) framework.dart:6911 ... Normal element mounting (67 frames) #72 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4340:16) framework.dart:4340 #73 MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6904:36) framework.dart:6904 #74 MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6916:32) framework.dart:6916 #75 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4340:16) framework.dart:4340 #76 Element.updateChild (package:flutter/src/widgets/framework.dart:3849:18) framework.dart:3849 #77 SlottedRenderObjectElement._updateChildren (package:flutter/src/widgets/slotted_render_object_widget.dart:295:33) slotted_render_object_widget.dart:295 #78 SlottedRenderObjectElement.mount (package:flutter/src/widgets/slotted_render_object_widget.dart:249:5) slotted_render_object_widget.dart:249 ... Normal element mounting (9 frames) #87 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4340:16) framework.dart:4340 #88 MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6904:36) framework.dart:6904 #89 MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6916:32) framework.dart:6916 ... Normal element mounting (9 frames) #98 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4340:16) framework.dart:4340 #99 Element.updateChild (package:flutter/src/widgets/framework.dart:3843:20) framework.dart:3843 #100 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16) framework.dart:5512 #101 StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11) framework.dart:5650 #102 Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7) framework.dart:5203 #103 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2905:19) framework.dart:2905 #104 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:1136:21) binding.dart:1136 #105 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:443:5) binding.dart:443 #106 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1392:15) binding.dart:1392 #107 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1313:9) binding.dart:1313 #108 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1171:5) binding.dart:1171 #109 _invoke (dart:ui/hooks.dart:312:13) hooks.dart:312 #110 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:419:5) platform_dispatcher.dart:419 #111 _drawFrame (dart:ui/hooks.dart:283:31) hooks.dart:283

Actual behavior UI is broken.

Expected behavior SuperEditor should be insertable anywhere in the treeview.

Platform Windows, but could obviously be replicated on any platform.

Flutter version 3.22.3

matthew-carroll commented 3 weeks ago

We recently switched SuperEditor to use Slivers. You now need to use a CustomScrollView to contain a SuperEditor instead of a SingleChildScrollView.

Please confirm whether that solves your problem.

paurakhsharma commented 3 weeks ago

Replacing it with this works

CustomScrollView(slivers: [SuperEditor(...)])

But still this doesn't work

CustomScrollView(
            slivers: [
              SliverToBoxAdapter(
                child: SizedBox(
                  height: 200,
                  child: SuperEditor(...),
                ),
              ),
            ],
          )

Restricting SuperEditor to only function within CustomScrollView creates a significant challenge, limiting its flexibility compared to being a standard Widget.

paurakhsharma commented 3 weeks ago

For example how would I do something like this:

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

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

class MainApp extends StatefulWidget {
  const MainApp({super.key});

  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  @override
  Widget build(BuildContext context) {
    final paragraphs = [
      [
        ParagraphNode(
          id: '1',
          text: AttributedText(
            'Paragraph 1: Notice that when this document is short enough, the messages are pushed to the bottom of the viewport.\n\nTry adding more content to see things scroll.',
          ),
        )
      ],
      [
        ParagraphNode(
          id: '2',
          text: AttributedText(
            'Paragraph 2: Notice that when this document is short enough, the messages are pushed to the bottom of the viewport.\n\nTry adding more content to see things scroll.',
          ),
        )
      ],
      [
        ParagraphNode(
          id: '3',
          text: AttributedText(
            'Paragraph 3: Notice that when this document is short enough, the messages are pushed to the bottom of the viewport.\n\nTry adding more content to see things scroll.',
          ),
        )
      ],
    ];
    return MaterialApp(
      home: Scaffold(
        body: SafeArea(
          child: CustomScrollView(
            slivers: [
              const SliverAppBar(
                title: Text('Super Editor Example'),
                floating: true,
                snap: true,
              ),
              SliverList.builder(
                itemCount: paragraphs.length,
                itemBuilder: (context, index) {
                  return Column(
                    children: [
                      Text('Paragraph ${index + 1}'),
                      NodesRenderer(nodes: paragraphs[index]),
                    ],
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class NodesRenderer extends StatefulWidget {
  final List<DocumentNode> nodes;
  const NodesRenderer({super.key, required this.nodes});

  @override
  State<NodesRenderer> createState() => _NodesRendererState();
}

class _NodesRendererState extends State<NodesRenderer> {
  late MutableDocument _doc;

  @override
  Widget build(BuildContext context) {
    return SuperReader(document: _doc);
  }

  @override
  void initState() {
    super.initState();
    _doc = _createDocument();
  }

  MutableDocument _createDocument() {
    return MutableDocument(nodes: widget.nodes);
  }
}
llfbandit commented 3 weeks ago

I just finished the upgrade. Finally, I just wrapped the editor with CustomScrollView for now and added dedicated UI size constaints to recover what it was before.

After a bit of testing, when there's no scrollable ancestor to the editor, so for example, putting it directly in the body of a Scaffold or at the root of an app, it works. So I guess there's a missing piece here. Since SliverHybridStack is already inside a CustomScrollView internally I don't know what's going on. Maybe this is just a context issue when finding scrollable ancestor...

Off-topic: Other than that, so far, 0.3.0-dev.4 is quite stable (I don't use history feature). The only issue I noticed is with multi-line selection. The view scrolls from the center of the viewport and very quickly. AutoScrollController?

matthew-carroll commented 3 weeks ago

@paurakhsharma - I'm not sure what you're trying to communicate with the large code sample you provided. Can you describe your UI/UX goal that you're unable to achieve?

@llfbandit - You alluded to two bugs. Can you please make sure you file tickets for those issues if you haven't already?

KevinBrendel commented 3 weeks ago

Based on @knopp's response and explanations here, here is a widget that can help you get back the old behavior if you wrap SuperEditor with it:

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:super_editor/super_editor.dart';

class SuperEditorUnsliverizer extends StatelessWidget {
  final SuperEditor child;

  const SuperEditorUnsliverizer({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      physics: const NeverScrollableScrollPhysics(),
      child: _FakeViewport(child: child),
    );
  }
}

class _FakeViewport extends SingleChildRenderObjectWidget {
  const _FakeViewport({super.key, required super.child});

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _RenderFakeViewport();
  }
}

class _RenderFakeViewport extends RenderBox
    with RenderObjectWithChildMixin<RenderSliver>
    implements RenderAbstractViewport {
  @override
  void debugAssertDoesMeetConstraints() {}

  @override
  RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect? rect, Axis? axis}) {
    return const RevealedOffset(offset: 0, rect: Rect.zero);
  }

  @override
  void setupParentData(RenderObject child) {}

  @override
  Rect get paintBounds => Rect.zero;

  @override
  void performLayout() {
    final childConstraints = SliverConstraints(
      axisDirection: AxisDirection.down,
      growthDirection: GrowthDirection.forward,
      userScrollDirection: ScrollDirection.forward,
      scrollOffset: 0,
      precedingScrollExtent: 0,
      overlap: 0,
      remainingPaintExtent: constraints.maxHeight,
      crossAxisExtent: constraints.maxWidth,
      crossAxisDirection: AxisDirection.right,
      viewportMainAxisExtent: constraints.maxHeight,
      remainingCacheExtent: double.infinity,
      cacheOrigin: 0,
    );
    child!.layout(childConstraints, parentUsesSize: true);
    final geometry = child!.geometry;
    size = Size(constraints.maxWidth, geometry!.scrollExtent);
  }

  RenderBox _getBox(RenderSliver sliver) {
    RenderSliver? firstSliver;
    RenderBox? firstBox;
    sliver.visitChildren((child) {
      if (child is RenderSliver && firstSliver == null) {
        firstSliver = child;
      }
      if (child is RenderBox && firstBox == null) {
        firstBox = child;
      }
    });
    return firstSliver != null ? _getBox(firstSliver!) : firstBox!;
  }

  @override
  Size computeDryLayout(covariant BoxConstraints constraints) {
    final layoutBox = _getBox(child!);
    return layoutBox.computeDryLayout(constraints);
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    final layoutBox = _getBox(child!);
    return layoutBox.computeMaxIntrinsicWidth(height);
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    final layoutBox = _getBox(child!);
    return layoutBox.computeMinIntrinsicWidth(height);
  }

  @override
  computeMaxIntrinsicHeight(double width) {
    final layoutBox = _getBox(child!);
    return layoutBox.computeMaxIntrinsicHeight(width);
  }

  @override
  computeMinIntrinsicHeight(double width) {
    final layoutBox = _getBox(child!);
    return layoutBox.computeMinIntrinsicHeight(width);
  }

  @override
  void applyPaintTransform(RenderObject child, Matrix4 transform) {}

  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    return child!.hitTest(
      SliverHitTestResult.wrap(result),
      mainAxisPosition: position.dy,
      crossAxisPosition: position.dx,
    );
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    context.paintChild(child!, offset);
  }

  @override
  void performResize() {}

  @override
  Rect get semanticBounds => Offset.zero & size;
}

Depending on your needs, you might not need the SingleChildScrollview or want different physics, etc. That part is pretty hacky and maybe won't be necessary with future versions.

andreidevo commented 2 weeks ago

it's already few hours I can't just add this package to the project. Please make Understandable Readme. Because now it's completely not

This bug I'm trying to solve, it's always gives this error of RenderBox

I will try to downgrade now, as it could be recent changes which leaded to the bugs

knopp commented 2 weeks ago

@andreidevo, can you provide a reproducible example?

As for the original issue, instead of putting SuperEditor into SingleChildScrollView, you can put it inside CustomScrollView as a sliver and things will just work. Or don't use a scroll view at all and SuperEditor will create its own.