diff --git a/simplistic_editor/lib/app_state.dart b/simplistic_editor/lib/app_state.dart new file mode 100644 index 000000000..1c295e0d2 --- /dev/null +++ b/simplistic_editor/lib/app_state.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'app_state_manager.dart'; +import 'formatting_toolbar.dart' show ToggleButtonsState; +import 'replacements.dart'; + +class AppState { + const AppState({ + required this.replacementsController, + required this.textEditingDeltaHistory, + required this.toggleButtonsState, + }); + + final ReplacementTextEditingController replacementsController; + final List textEditingDeltaHistory; + final Set toggleButtonsState; + + AppState copyWith({ + ReplacementTextEditingController? replacementsController, + List? textEditingDeltaHistory, + Set? toggleButtonsState, + }) { + return AppState( + replacementsController: + replacementsController ?? this.replacementsController, + textEditingDeltaHistory: + textEditingDeltaHistory ?? this.textEditingDeltaHistory, + toggleButtonsState: toggleButtonsState ?? this.toggleButtonsState, + ); + } +} + +class AppStateWidget extends StatefulWidget { + const AppStateWidget({super.key, required this.child}); + + final Widget child; + + static AppStateWidgetState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } + + @override + State createState() => AppStateWidgetState(); +} + +class AppStateWidgetState extends State { + AppState _data = AppState( + replacementsController: ReplacementTextEditingController( + text: 'The quick brown fox jumps over the lazy dog.'), + textEditingDeltaHistory: [], + toggleButtonsState: {}, + ); + + void updateTextEditingDeltaHistory(List textEditingDeltas) { + _data = _data.copyWith(textEditingDeltaHistory: [ + ..._data.textEditingDeltaHistory, + ...textEditingDeltas + ]); + setState(() {}); + } + + void updateToggleButtonsStateOnSelectionChanged( + TextSelection selection, ReplacementTextEditingController controller) { + // When the selection changes we want to check the replacements at the new + // selection. Enable/disable toggle buttons based on the replacements found + // at the new selection. + final List replacementStyles = + controller.getReplacementsAtSelection(selection); + final Set hasChanged = {}; + + if (replacementStyles.isEmpty) { + _data = _data.copyWith( + toggleButtonsState: Set.from(_data.toggleButtonsState) + ..removeAll({ + ToggleButtonsState.bold, + ToggleButtonsState.italic, + ToggleButtonsState.underline, + }), + ); + } + + for (final TextStyle style in replacementStyles) { + // See [_updateToggleButtonsStateOnButtonPressed] for how + // Bold, Italic and Underline are encoded into [style] + if (style.fontWeight != null && + !hasChanged.contains(ToggleButtonsState.bold)) { + _data = _data.copyWith( + toggleButtonsState: Set.from(_data.toggleButtonsState) + ..add(ToggleButtonsState.bold), + ); + hasChanged.add(ToggleButtonsState.bold); + } + + if (style.fontStyle != null && + !hasChanged.contains(ToggleButtonsState.italic)) { + _data = _data.copyWith( + toggleButtonsState: Set.from(_data.toggleButtonsState) + ..add(ToggleButtonsState.italic), + ); + hasChanged.add(ToggleButtonsState.italic); + } + + if (style.decoration != null && + !hasChanged.contains(ToggleButtonsState.underline)) { + _data = _data.copyWith( + toggleButtonsState: Set.from(_data.toggleButtonsState) + ..add(ToggleButtonsState.underline), + ); + hasChanged.add(ToggleButtonsState.underline); + } + } + + for (final TextStyle style in replacementStyles) { + if (style.fontWeight == null && + !hasChanged.contains(ToggleButtonsState.bold)) { + _data = _data.copyWith( + toggleButtonsState: Set.from(_data.toggleButtonsState) + ..remove(ToggleButtonsState.bold), + ); + hasChanged.add(ToggleButtonsState.bold); + } + + if (style.fontStyle == null && + !hasChanged.contains(ToggleButtonsState.italic)) { + _data = _data.copyWith( + toggleButtonsState: Set.from(_data.toggleButtonsState) + ..remove(ToggleButtonsState.italic), + ); + hasChanged.add(ToggleButtonsState.italic); + } + + if (style.decoration == null && + !hasChanged.contains(ToggleButtonsState.underline)) { + _data = _data.copyWith( + toggleButtonsState: Set.from(_data.toggleButtonsState) + ..remove(ToggleButtonsState.underline), + ); + hasChanged.add(ToggleButtonsState.underline); + } + } + + setState(() {}); + } + + void updateToggleButtonsStateOnButtonPressed(int index) { + Map attributeMap = const { + 0: TextStyle(fontWeight: FontWeight.bold), + 1: TextStyle(fontStyle: FontStyle.italic), + 2: TextStyle(decoration: TextDecoration.underline), + }; + + final ReplacementTextEditingController controller = + _data.replacementsController; + + final TextRange replacementRange = TextRange( + start: controller.selection.start, + end: controller.selection.end, + ); + + final targetToggleButtonState = ToggleButtonsState.values[index]; + + if (_data.toggleButtonsState.contains(targetToggleButtonState)) { + _data = _data.copyWith( + toggleButtonsState: Set.from(_data.toggleButtonsState) + ..remove(targetToggleButtonState), + ); + } else { + _data = _data.copyWith( + toggleButtonsState: Set.from(_data.toggleButtonsState) + ..add(targetToggleButtonState), + ); + } + + if (_data.toggleButtonsState.contains(targetToggleButtonState)) { + controller.applyReplacement( + TextEditingInlineSpanReplacement( + replacementRange, + (string, range) => TextSpan(text: string, style: attributeMap[index]), + true, + ), + ); + _data = _data.copyWith(replacementsController: controller); + setState(() {}); + } else { + controller.disableExpand(attributeMap[index]!); + controller.removeReplacementsAtRange( + replacementRange, attributeMap[index]); + _data = _data.copyWith(replacementsController: controller); + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + return AppStateManager( + state: _data, + child: widget.child, + ); + } +} diff --git a/simplistic_editor/lib/app_state_manager.dart b/simplistic_editor/lib/app_state_manager.dart new file mode 100644 index 000000000..3f9120d86 --- /dev/null +++ b/simplistic_editor/lib/app_state_manager.dart @@ -0,0 +1,27 @@ +import 'package:flutter/widgets.dart'; + +import 'app_state.dart'; + +class AppStateManager extends InheritedWidget { + const AppStateManager({ + super.key, + required super.child, + required AppState state, + }) : _appState = state; + + static AppStateManager of(BuildContext context) { + final AppStateManager? result = + context.dependOnInheritedWidgetOfExactType(); + assert(result != null, 'No AppStateManager found in context'); + return result!; + } + + final AppState _appState; + + AppState get appState => _appState; + + @override + bool updateShouldNotify(AppStateManager oldWidget) { + return appState != oldWidget.appState; + } +} diff --git a/simplistic_editor/lib/basic_text_input_client.dart b/simplistic_editor/lib/basic_text_input_client.dart index 941e082dc..cd720030c 100644 --- a/simplistic_editor/lib/basic_text_input_client.dart +++ b/simplistic_editor/lib/basic_text_input_client.dart @@ -1,13 +1,13 @@ import 'dart:math' as math; + import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +import 'app_state.dart'; import 'replacements.dart'; -import 'text_editing_delta_history_manager.dart'; -import 'toggle_buttons_state_manager.dart'; /// Signature for the callback that reports when the user changes the selection /// (including the cursor location). @@ -43,8 +43,7 @@ class BasicTextInputClientState extends State with TextSelectionDelegate implements DeltaTextInputClient { final GlobalKey _textKey = GlobalKey(); - late final ToggleButtonsStateManager toggleButtonStateManager; - late final TextEditingDeltaHistoryManager textEditingDeltaHistoryManager; + late AppStateWidgetState manager; final ClipboardStatusNotifier? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier(); @@ -58,8 +57,7 @@ class BasicTextInputClientState extends State @override void didChangeDependencies() { super.didChangeDependencies(); - toggleButtonStateManager = ToggleButtonsStateManager.of(context); - textEditingDeltaHistoryManager = TextEditingDeltaHistoryManager.of(context); + manager = AppStateWidget.of(context); } @override @@ -149,8 +147,7 @@ class BasicTextInputClientState extends State final bool selectionChanged = _value.selection.start != value.selection.start || _value.selection.end != value.selection.end; - textEditingDeltaHistoryManager - .updateTextEditingDeltaHistoryOnInput(textEditingDeltas); + manager.updateTextEditingDeltaHistory(textEditingDeltas); _value = value; @@ -162,7 +159,8 @@ class BasicTextInputClientState extends State } if (selectionChanged) { - toggleButtonStateManager.updateToggleButtonsOnSelection(value.selection); + manager.updateToggleButtonsStateOnSelectionChanged(value.selection, + widget.controller as ReplacementTextEditingController); } } @@ -256,7 +254,8 @@ class BasicTextInputClientState extends State final TextSelection validSelection = TextSelection.collapsed(offset: _value.text.length); _handleSelectionChanged(validSelection, null); - toggleButtonStateManager.updateToggleButtonsOnSelection(validSelection); + manager.updateToggleButtonsStateOnSelectionChanged(validSelection, + widget.controller as ReplacementTextEditingController); } } } @@ -284,8 +283,7 @@ class BasicTextInputClientState extends State } if (value != _value) { - textEditingDeltaHistoryManager - .updateTextEditingDeltaHistoryOnInput([textEditingDelta]); + manager.updateTextEditingDeltaHistory([textEditingDelta]); } userUpdateTextEditingValue(value, cause); @@ -595,8 +593,7 @@ class BasicTextInputClientState extends State (widget.controller as ReplacementTextEditingController) .syncReplacementRanges(selectionUpdate); } - textEditingDeltaHistoryManager - .updateTextEditingDeltaHistoryOnInput([selectionUpdate]); + manager.updateTextEditingDeltaHistory([selectionUpdate]); } } @@ -610,8 +607,8 @@ class BasicTextInputClientState extends State _handleSelectionChanged(_value.selection, cause); if (selectionRangeChanged) { - toggleButtonStateManager - .updateToggleButtonsOnSelection(_value.selection); + manager.updateToggleButtonsStateOnSelectionChanged(_value.selection, + widget.controller as ReplacementTextEditingController); } } } diff --git a/simplistic_editor/lib/formatting_toolbar.dart b/simplistic_editor/lib/formatting_toolbar.dart new file mode 100644 index 000000000..8f7e9521a --- /dev/null +++ b/simplistic_editor/lib/formatting_toolbar.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import 'app_state.dart'; +import 'app_state_manager.dart'; + +/// The toggle buttons that can be selected. +enum ToggleButtonsState { + bold, + italic, + underline, +} + +class FormattingToolbar extends StatelessWidget { + const FormattingToolbar({super.key}); + + @override + Widget build(BuildContext context) { + final AppStateManager manager = AppStateManager.of(context); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ToggleButtons( + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + isSelected: [ + manager.appState.toggleButtonsState + .contains(ToggleButtonsState.bold), + manager.appState.toggleButtonsState + .contains(ToggleButtonsState.italic), + manager.appState.toggleButtonsState + .contains(ToggleButtonsState.underline), + ], + onPressed: (index) => AppStateWidget.of(context) + .updateToggleButtonsStateOnButtonPressed(index), + children: const [ + Icon(Icons.format_bold), + Icon(Icons.format_italic), + Icon(Icons.format_underline), + ], + ), + ], + ), + ); + } +} diff --git a/simplistic_editor/lib/main.dart b/simplistic_editor/lib/main.dart index e95255e49..1b6287ffb 100644 --- a/simplistic_editor/lib/main.dart +++ b/simplistic_editor/lib/main.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'app_state.dart'; +import 'app_state_manager.dart'; import 'basic_text_field.dart'; +import 'formatting_toolbar.dart'; import 'replacements.dart'; -import 'text_editing_delta_history_manager.dart'; -import 'toggle_buttons_state_manager.dart'; +import 'text_editing_delta_history_view.dart'; void main() { runApp(const MyApp()); @@ -15,14 +16,16 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Simplistic Editor', - theme: ThemeData( - primarySwatch: Colors.blue, - useMaterial3: true, + return AppStateWidget( + child: MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Simplistic Editor', + theme: ThemeData( + primarySwatch: Colors.blue, + useMaterial3: true, + ), + home: const MyHomePage(title: 'Simplistic Editor'), ), - home: const MyHomePage(title: 'Simplistic Editor'), ); } } @@ -37,228 +40,20 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - final ReplacementTextEditingController _replacementTextEditingController = - ReplacementTextEditingController( - text: 'The quick brown fox jumps over the lazy dog.', - ); + late ReplacementTextEditingController _replacementTextEditingController; final FocusNode _focusNode = FocusNode(); - final Set _isSelected = {}; - final List _textEditingDeltaHistory = []; - - void _updateTextEditingDeltaHistory( - List textEditingDeltas) { - for (final TextEditingDelta delta in textEditingDeltas) { - _textEditingDeltaHistory.add(delta); - } - - setState(() {}); - } - - List _buildTextEditingDeltaHistoryViews( - List textEditingDeltas) { - List textEditingDeltaViews = []; - - for (final TextEditingDelta delta in textEditingDeltas) { - final TextEditingDeltaView deltaView; - - if (delta is TextEditingDeltaInsertion) { - deltaView = TextEditingDeltaView( - deltaType: 'Insertion', - deltaText: delta.textInserted, - deltaRange: TextRange.collapsed(delta.insertionOffset), - newSelection: delta.selection, - newComposing: delta.composing, - ); - } else if (delta is TextEditingDeltaDeletion) { - deltaView = TextEditingDeltaView( - deltaType: 'Deletion', - deltaText: delta.textDeleted, - deltaRange: delta.deletedRange, - newSelection: delta.selection, - newComposing: delta.composing, - ); - } else if (delta is TextEditingDeltaReplacement) { - deltaView = TextEditingDeltaView( - deltaType: 'Replacement', - deltaText: delta.replacementText, - deltaRange: delta.replacedRange, - newSelection: delta.selection, - newComposing: delta.composing, - ); - } else if (delta is TextEditingDeltaNonTextUpdate) { - deltaView = TextEditingDeltaView( - deltaType: 'NonTextUpdate', - deltaText: '', - deltaRange: TextRange.empty, - newSelection: delta.selection, - newComposing: delta.composing, - ); - } else { - deltaView = const TextEditingDeltaView( - deltaType: 'Error', - deltaText: 'Error', - deltaRange: TextRange.empty, - newSelection: TextRange.empty, - newComposing: TextRange.empty, - ); - } - - textEditingDeltaViews.add(deltaView); - } - - return textEditingDeltaViews.reversed.toList(); - } - - void _updateToggleButtonsStateOnSelectionChanged(TextSelection selection) { - // When the selection changes we want to check the replacements at the new - // selection. Enable/disable toggle buttons based on the replacements found - // at the new selection. - final List replacementStyles = - _replacementTextEditingController.getReplacementsAtSelection(selection); - final Set hasChanged = {}; - - if (replacementStyles.isEmpty) { - _isSelected.removeAll({ - ToggleButtonsState.bold, - ToggleButtonsState.italic, - ToggleButtonsState.underline - }); - } - - for (final TextStyle style in replacementStyles) { - // See [_updateToggleButtonsStateOnButtonPressed] for how - // Bold, Italic and Underline are encoded into [style] - if (style.fontWeight != null && - !hasChanged.contains(ToggleButtonsState.bold)) { - _isSelected.add(ToggleButtonsState.bold); - hasChanged.add(ToggleButtonsState.bold); - } - - if (style.fontStyle != null && - !hasChanged.contains(ToggleButtonsState.italic)) { - _isSelected.add(ToggleButtonsState.italic); - hasChanged.add(ToggleButtonsState.italic); - } - - if (style.decoration != null && - !hasChanged.contains(ToggleButtonsState.underline)) { - _isSelected.add(ToggleButtonsState.underline); - hasChanged.add(ToggleButtonsState.underline); - } - } - for (final TextStyle style in replacementStyles) { - if (style.fontWeight == null && - !hasChanged.contains(ToggleButtonsState.bold)) { - _isSelected.remove(ToggleButtonsState.bold); - hasChanged.add(ToggleButtonsState.bold); - } - - if (style.fontStyle == null && - !hasChanged.contains(ToggleButtonsState.italic)) { - _isSelected.remove(ToggleButtonsState.italic); - hasChanged.add(ToggleButtonsState.italic); - } - - if (style.decoration == null && - !hasChanged.contains(ToggleButtonsState.underline)) { - _isSelected.remove(ToggleButtonsState.underline); - hasChanged.add(ToggleButtonsState.underline); - } - } - - setState(() {}); - } - - void _updateToggleButtonsStateOnButtonPressed(int index) { - Map attributeMap = const { - 0: TextStyle(fontWeight: FontWeight.bold), - 1: TextStyle(fontStyle: FontStyle.italic), - 2: TextStyle(decoration: TextDecoration.underline), - }; - - final TextRange replacementRange = TextRange( - start: _replacementTextEditingController.selection.start, - end: _replacementTextEditingController.selection.end, - ); - - final targetToggleButtonState = ToggleButtonsState.values[index]; - - if (_isSelected.contains(targetToggleButtonState)) { - _isSelected.remove(targetToggleButtonState); - } else { - _isSelected.add(targetToggleButtonState); - } - - if (_isSelected.contains(targetToggleButtonState)) { - _replacementTextEditingController.applyReplacement( - TextEditingInlineSpanReplacement( - replacementRange, - (string, range) => TextSpan(text: string, style: attributeMap[index]), - true, - ), - ); - setState(() {}); - } else { - _replacementTextEditingController.disableExpand(attributeMap[index]!); - _replacementTextEditingController.removeReplacementsAtRange( - replacementRange, attributeMap[index]); - setState(() {}); - } - } - - Widget _buildTextEditingDeltaViewHeading(String text) { - return Text( - text, - style: const TextStyle( - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), - ); + @override + void initState() { + super.initState(); + _replacementTextEditingController = ReplacementTextEditingController(); } - Widget _buildTextEditingDeltaViewHeader() { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 35.0, vertical: 10.0), - child: Row( - children: [ - Expanded( - child: Tooltip( - message: 'The type of text input that is occurring.' - ' Check out the documentation for TextEditingDelta for more information.', - child: _buildTextEditingDeltaViewHeading('Delta Type'), - ), - ), - Expanded( - child: Tooltip( - message: 'The text that is being inserted or deleted', - child: _buildTextEditingDeltaViewHeading('Delta Text'), - ), - ), - Expanded( - child: Tooltip( - message: - 'The offset in the text where the text input is occurring.', - child: _buildTextEditingDeltaViewHeading('Delta Offset'), - ), - ), - Expanded( - child: Tooltip( - message: - 'The new text selection range after the text input has occurred.', - child: _buildTextEditingDeltaViewHeading('New Selection'), - ), - ), - Expanded( - child: Tooltip( - message: - 'The new composing range after the text input has occurred.', - child: _buildTextEditingDeltaViewHeading('New Composing'), - ), - ), - ], - ), - ); + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _replacementTextEditingController = + AppStateManager.of(context).appState.replacementsController; } static Route _aboutDialogBuilder( @@ -297,151 +92,29 @@ class _MyHomePageState extends State { body: Padding( padding: const EdgeInsets.all(16.0), child: Center( - child: ToggleButtonsStateManager( - isToggleButtonsSelected: _isSelected, - updateToggleButtonsStateOnButtonPressed: - _updateToggleButtonsStateOnButtonPressed, - updateToggleButtonStateOnSelectionChanged: - _updateToggleButtonsStateOnSelectionChanged, - child: TextEditingDeltaHistoryManager( - history: _textEditingDeltaHistory, - updateHistoryOnInput: _updateTextEditingDeltaHistory, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Builder(builder: (innerContext) { - final ToggleButtonsStateManager manager = - ToggleButtonsStateManager.of(innerContext); - - return ToggleButtons( - borderRadius: - const BorderRadius.all(Radius.circular(4.0)), - isSelected: [ - manager.toggleButtonsState - .contains(ToggleButtonsState.bold), - manager.toggleButtonsState - .contains(ToggleButtonsState.italic), - manager.toggleButtonsState - .contains(ToggleButtonsState.underline), - ], - onPressed: (index) => manager - .updateToggleButtonsOnButtonPressed(index), - children: const [ - Icon(Icons.format_bold), - Icon(Icons.format_italic), - Icon(Icons.format_underline), - ], - ); - }), - ], + child: Column( + children: [ + const FormattingToolbar(), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 35.0), + child: BasicTextField( + controller: _replacementTextEditingController, + style: const TextStyle( + fontSize: 18.0, + color: Colors.black, ), + focusNode: _focusNode, ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 35.0), - child: BasicTextField( - controller: _replacementTextEditingController, - style: const TextStyle( - fontSize: 18.0, color: Colors.black), - focusNode: _focusNode, - ), - ), - ), - Expanded( - child: Column( - children: [ - _buildTextEditingDeltaViewHeader(), - Expanded( - child: Builder( - builder: (innerContext) { - final TextEditingDeltaHistoryManager manager = - TextEditingDeltaHistoryManager.of( - innerContext); - return ListView.separated( - padding: const EdgeInsets.symmetric( - horizontal: 35.0), - itemBuilder: (context, index) { - return _buildTextEditingDeltaHistoryViews( - manager.textEditingDeltaHistory)[index]; - }, - itemCount: - manager.textEditingDeltaHistory.length, - separatorBuilder: (context, index) { - return const SizedBox(height: 2.0); - }, - ); - }, - ), - ), - const SizedBox(height: 10), - ], - ), - ), - ], + ), + ), + const Expanded( + child: TextEditingDeltaHistoryView(), ), - ), + ], ), ), ), ); } } - -class TextEditingDeltaView extends StatelessWidget { - const TextEditingDeltaView({ - super.key, - required this.deltaType, - required this.deltaText, - required this.deltaRange, - required this.newSelection, - required this.newComposing, - }); - - final String deltaType; - final String deltaText; - final TextRange deltaRange; - final TextRange newSelection; - final TextRange newComposing; - - @override - Widget build(BuildContext context) { - late final Color rowColor; - - switch (deltaType) { - case 'Insertion': - rowColor = Colors.greenAccent.shade100; - break; - case 'Deletion': - rowColor = Colors.redAccent.shade100; - break; - case 'Replacement': - rowColor = Colors.yellowAccent.shade100; - break; - case 'NonTextUpdate': - rowColor = Colors.blueAccent.shade100; - break; - default: - rowColor = Colors.white; - } - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - color: rowColor, - ), - padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 8.0), - child: Row( - children: [ - Expanded(child: Text(deltaType)), - Expanded(child: Text(deltaText)), - Expanded(child: Text('(${deltaRange.start}, ${deltaRange.end})')), - Expanded(child: Text('(${newSelection.start}, ${newSelection.end})')), - Expanded(child: Text('(${newComposing.start}, ${newComposing.end})')), - ], - ), - ); - } -} diff --git a/simplistic_editor/lib/text_editing_delta_history_manager.dart b/simplistic_editor/lib/text_editing_delta_history_manager.dart deleted file mode 100644 index beb7bff6f..000000000 --- a/simplistic_editor/lib/text_editing_delta_history_manager.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/services.dart' show TextEditingDelta; -import 'package:flutter/widgets.dart'; - -/// Signature for the callback that updates text editing delta history when a new delta -/// is received. -typedef TextEditingDeltaHistoryUpdateCallback = void Function( - List textEditingDeltas); - -class TextEditingDeltaHistoryManager extends InheritedWidget { - const TextEditingDeltaHistoryManager({ - super.key, - required super.child, - required List history, - required TextEditingDeltaHistoryUpdateCallback updateHistoryOnInput, - }) : _textEditingDeltaHistory = history, - _updateTextEditingDeltaHistoryOnInput = updateHistoryOnInput; - - static TextEditingDeltaHistoryManager of(BuildContext context) { - final TextEditingDeltaHistoryManager? result = context - .dependOnInheritedWidgetOfExactType(); - assert( - result != null, 'No TextEditingDeltaHistoryManager found in context'); - return result!; - } - - final List _textEditingDeltaHistory; - final TextEditingDeltaHistoryUpdateCallback - _updateTextEditingDeltaHistoryOnInput; - - List get textEditingDeltaHistory => - _textEditingDeltaHistory; - TextEditingDeltaHistoryUpdateCallback - get updateTextEditingDeltaHistoryOnInput => - _updateTextEditingDeltaHistoryOnInput; - - @override - bool updateShouldNotify(TextEditingDeltaHistoryManager oldWidget) { - return textEditingDeltaHistory != oldWidget.textEditingDeltaHistory; - } -} diff --git a/simplistic_editor/lib/text_editing_delta_history_view.dart b/simplistic_editor/lib/text_editing_delta_history_view.dart new file mode 100644 index 000000000..1cac8eb41 --- /dev/null +++ b/simplistic_editor/lib/text_editing_delta_history_view.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'app_state_manager.dart'; + +class TextEditingDeltaHistoryView extends StatelessWidget { + const TextEditingDeltaHistoryView({super.key}); + + List _buildTextEditingDeltaHistoryViews( + List textEditingDeltas) { + List textEditingDeltaViews = []; + + for (final TextEditingDelta delta in textEditingDeltas) { + final TextEditingDeltaView deltaView; + + if (delta is TextEditingDeltaInsertion) { + deltaView = TextEditingDeltaView( + deltaType: 'Insertion', + deltaText: delta.textInserted, + deltaRange: TextRange.collapsed(delta.insertionOffset), + newSelection: delta.selection, + newComposing: delta.composing, + ); + } else if (delta is TextEditingDeltaDeletion) { + deltaView = TextEditingDeltaView( + deltaType: 'Deletion', + deltaText: delta.textDeleted, + deltaRange: delta.deletedRange, + newSelection: delta.selection, + newComposing: delta.composing, + ); + } else if (delta is TextEditingDeltaReplacement) { + deltaView = TextEditingDeltaView( + deltaType: 'Replacement', + deltaText: delta.replacementText, + deltaRange: delta.replacedRange, + newSelection: delta.selection, + newComposing: delta.composing, + ); + } else if (delta is TextEditingDeltaNonTextUpdate) { + deltaView = TextEditingDeltaView( + deltaType: 'NonTextUpdate', + deltaText: '', + deltaRange: TextRange.empty, + newSelection: delta.selection, + newComposing: delta.composing, + ); + } else { + deltaView = const TextEditingDeltaView( + deltaType: 'Error', + deltaText: 'Error', + deltaRange: TextRange.empty, + newSelection: TextRange.empty, + newComposing: TextRange.empty, + ); + } + + textEditingDeltaViews.add(deltaView); + } + + return textEditingDeltaViews.reversed.toList(); + } + + Widget _buildTextEditingDeltaViewHeader() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 35.0, vertical: 10.0), + child: Row( + children: [ + Expanded( + child: Tooltip( + message: 'The type of text input that is occurring.' + ' Check out the documentation for TextEditingDelta for more information.', + child: _buildTextEditingDeltaViewHeading('Delta Type'), + ), + ), + Expanded( + child: Tooltip( + message: 'The text that is being inserted or deleted', + child: _buildTextEditingDeltaViewHeading('Delta Text'), + ), + ), + Expanded( + child: Tooltip( + message: + 'The offset in the text where the text input is occurring.', + child: _buildTextEditingDeltaViewHeading('Delta Offset'), + ), + ), + Expanded( + child: Tooltip( + message: + 'The new text selection range after the text input has occurred.', + child: _buildTextEditingDeltaViewHeading('New Selection'), + ), + ), + Expanded( + child: Tooltip( + message: + 'The new composing range after the text input has occurred.', + child: _buildTextEditingDeltaViewHeading('New Composing'), + ), + ), + ], + ), + ); + } + + Widget _buildTextEditingDeltaViewHeading(String text) { + return Text( + text, + style: const TextStyle( + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + ), + ); + } + + @override + Widget build(BuildContext context) { + final AppStateManager manager = AppStateManager.of(context); + + return Column( + children: [ + _buildTextEditingDeltaViewHeader(), + Expanded( + child: ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 35.0), + itemBuilder: (context, index) { + return _buildTextEditingDeltaHistoryViews( + manager.appState.textEditingDeltaHistory)[index]; + }, + itemCount: manager.appState.textEditingDeltaHistory.length, + separatorBuilder: (context, index) { + return const SizedBox(height: 2.0); + }, + ), + ), + const SizedBox(height: 10), + ], + ); + } +} + +class TextEditingDeltaView extends StatelessWidget { + const TextEditingDeltaView({ + super.key, + required this.deltaType, + required this.deltaText, + required this.deltaRange, + required this.newSelection, + required this.newComposing, + }); + + final String deltaType; + final String deltaText; + final TextRange deltaRange; + final TextRange newSelection; + final TextRange newComposing; + + @override + Widget build(BuildContext context) { + late final Color rowColor; + + switch (deltaType) { + case 'Insertion': + rowColor = Colors.greenAccent.shade100; + break; + case 'Deletion': + rowColor = Colors.redAccent.shade100; + break; + case 'Replacement': + rowColor = Colors.yellowAccent.shade100; + break; + case 'NonTextUpdate': + rowColor = Colors.blueAccent.shade100; + break; + default: + rowColor = Colors.white; + } + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + color: rowColor, + ), + padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 8.0), + child: Row( + children: [ + Expanded(child: Text(deltaType)), + Expanded(child: Text(deltaText)), + Expanded(child: Text('(${deltaRange.start}, ${deltaRange.end})')), + Expanded(child: Text('(${newSelection.start}, ${newSelection.end})')), + Expanded(child: Text('(${newComposing.start}, ${newComposing.end})')), + ], + ), + ); + } +} diff --git a/simplistic_editor/lib/toggle_buttons_state_manager.dart b/simplistic_editor/lib/toggle_buttons_state_manager.dart deleted file mode 100644 index 769dd5f7f..000000000 --- a/simplistic_editor/lib/toggle_buttons_state_manager.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/widgets.dart'; - -/// Signature for the callback that updates toggle button state when the user changes the selection -/// (including the cursor location). -typedef UpdateToggleButtonsStateOnSelectionChangedCallback = void Function( - TextSelection selection); - -/// Signature for the callback that updates toggle button state when the user -/// presses the toggle button. -typedef UpdateToggleButtonsStateOnButtonPressedCallback = void Function( - int index); - -/// The toggle buttons that can be selected. -enum ToggleButtonsState { - bold, - italic, - underline, -} - -class ToggleButtonsStateManager extends InheritedWidget { - const ToggleButtonsStateManager({ - super.key, - required super.child, - required Set isToggleButtonsSelected, - required UpdateToggleButtonsStateOnButtonPressedCallback - updateToggleButtonsStateOnButtonPressed, - required UpdateToggleButtonsStateOnSelectionChangedCallback - updateToggleButtonStateOnSelectionChanged, - }) : _isToggleButtonsSelected = isToggleButtonsSelected, - _updateToggleButtonsStateOnButtonPressed = - updateToggleButtonsStateOnButtonPressed, - _updateToggleButtonStateOnSelectionChanged = - updateToggleButtonStateOnSelectionChanged; - - static ToggleButtonsStateManager of(BuildContext context) { - final ToggleButtonsStateManager? result = - context.dependOnInheritedWidgetOfExactType(); - assert(result != null, 'No ToggleButtonsStateManager found in context'); - return result!; - } - - final Set _isToggleButtonsSelected; - final UpdateToggleButtonsStateOnButtonPressedCallback - _updateToggleButtonsStateOnButtonPressed; - final UpdateToggleButtonsStateOnSelectionChangedCallback - _updateToggleButtonStateOnSelectionChanged; - - Set get toggleButtonsState => _isToggleButtonsSelected; - UpdateToggleButtonsStateOnButtonPressedCallback - get updateToggleButtonsOnButtonPressed => - _updateToggleButtonsStateOnButtonPressed; - UpdateToggleButtonsStateOnSelectionChangedCallback - get updateToggleButtonsOnSelection => - _updateToggleButtonStateOnSelectionChanged; - - @override - bool updateShouldNotify(ToggleButtonsStateManager oldWidget) => - toggleButtonsState != oldWidget.toggleButtonsState; -} diff --git a/simplistic_editor/test/main_screen_test.dart b/simplistic_editor/test/main_screen_test.dart index 39e584895..c0ce80478 100644 --- a/simplistic_editor/test/main_screen_test.dart +++ b/simplistic_editor/test/main_screen_test.dart @@ -10,6 +10,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:simplistic_editor/basic_text_input_client.dart'; import 'package:simplistic_editor/main.dart'; +import 'package:simplistic_editor/text_editing_delta_history_view.dart'; void main() { testWidgets('Default main page shows all components', (tester) async {