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, ); } }