diff --git a/example/lib/main.dart b/example/lib/main.dart index 2c137c9..916c102 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -99,7 +99,6 @@ class _HomePageState extends State { TextButton.icon( style: TextButton.styleFrom( padding: EdgeInsets.symmetric(horizontal: 8.0), - primary: Colors.white, ), icon: Icon(FontAwesomeIcons.github), onPressed: () => diff --git a/lib/src/code_field/code_controller.dart b/lib/src/code_field/code_controller.dart index b749ee5..16aedd3 100644 --- a/lib/src/code_field/code_controller.dart +++ b/lib/src/code_field/code_controller.dart @@ -86,7 +86,8 @@ class CodeController extends TextEditingController { patternList.addAll(patternMap!.keys.map((e) => '($e)')); _styleList.addAll(patternMap!.values); } - _styleRegExp = RegExp(patternList.join('|'), multiLine: true, unicode: true); + _styleRegExp = + RegExp(patternList.join('|'), multiLine: true, unicode: true); } /// Sets a specific cursor position in the text @@ -305,24 +306,18 @@ class CodeController extends TextEditingController { } CodeController copyWith({ - Mode? _language, - CodeAutoComplete? autoComplete, + Mode? language, Map? patternMap, Map? stringMap, EditorParams? params, List? modifiers, - String? _languageId, - RegExp? _styleRegExp, }) { return CodeController( - _language: _language ?? this._language, - autoComplete: autoComplete ?? this.autoComplete, + language: language ?? this.language, patternMap: patternMap ?? this.patternMap, stringMap: stringMap ?? this.stringMap, params: params ?? this.params, modifiers: modifiers ?? this.modifiers, - _languageId: _languageId ?? this._languageId, - _styleRegExp: _styleRegExp ?? this._styleRegExp, ); } } diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index 35a4d29..21e9b6a 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -17,7 +17,7 @@ class CodeField extends StatefulWidget { /// {@macro flutter.widgets.textField.smartQuotesType} final SmartQuotesType? smartQuotesType; -/// {@macro flutter.widgets.textField.smartDashesType} + /// {@macro flutter.widgets.textField.smartDashesType} final SmartDashesType? smartDashesType; /// {@macro flutter.widgets.textField.keyboardType} @@ -64,13 +64,13 @@ class CodeField extends StatefulWidget { /// {@macro flutter.widgets.textField.selectionControls} final TextSelectionControls? selectionControls; + final Key? fieldKey; /// {@macro flutter.widgets.textField.textInputAction} final TextInputAction? textInputAction; /// {@macro flutter.services.TextInputConfiguration.enableSuggestions} final bool enableSuggestions; - final Color? background; final EdgeInsets padding; final Decoration? decoration; @@ -86,6 +86,7 @@ class CodeField extends StatefulWidget { const CodeField({ Key? key, required this.controller, + this.fieldKey, this.minLines, this.maxLines, this.expands = false, @@ -132,6 +133,10 @@ class _CodeFieldState extends State { FocusNode? _focusNode; String? lines; String longestLine = ''; + // Track the available width of the code column in order to compute + // visual line wrapping for line-numbers. + double _codeColumnWidth = 0; + var _currentTextStyle = const TextStyle(); @override void initState() { @@ -186,8 +191,34 @@ class _CodeFieldState extends State { final str = widget.controller.text.split('\n'); final buf = []; - for (var k = 0; k < str.length; k++) { - buf.add((k + 1).toString()); + if (widget.wrap && _codeColumnWidth > 0) { + // When wrapping is enabled we need to account for visual lines that + // are created by the soft wrap. We measure each logical line using a + // TextPainter and add blank placeholders so the scrolling offset of + // the linked controllers stays in sync. + for (var k = 0; k < str.length; k++) { + final logicalLine = str[k]; + + // Compute how many visual lines this logical line occupies. + final tp = TextPainter( + text: TextSpan(text: logicalLine, style: _currentTextStyle), + textDirection: TextDirection.ltr, + )..layout(maxWidth: _codeColumnWidth); + final visualLines = tp.computeLineMetrics().length; + + // First visual line gets the real line number. + buf.add((k + 1).toString()); + + // Remaining visual lines get an empty placeholder so that the + // total number of lines matches the code field. + for (int v = 1; v < visualLines; v++) { + buf.add(''); + } + } + } else { + for (var k = 0; k < str.length; k++) { + buf.add((k + 1).toString()); + } } _numberController?.text = buf.join('\n'); @@ -325,6 +356,7 @@ class _CodeFieldState extends State { } final codeField = TextField( + key: widget.fieldKey, keyboardType: widget.keyboardType, smartQuotesType: widget.smartQuotesType, smartDashesType: widget.smartDashesType, @@ -369,7 +401,7 @@ class _CodeFieldState extends State { textSelectionTheme: widget.textSelectionTheme, ), child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { + builder: (context, constraints) { // Control horizontal scrolling return widget.wrap ? codeField @@ -386,7 +418,34 @@ class _CodeFieldState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.lineNumbers && numberCol != null) numberCol, - Expanded(child: codeCol), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + // Save the textStyle and available width so _onTextChanged can + // accurately compute the amount of visual lines when wrapping. + _currentTextStyle = textStyle; + // Subtract left padding that will be applied inside the + // SingleChildScrollView when horizontal scrolling is disabled. + var availableWidth = constraints.maxWidth; + + const caretMargin = 3.0; // 1 gap + 2 cursor + availableWidth -= caretMargin; + + if ((availableWidth - _codeColumnWidth).abs() > 0.5) { + _codeColumnWidth = max(0, availableWidth); + print('availableWidth: $availableWidth'); + // Recalculate the line numbers because wrapping may have + // changed due to a width change. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _onTextChanged(); + } + }); + } + return codeCol; + }, + ), + ), ], ), ); diff --git a/lib/src/line_numbers/line_number_controller.dart b/lib/src/line_numbers/line_number_controller.dart index e5ce6bb..8ac2c30 100644 --- a/lib/src/line_numbers/line_number_controller.dart +++ b/lib/src/line_numbers/line_number_controller.dart @@ -18,14 +18,19 @@ class LineNumberController extends TextEditingController { for (int k = 0; k < list.length; k++) { final el = list[k]; - final number = int.parse(el); - var textSpan = TextSpan(text: el, style: style); - - if (lineNumberBuilder != null) { - textSpan = lineNumberBuilder!(number, style); + // Blank lines are placeholders inserted to align with wrapped + // visual lines. They should render as empty text spans to + // preserve vertical spacing. + if (el.trim().isEmpty) { + children.add(TextSpan(text: '', style: style)); + } else { + final number = int.tryParse(el) ?? 0; + var textSpan = TextSpan(text: el, style: style); + if (lineNumberBuilder != null && number != 0) { + textSpan = lineNumberBuilder!(number, style); + } + children.add(textSpan); } - - children.add(textSpan); if (k < list.length - 1) { children.add(const TextSpan(text: '\n')); }