Skip to content

Commit

Permalink
Add widget to resize the tree according to the panel size
Browse files Browse the repository at this point in the history
  • Loading branch information
angelosilvestre committed Sep 25, 2024
1 parent aeea934 commit cc874f8
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 24 deletions.
109 changes: 89 additions & 20 deletions super_editor/example/lib/demos/mobile_chat/demo_mobile_chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ class MobileChatDemo extends StatefulWidget {
}

class _MobileChatDemoState extends State<MobileChatDemo> {
final FocusNode _focusNode = FocusNode();
late final Editor _editor;
late final KeyboardPanelController _keyboardPanelController;
final SoftwareKeyboardController _softwareKeyboardController = SoftwareKeyboardController();

@override
void initState() {
Expand All @@ -18,22 +21,49 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
final document = MutableDocument.empty();
final composer = MutableDocumentComposer();
_editor = createDefaultDocumentEditor(document: document, composer: composer);
_keyboardPanelController = KeyboardPanelController(
softwareKeyboardController: _softwareKeyboardController,
);
}

@override
void dispose() {
_keyboardPanelController.dispose();
_focusNode.dispose();
super.dispose();
}

void _endEditing() {
_keyboardPanelController.closeKeyboardAndPanel();

_editor.execute([
const ClearSelectionRequest(),
]);

// If we clear SuperEditor's selection, but leave SuperEditor focused, then
// SuperEditor will automatically place the caret at the end of the document.
// This is because SuperEditor always expects a place for text input when it
// has focus. To prevent this from happening, we explicitly remove focus
// from SuperEditor.
_focusNode.unfocus();
}

@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: ColoredBox(color: Colors.white),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _buildCommentEditor(),
),
],
return KeyboardScaffoldSafeArea(
child: Stack(
children: [
Positioned.fill(
child: ColoredBox(color: Colors.white),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _buildCommentEditor(),
),
],
),
);
}

Expand Down Expand Up @@ -63,20 +93,59 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
],
),
padding: const EdgeInsets.only(top: 16, bottom: 24),
child: CustomScrollView(
shrinkWrap: true,
slivers: [
SuperEditor(
editor: _editor,
child: KeyboardPanelScaffold(
controller: _keyboardPanelController,
aboveKeyboardBuilder: _buildKeyboardToolbar,
keyboardPanelBuilder: (context) => Container(
color: Colors.blue,
height: 100,
),
contentBuilder: (context, isKeyboardVisible) {
return CustomScrollView(
shrinkWrap: true,
stylesheet: _chatStylesheet,
),
],
slivers: [
SuperEditor(
editor: _editor,
focusNode: _focusNode,
softwareKeyboardController: _softwareKeyboardController,
shrinkWrap: true,
stylesheet: _chatStylesheet,
selectionPolicies: const SuperEditorSelectionPolicies(
clearSelectionWhenEditorLosesFocus: false,
clearSelectionWhenImeConnectionCloses: false,
),
),
],
);
},
),
),
],
);
}

Widget _buildKeyboardToolbar(BuildContext context, bool isKeyboardPanelVisible) {
return Container(
width: double.infinity,
height: 54,
color: Colors.grey.shade100,
child: Row(
children: [
const SizedBox(width: 24),
GestureDetector(
onTap: _endEditing,
child: const Icon(Icons.close),
),
const Spacer(),
GestureDetector(
onTap: () => _keyboardPanelController.toggleKeyboard(),
child: Icon(isKeyboardPanelVisible ? Icons.keyboard : Icons.keyboard_hide),
),
const SizedBox(width: 24),
],
),
);
}
}

final _chatStylesheet = defaultStylesheet.copyWith(
Expand Down
1 change: 1 addition & 0 deletions super_editor/example/lib/main_super_editor_chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ void main() {
runApp(
MaterialApp(
home: Scaffold(
resizeToAvoidBottomInset: false,
body: MobileChatDemo(),
),
debugShowCheckedModeBanner: false,
Expand Down
100 changes: 96 additions & 4 deletions super_editor/lib/src/infrastructure/keyboard_panel_scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import 'package:super_editor/src/infrastructure/flutter/flutter_scheduler.dart';
/// It is required that the enclosing [Scaffold] has `resizeToAvoidBottomInset` set to `false`,
/// otherwise we can't get the software keyboard height to size the keyboard panel. If
/// `resizeToAvoidBottomInset` is set to `true`, the panel won't be displayed.
///
/// Place a [KeyboardScaffoldSafeArea] higher in the widget tree to adjust the padding so
/// that the content is above the keyboard panel and software keyboard.
class KeyboardPanelScaffold extends StatefulWidget {
const KeyboardPanelScaffold({
super.key,
Expand Down Expand Up @@ -87,6 +90,8 @@ class _KeyboardPanelScaffoldState extends State<KeyboardPanelScaffold> with Sing
widget.controller.toolbarVisibility == KeyboardToolbarVisibility.visible ||
(widget.controller.toolbarVisibility == KeyboardToolbarVisibility.auto && _keyboardHeight.value > 0);

final _toolbarKey = GlobalKey();

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -165,7 +170,7 @@ class _KeyboardPanelScaffoldState extends State<KeyboardPanelScaffold> with Sing
// The keyboard is expanding.
_maxBottomInsets = newBottomInset;
_keyboardHeight.value = _maxBottomInsets;

onNextFrame((ts) => _updateSafeArea());
return;
}

Expand All @@ -174,6 +179,7 @@ class _KeyboardPanelScaffoldState extends State<KeyboardPanelScaffold> with Sing
// Follow the keyboard back down.
_maxBottomInsets = newBottomInset;
_keyboardHeight.value = _maxBottomInsets;
onNextFrame((ts) => _updateSafeArea());
return;
}
}
Expand All @@ -183,13 +189,25 @@ class _KeyboardPanelScaffoldState extends State<KeyboardPanelScaffold> with Sing
void _updatePanelForExitAnimation() {
setState(() {
_keyboardHeight.value = _maxBottomInsets * Curves.easeInQuad.transform(_panelExitAnimation.value);
onNextFrame((ts) => _updateSafeArea());
if (_panelExitAnimation.status == AnimationStatus.dismissed) {
// The panel has been fully collapsed. Reset the max known bottom insets.
_maxBottomInsets = 0.0;
}
});
}

/// Update the bottom insets of the enclosing [KeyboardScaffoldSafeArea].
void _updateSafeArea() {
final keyboardSafeAreaData = KeyboardScaffoldSafeArea.maybeOf(context);
if (keyboardSafeAreaData == null) {
return;
}

final toolbarSize = (_toolbarKey.currentContext?.findRenderObject() as RenderBox?)?.size;
keyboardSafeAreaData.bottomInsets = _keyboardHeight.value + (toolbarSize?.height ?? 0);
}

@override
Widget build(BuildContext context) {
final wantsToShowKeyboardPanel = widget.controller.wantsToShowKeyboardPanel ||
Expand All @@ -216,9 +234,12 @@ class _KeyboardPanelScaffoldState extends State<KeyboardPanelScaffold> with Sing
mainAxisSize: MainAxisSize.min,
children: [
if (_wantsToShowAboveKeyboardPanel)
widget.aboveKeyboardBuilder(
context,
widget.controller.wantsToShowKeyboardPanel,
KeyedSubtree(
key: _toolbarKey,
child: widget.aboveKeyboardBuilder(
context,
widget.controller.wantsToShowKeyboardPanel,
),
),
SizedBox(
height: _keyboardHeight.value,
Expand Down Expand Up @@ -318,3 +339,74 @@ enum KeyboardToolbarVisibility {
/// The toolbar should be visible only when the software keyboard is open.
auto,
}

/// Applies padding to the bottom of the child to avoid the software keyboard and
/// the above-keyboard toolbar.
///
/// The padding is set by a [KeyboardPanelScaffold] widget in the subtree.
class KeyboardScaffoldSafeArea extends StatefulWidget {
static KeyboardSafeAreaData of(BuildContext context) {
return maybeOf(context)!;
}

static KeyboardSafeAreaData? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_InheritedKeyboardScaffoldSafeArea>()?.keyboardSafeAreaData;
}

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

final Widget child;

@override
State<KeyboardScaffoldSafeArea> createState() => _KeyboardScaffoldSafeAreaState();
}

class _KeyboardScaffoldSafeAreaState extends State<KeyboardScaffoldSafeArea> {
final KeyboardSafeAreaData _keyboardSafeAreaData = KeyboardSafeAreaData();

@override
Widget build(BuildContext context) {
return _InheritedKeyboardScaffoldSafeArea(
keyboardSafeAreaData: _keyboardSafeAreaData,
child: ListenableBuilder(
listenable: _keyboardSafeAreaData,
builder: (context, _) {
return Padding(
padding: EdgeInsets.only(bottom: _keyboardSafeAreaData.bottomInsets),
child: widget.child,
);
},
),
);
}
}

class _InheritedKeyboardScaffoldSafeArea extends InheritedWidget {
const _InheritedKeyboardScaffoldSafeArea({
required this.keyboardSafeAreaData,
required super.child,
});

final KeyboardSafeAreaData keyboardSafeAreaData;

@override
bool updateShouldNotify(covariant _InheritedKeyboardScaffoldSafeArea oldWidget) {
return oldWidget.keyboardSafeAreaData != keyboardSafeAreaData;
}
}

class KeyboardSafeAreaData with ChangeNotifier {
KeyboardSafeAreaData({
double bottomInsets = 0.0,
}) : _bottomInsets = bottomInsets;

double get bottomInsets => _bottomInsets;
double _bottomInsets;
set bottomInsets(double value) {
_bottomInsets = value;
notifyListeners();
}
}

0 comments on commit cc874f8

Please sign in to comment.