diff --git a/packages/devtools_app/lib/src/shared/console/console.dart b/packages/devtools_app/lib/src/shared/console/console.dart index 8112665d0b7..273d4d70b74 100644 --- a/packages/devtools_app/lib/src/shared/console/console.dart +++ b/packages/devtools_app/lib/src/shared/console/console.dart @@ -162,46 +162,55 @@ class _ConsoleOutputState extends State<_ConsoleOutput> child: Padding( padding: const EdgeInsets.symmetric(horizontal: denseSpacing), child: SelectionArea( - child: ListView.separated( - padding: const EdgeInsets.all(denseSpacing), - itemCount: _currentLines.length + (widget.footer != null ? 1 : 0), - controller: _scroll, - // Scroll physics to try to keep content within view and avoid bouncing. - physics: const ClampingScrollPhysics( - parent: RangeMaintainingScrollPhysics(), - ), - separatorBuilder: (_, _) { - return const PaddedDivider.noPadding(); - }, - itemBuilder: (context, index) { - if (index == _currentLines.length && widget.footer != null) { - return widget.footer!; - } - final line = _currentLines[index]; - if (line is TextConsoleLine) { - return Text.rich( - TextSpan( - // TODO(jacobr): consider caching the processed ansi terminal - // codes. - children: textSpansFromAnsi( - line.text, - theme.regularTextStyle, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: ListView.separated( + padding: const EdgeInsets.all(denseSpacing), + itemCount: _currentLines.length, + controller: _scroll, + // Scroll physics to try to keep content within view and avoid bouncing. + physics: const ClampingScrollPhysics( + parent: RangeMaintainingScrollPhysics(), ), - ); - } else if (line is VariableConsoleLine) { - return ExpandableVariable( - variable: line.variable, - isSelectable: false, - ); - } else { - assert( - false, - 'ConsoleLine of unsupported type ${line.runtimeType} encountered', - ); - return const SizedBox(); - } - }, + separatorBuilder: (_, _) { + return const PaddedDivider.noPadding(); + }, + itemBuilder: (context, index) { + final line = _currentLines[index]; + if (line is TextConsoleLine) { + return Text.rich( + TextSpan( + // TODO(jacobr): consider caching the processed ansi terminal + // codes. + children: textSpansFromAnsi( + line.text, + theme.regularTextStyle, + ), + ), + ); + } else if (line is VariableConsoleLine) { + return ExpandableVariable( + variable: line.variable, + isSelectable: false, + ); + } else { + assert( + false, + 'ConsoleLine of unsupported type ${line.runtimeType} encountered', + ); + return const SizedBox(); + } + }, + ), + ), + // consider constraining a max height. + Padding( + padding: const EdgeInsets.only(top: denseSpacing), + child: widget.footer!, + ), + ], ), ), ), diff --git a/packages/devtools_app/lib/src/shared/console/widgets/evaluate.dart b/packages/devtools_app/lib/src/shared/console/widgets/evaluate.dart index fcb8855526c..cc7142a8dfa 100644 --- a/packages/devtools_app/lib/src/shared/console/widgets/evaluate.dart +++ b/packages/devtools_app/lib/src/shared/console/widgets/evaluate.dart @@ -35,8 +35,6 @@ class ExpressionEvalField extends StatefulWidget { final AutoCompleteResultsFunction getAutoCompleteResults; - static const _evalFieldHeight = 32.0; - @override ExpressionEvalFieldState createState() => ExpressionEvalFieldState(); } @@ -218,46 +216,44 @@ class ExpressionEvalFieldState extends State return KeyEventResult.ignored; }, - child: SizedBox( - height: ExpressionEvalField._evalFieldHeight, - child: AutoCompleteSearchField( - controller: _autoCompleteController, - searchFieldEnabled: true, - shouldRequestFocus: false, - clearFieldOnEscapeWhenOverlayHidden: true, - onSelection: _onSelection, - decoration: InputDecoration( - contentPadding: const EdgeInsets.all(denseSpacing), - border: const OutlineInputBorder(), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide.none, - ), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide.none, - ), - labelText: 'Eval. Enter "?" for help.', - labelStyle: Theme.of(context).subtleTextStyle, + child: AutoCompleteSearchField( + controller: _autoCompleteController, + searchFieldEnabled: true, + shouldRequestFocus: false, + clearFieldOnEscapeWhenOverlayHidden: true, + onSelection: _onSelection, + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(denseSpacing), + border: const OutlineInputBorder(), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide.none, ), - overlayXPositionBuilder: ( - String inputValue, - TextStyle? inputStyle, - ) { - // X-coordinate is equivalent to the width of the input text - // up to the last "." or the insertion point (cursor): - final indexOfDot = inputValue.lastIndexOf('.'); - final textSegment = - indexOfDot != -1 - ? inputValue.substring(0, indexOfDot + 1) - : inputValue; - return calculateTextSpanWidth( - TextSpan(text: textSegment, style: inputStyle), - ); - }, - // Disable ligatures, so the suggestions of the auto complete work correcly. - style: Theme.of(context).fixedFontStyle.copyWith( - fontFeatures: [const FontFeature.disable('liga')], + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide.none, ), + labelText: 'Eval. Enter "?" for help.', + labelStyle: Theme.of(context).subtleTextStyle, + ), + overlayXPositionBuilder: ( + String inputValue, + TextStyle? inputStyle, + ) { + // X-coordinate is equivalent to the width of the input text + // up to the last "." or the insertion point (cursor): + final indexOfDot = inputValue.lastIndexOf('.'); + final textSegment = + indexOfDot != -1 + ? inputValue.substring(0, indexOfDot + 1) + : inputValue; + return calculateTextSpanWidth( + TextSpan(text: textSegment, style: inputStyle), + ); + }, + // Disable ligatures, so the suggestions of the auto complete work correcly. + style: Theme.of(context).fixedFontStyle.copyWith( + fontFeatures: [const FontFeature.disable('liga')], ), + maxLines: null, ), ), ), diff --git a/packages/devtools_app/lib/src/shared/ui/search.dart b/packages/devtools_app/lib/src/shared/ui/search.dart index cef7e7ca038..5df10ceff81 100644 --- a/packages/devtools_app/lib/src/shared/ui/search.dart +++ b/packages/devtools_app/lib/src/shared/ui/search.dart @@ -893,8 +893,11 @@ class SearchField extends StatefulWidget { this.onClose, this.searchFieldWidth = defaultSearchFieldWidth, double? searchFieldHeight, + int? maxLines = 1, super.key, - }) : searchFieldHeight = searchFieldHeight ?? defaultTextFieldHeight; + }) : assert(maxLines != 0, "'maxLines' must not be 0"), + searchFieldHeight = searchFieldHeight ?? defaultTextFieldHeight, + _maxLines = maxLines; final T searchController; @@ -917,6 +920,11 @@ class SearchField extends StatefulWidget { /// triggered. final VoidCallback? onClose; + /// The maximum number of lines, by default one. + /// + /// Can be set to null to remove the restriction; must not be zero. + final int? _maxLines; + @override State createState() => _SearchFieldState(); } @@ -928,18 +936,23 @@ class _SearchFieldState extends State @override Widget build(BuildContext context) { - return SizedBox( - width: widget.searchFieldWidth, - height: widget.searchFieldHeight, - child: StatelessSearchField( - controller: searchController, - searchFieldEnabled: widget.searchFieldEnabled, - shouldRequestFocus: widget.shouldRequestFocus, - supportsNavigation: widget.supportsNavigation, - onClose: widget.onClose, - searchFieldHeight: widget.searchFieldHeight, - ), + final searchField = StatelessSearchField( + controller: searchController, + searchFieldEnabled: widget.searchFieldEnabled, + shouldRequestFocus: widget.shouldRequestFocus, + supportsNavigation: widget.supportsNavigation, + onClose: widget.onClose, + searchFieldHeight: widget.searchFieldHeight, + maxLines: widget._maxLines, ); + + return widget._maxLines != 1 + ? searchField + : SizedBox( + width: widget.searchFieldWidth, + height: widget.searchFieldHeight, + child: searchField, + ); } } @@ -970,7 +983,9 @@ class StatelessSearchField this.suffix, this.style, this.searchFieldHeight, - }); + int? maxLines = 1, + }) : assert(maxLines != 0, "'maxLines' must not be 0"), + _maxLines = maxLines; final SearchControllerMixin controller; @@ -1014,6 +1029,11 @@ class StatelessSearchField final double? searchFieldHeight; + /// The maximum number of lines, by default one. + /// + /// Can be set to null to remove the restriction; must not be zero. + final int? _maxLines; + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -1032,6 +1052,7 @@ class StatelessSearchField focusNode: controller.searchFieldFocusNode, controller: controller.searchTextFieldController, style: textStyle, + maxLines: _maxLines, onChanged: onChanged, onEditingComplete: () { controller.searchFieldFocusNode?.requestFocus(); @@ -1116,7 +1137,9 @@ class AutoCompleteSearchField extends StatefulWidget { this.onFocusLost, this.style, this.keyEventsToIgnore = const {}, - }); + int? maxLines = 1, + }) : assert(maxLines != 0, "'maxLines' must not be 0"), + _maxLines = maxLines; final AutoCompleteSearchControllerMixin controller; @@ -1164,6 +1187,11 @@ class AutoCompleteSearchField extends StatefulWidget { /// [controller.autocompleteFocusNode] has lost focus. final VoidCallback? onFocusLost; + /// The maximum number of lines, by default one. + /// + /// Can be set to null to remove the restriction; must not be zero. + final int? _maxLines; + @override State createState() => _AutoCompleteSearchFieldState(); @@ -1214,6 +1242,7 @@ class _AutoCompleteSearchFieldState extends State }, onClose: widget.onClose, style: widget.style, + maxLines: widget._maxLines, ), ), ); diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 5347d868454..44054f9172d 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -40,6 +40,8 @@ TODO: Remove this section if there are not any general updates. * Updated syntax highlighting with support for digit separators, and improved comment and string interpolation handling. - [#8861](https://github.com/flutter/devtools/pull/8861) +* Added soft line wrapping in the debugger console. + [#8855](https://github.com/flutter/devtools/pull/8855). ## Network profiler updates