diff --git a/super_editor/example/lib/main_super_text_field.dart b/super_editor/example/lib/main_super_text_field.dart index 1575b1e8f6..837a8dc986 100644 --- a/super_editor/example/lib/main_super_text_field.dart +++ b/super_editor/example/lib/main_super_text_field.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:super_editor/super_editor.dart'; import 'package:super_editor/super_text_field.dart'; @@ -6,11 +7,17 @@ import 'package:super_editor/super_text_field.dart'; void main() { runApp( MaterialApp( + debugShowCheckedModeBanner: false, home: _SuperTextFieldDemo(), ), ); } +const _headingTextStyle = TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, +); + class _SuperTextFieldDemo extends StatefulWidget { const _SuperTextFieldDemo(); @@ -19,23 +26,100 @@ class _SuperTextFieldDemo extends StatefulWidget { } class _SuperTextFieldDemoState extends State<_SuperTextFieldDemo> { + final _darkBackground = const Color(0xFF222222); + final _lightBackground = Colors.white; + final _brightness = ValueNotifier(Brightness.light); + @override Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 500), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _SingleLineTextField(), - const SizedBox(height: 16), - _MultiLineTextField(), - ], - ), - ), - ), + return ValueListenableBuilder( + valueListenable: _brightness, + builder: (context, brightness, child) { + return Theme( + data: ThemeData(brightness: brightness), + child: child!, + ); + }, + child: Builder(builder: (context) { + final isLight = Theme.of(context).brightness == Brightness.light; + + return Scaffold( + appBar: AppBar( + title: Text('SuperTextField Demo'), + ), + body: ColoredBox( + color: isLight ? _lightBackground : _darkBackground, + child: Stack( + children: [ + Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 500), + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + children: [ + Text( + 'Single Line SuperTextField', + style: _headingTextStyle, + ), + const SizedBox(height: 8), + _SingleLineTextField(), + const SizedBox(height: 16), + Text( + 'Multi Line SuperTextField', + style: _headingTextStyle, + ), + const SizedBox(height: 8), + _MultiLineTextField(), + const SizedBox(height: 16), + Text( + 'Dynamic SuperTextField', + style: _headingTextStyle, + ), + const SizedBox(height: 8), + _DynamicTextField(), + ], + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: ListenableBuilder( + listenable: _brightness, + builder: (context, child) { + return child!; + }, + child: _buildCornerFabs(), + ), + ), + ], + ), + )); + }), + ); + } + + Widget _buildCornerFabs() { + return Padding( + padding: const EdgeInsets.only(right: 16, bottom: 16), + child: _buildLightAndDarkModeToggle(), + ); + } + + Widget _buildLightAndDarkModeToggle() { + return FloatingActionButton( + backgroundColor: _brightness.value == Brightness.light ? _darkBackground : _lightBackground, + foregroundColor: _brightness.value == Brightness.light ? _lightBackground : _darkBackground, + elevation: 5, + onPressed: () { + _brightness.value = _brightness.value == Brightness.light ? Brightness.dark : Brightness.light; + }, + child: _brightness.value == Brightness.light + ? const Icon( + Icons.dark_mode, + ) + : const Icon( + Icons.light_mode, + ), ); } } @@ -62,6 +146,8 @@ class _SingleLineTextFieldState extends State<_SingleLineTextField> { @override Widget build(BuildContext context) { + final isLight = Theme.of(context).brightness == Brightness.light; + return TapRegion( groupId: "textfields", onTapOutside: (_) => _focusNode.unfocus(), @@ -71,7 +157,8 @@ class _SingleLineTextFieldState extends State<_SingleLineTextField> { child: SuperTextField( focusNode: _focusNode, textController: _textController, - textStyleBuilder: _textStyleBuilder, + controlsColor: isLight ? Colors.black : Colors.white, + textStyleBuilder: (_) => _textStyleBuilder(_, isLight), hintBuilder: _createHintBuilder("Enter single line text..."), padding: const EdgeInsets.all(4), minLines: 1, @@ -105,6 +192,8 @@ class _MultiLineTextFieldState extends State<_MultiLineTextField> { @override Widget build(BuildContext context) { + final isLight = Theme.of(context).brightness == Brightness.light; + return TapRegion( groupId: "textfields", onTapOutside: (_) => _focusNode.unfocus(), @@ -113,8 +202,9 @@ class _MultiLineTextFieldState extends State<_MultiLineTextField> { borderBuilder: _borderBuilder, child: SuperTextField( focusNode: _focusNode, + controlsColor: isLight ? Colors.black : Colors.white, textController: _textController, - textStyleBuilder: _textStyleBuilder, + textStyleBuilder: (_) => _textStyleBuilder(_, isLight), hintBuilder: _createHintBuilder("Type some text..."), padding: const EdgeInsets.all(4), minLines: 5, @@ -126,7 +216,7 @@ class _MultiLineTextFieldState extends State<_MultiLineTextField> { } } -BoxDecoration _borderBuilder(TextFieldBorderState borderState) { +BoxDecoration _borderBuilder(TextFieldBorderState borderState, {Color? borderColor}) { return BoxDecoration( borderRadius: BorderRadius.circular(4), border: Border.all( @@ -134,15 +224,15 @@ BoxDecoration _borderBuilder(TextFieldBorderState borderState) { ? Colors.red : borderState.hasFocus ? Colors.blue - : Colors.grey.shade300, + : (borderColor ?? Colors.grey.shade300), width: borderState.hasError ? 2 : 1, ), ); } -TextStyle _textStyleBuilder(Set attributions) { +TextStyle _textStyleBuilder(Set attributions, bool isLight) { return defaultTextFieldStyleBuilder(attributions).copyWith( - color: Colors.black, + color: isLight ? Colors.black : Colors.white, ); } @@ -154,3 +244,160 @@ WidgetBuilder _createHintBuilder(String hintText) { ); }; } + +class _DynamicTextField extends StatefulWidget { + const _DynamicTextField({super.key}); + + @override + State<_DynamicTextField> createState() => __DynamicTextFieldState(); +} + +class __DynamicTextFieldState extends State<_DynamicTextField> { + final _focusNode = FocusNode(); + final _textController = ImeAttributedTextEditingController( + controller: AttributedTextEditingController(), + ); + int _minLines = 1; + int _maxLines = 1; + final _formKey = GlobalKey(); + final _maxController = TextEditingController(text: '1'); + final _minController = TextEditingController(text: '1'); + + Color _borderColor = Colors.grey; + + @override + void dispose() { + _textController.dispose(); + _focusNode.dispose(); + _maxController.dispose(); + _minController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isLight = Theme.of(context).brightness == Brightness.light; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TapRegion( + groupId: "textfields", + onTapOutside: (_) => _focusNode.unfocus(), + child: TextFieldBorder( + focusNode: _focusNode, + borderBuilder: (_) => _borderBuilder(_, borderColor: _borderColor), + child: SuperTextField( + focusNode: _focusNode, + textController: _textController, + controlsColor: isLight ? Colors.black : Colors.white, + textStyleBuilder: (_) => _textStyleBuilder(_, isLight), + hintBuilder: _createHintBuilder("Enter single line text..."), + padding: const EdgeInsets.all(4), + minLines: _minLines, + maxLines: _maxLines, + inputSource: TextInputSource.ime, + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Change below parameters', + style: _headingTextStyle, + ), + const SizedBox(height: 8), + Form( + key: _formKey, + child: Column( + children: [ + _linesSelector('Min Lines', _minController), + const SizedBox(height: 8), + _linesSelector('Max Lines', _maxController), + ], + ), + ), + const SizedBox(height: 8), + Text( + 'Change border color', + style: _headingTextStyle, + ), + const SizedBox(height: 8), + Wrap( + children: [Colors.blue, Colors.green, Colors.orange, Colors.red].map((color) => _colorTile(color)).toList(), + ), + ], + ); + } + + Widget _colorTile(Color color) { + return GestureDetector( + onTap: () { + setState(() { + _borderColor = color; + }); + }, + child: Container( + height: 40, + width: 40, + margin: const EdgeInsets.symmetric(horizontal: 4), + color: color, + ), + ); + } + + Row _linesSelector(String title, TextEditingController controller) { + return Row( + children: [ + Text(title), + const SizedBox(width: 8), + Expanded( + child: _numberTextField(controller), + ), + ], + ); + } + + TextFormField _numberTextField(TextEditingController controller) { + return TextFormField( + controller: controller, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.fromLTRB(8, 4, 8, 4), + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.grey), + ), + ), + validator: (userInput) { + if (userInput == null) { + return 'Invalid number'; + } + + int value = int.parse(userInput); + if (value <= 0 || value > 5) { + return 'Please select a value between (1, 5)'; + } + + int min = int.parse(_minController.text); + int max = int.parse(_maxController.text); + + if (min > max) { + return 'Min lines cannot be greater than max lines'; + } + + return null; + }, + onChanged: (userInput) { + if (_formKey.currentState!.validate()) { + setState(() { + _minLines = int.parse(_minController.text); + _maxLines = int.parse(_maxController.text); + }); + } + }, + ); + } +}