superlistapp / super_editor

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

[SuperEditor] iOS carat doesn't blink when editor autoFocus set to true #1566

Closed Jethro87 closed 1 year ago

Jethro87 commented 1 year ago

Running super_editor stable / Flutter stable on iOS, the carat doesn't blink when autoFocus in the editor is set to true. You have to scroll a bit for the carat to start blinking.

Here's the error that occurs:

════════ Exception caught by foundation library ════════════════════════════════
The following assertion was thrown while dispatching notifications for ValueNotifier<TextInputConnection?>:
RenderBox was not laid out: RenderSemanticsAnnotations#374fe NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
'package:flutter/src/rendering/box.dart':
box.dart:1
Failed assertion: line 1965 pos 12: 'hasSize'

When the exception was thrown, this was the stack
#2      RenderBox.size
box.dart:1965
#3      SuperEditorImeInteractorState._reportSizeAndTransformToIme
supereditor_ime_interactor.dart:329
#4      SuperEditorImeInteractorState._reportVisualInformationToIme
supereditor_ime_interactor.dart:296
#5      SuperEditorImeInteractorState._onImeConnectionChange
supereditor_ime_interactor.dart:274
#6      ChangeNotifier.notifyListeners
change_notifier.dart:403
#7      ValueNotifier.value=
change_notifier.dart:530
#8      _DocumentSelectionOpenAndCloseImePolicyState._onSelectionChange.<anonymous closure>
document_ime_interaction_policies.dart:337
#9      Scheduler.runAsSoonAsPossible
flutter_scheduler.dart:34
#10     _DocumentSelectionOpenAndCloseImePolicyState._onSelectionChange
document_ime_interaction_policies.dart:329
#11     ChangeNotifier.notifyListeners
change_notifier.dart:403
#12     ValueNotifier.value=
change_notifier.dart:530
#13     PausableValueNotifier.resumeNotifications
pausable_value_notifier.dart:49
#14     MutableDocumentComposer.onTransactionEnd
document_composer.dart:156
#15     Editor.execute
editor.dart:166
#16     _EditorSelectionAndFocusPolicyState._onFocusChange
document_focus_and_selection_policies.dart:164
#17     ChangeNotifier.notifyListeners
change_notifier.dart:403
#18     FocusNode._notify
focus_manager.dart:1050
#19     FocusManager._applyFocusChange
focus_manager.dart:1646
(elided 4 frames from class _AssertionError and dart:async)
The ValueNotifier<TextInputConnection?> sending notification was: ValueNotifier<TextInputConnection?>#626ea(Instance of 'TextInputConnection')
════════════════════════════════════════════════════════════════════════════════
Jethro87 commented 1 year ago

cc @matthew-carroll

Jethro87 commented 1 year ago

I'm not sure if this is the same issue, but this issue (carat not blinking) also occurs when initializing an editor and attempting to set selection to the last selectable position. Here's a demo in super_editor example:

https://github.com/superlistapp/super_editor/assets/6467808/50aa7f01-9bad-4765-b6ff-d45403493e38

and one from my app:

https://github.com/superlistapp/super_editor/assets/6467808/5ef33ecc-9713-4499-b0d6-f16bbaba5fa3

I've modified the super_editor sliver example to include a SliverFillRemaining widget that, when tapped, sets selection at the end of the document. Notably, this issue only seems to occur the first time the selection is set. Scrolling or dismissing the keyboard and opening it again doesn't result in a frozen carat.

Here's the updated sliver_example_editor.dart code:

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

/// Example editor to show how the Rich Text Editor is working.
///
/// As the editor has an internal scrolling mechanism, for using it with Slivers
/// you need to give them a finite height or space to fill itself. That is why
/// the [SuperEditor] has a [SizedBox] wrapped around it to give a height.
class SliverExampleEditor extends StatefulWidget {
  @override
  State<SliverExampleEditor> createState() => _SliverExampleEditorState();
}

class _SliverExampleEditorState extends State<SliverExampleEditor> {
  // Toggle this, as a developer, to turn auto-scrolling debug
  // paint on/off.
  static const _showDebugPaint = false;

  final _scrollableKey = GlobalKey(debugLabel: "sliver_scrollable");
  late ScrollController _scrollController;
  final _minimapKey = GlobalKey(debugLabel: "sliver_minimap");

  late MutableDocument _doc;
  late MutableDocumentComposer _composer;
  late Editor _docEditor;
  final GlobalKey _docLayoutKey = GlobalKey();
  late DocumentLayout Function() _docLayout;
  late SuperEditorContext _editContext;
  final _docScroller = DocumentScroller();
  late final FocusNode _editorFocusNode;

  @override
  void initState() {
    super.initState();

    _scrollController = ScrollController();
    _docLayout = () => _docLayoutKey.currentState! as DocumentLayout;
    _doc = _createInitialDocument();
    _composer = MutableDocumentComposer();
    _docEditor = createDefaultDocumentEditor(document: _doc, composer: _composer);
    _editorFocusNode = FocusNode();
    _editContext = SuperEditorContext(
      editor: _docEditor,
      document: _doc,
      composer: _composer,
      getDocumentLayout: _docLayout,
      commonOps: CommonEditorOperations(
        document: _doc,
        editor: _docEditor,
        composer: _composer,
        documentLayoutResolver: _docLayout,
      ),
      scroller: _docScroller,
    );
  }

  @override
  void dispose() {
    _scrollController.dispose();
    _editorFocusNode.dispose();

    super.dispose();
  }

  void _setSelectionAtEndOfDocument() {
    final DocumentPosition? position = _editContext.documentLayout.findLastSelectablePosition();

    if (position == null) {
      return;
    }

    final newSelection = DocumentSelection.collapsed(
      position: position,
    );

    _composer.setSelectionWithReason(newSelection);
    _editorFocusNode.requestFocus();
  }

  @override
  Widget build(BuildContext context) {
    return ScrollingMinimaps(
      child: Stack(
        children: [
          Positioned.fill(
            child: CustomScrollView(
              key: _scrollableKey,
              controller: _scrollController,
              slivers: [
                SliverToBoxAdapter(
                  child: SuperEditor(
                    editor: _docEditor,
                    document: _doc,
                    composer: _composer,
                    focusNode: _editorFocusNode,
                    documentLayoutKey: _docLayoutKey,
                    stylesheet: defaultStylesheet.copyWith(
                      documentPadding: const EdgeInsets.symmetric(vertical: 56, horizontal: 24),
                    ),
                    debugPaint: const DebugPaintConfig(
                      gestures: _showDebugPaint,
                      scrollingMinimapId: _showDebugPaint ? "sliver_demo" : null,
                    ),
                  ),
                ),
                SliverFillRemaining(
                  hasScrollBody: false,
                  child: Semantics(
                    label: 'Tap to edit',
                    button: true,
                    child: GestureDetector(
                      onTap: _setSelectionAtEndOfDocument,
                    ),
                  ),
                ),
              ],
            ),
          ),
          if (_showDebugPaint) _buildScrollingMinimap(),
        ],
      ),
    );
  }

  Widget _buildScrollingMinimap() {
    return Positioned(
      top: 0,
      bottom: 0,
      right: 0,
      width: 200,
      child: ColoredBox(
        color: Colors.black.withOpacity(0.2),
        child: Center(
          child: ScrollingMinimap.fromRepository(
            key: _minimapKey,
            minimapId: "sliver_demo",
            minimapScale: 0.1,
          ),
        ),
      ),
    );
  }
}

MutableDocument _createInitialDocument() {
  return MutableDocument(
    nodes: [
      ParagraphNode(
        id: Editor.createNodeId(),
        text: AttributedText(
          'Hello',
        ),
      ),
    ],
  );
}

@matthew-carroll If you think this is a different issue I can move this into a separate github issue. Just let me know, thanks.