diff --git a/lib/frontend/cache.dart b/lib/frontend/cache.dart index aeffc09d..8cd6abe6 100644 --- a/lib/frontend/cache.dart +++ b/lib/frontend/cache.dart @@ -18,7 +18,7 @@ class TextLayoutCache { return _cache[key]; } - Paragraph performAndCacheLayout(String text, TextStyle style, int key) { + Paragraph performAndCacheLayout(String text, TextStyle style, int? key) { final builder = ParagraphBuilder(style.getParagraphStyle()); builder.pushStyle(style.getTextStyle()); builder.addText(text); @@ -26,7 +26,9 @@ class TextLayoutCache { final paragraph = builder.build(); paragraph.layout(ParagraphConstraints(width: double.infinity)); - _cache[key] = paragraph; + if (key != null) { + _cache[key] = paragraph; + } return paragraph; } diff --git a/lib/frontend/input_behavior_default.dart b/lib/frontend/input_behavior_default.dart index 885f9ad3..f7306d5f 100644 --- a/lib/frontend/input_behavior_default.dart +++ b/lib/frontend/input_behavior_default.dart @@ -5,7 +5,7 @@ import 'package:xterm/frontend/input_map.dart'; import 'package:xterm/xterm.dart'; class InputBehaviorDefault extends InputBehavior { - const InputBehaviorDefault(); + InputBehaviorDefault(); @override bool get acceptKeyStroke => true; @@ -32,11 +32,28 @@ class InputBehaviorDefault extends InputBehavior { } } + String? _composingString = null; + @override TextEditingValue? onTextEdit( TextEditingValue value, TerminalUiInteraction terminal) { - terminal.raiseOnInput(value.text); - if (value == TextEditingValue.empty) { + var inputText = value.text; + // we just want to detect if a composing is going on and notify the terminal + // about it + if (value.composing.start != value.composing.end) { + _composingString = inputText; + terminal.updateComposingString(_composingString!); + return null; + } + //when we reach this point the composing state is over + if (_composingString != null) { + _composingString = null; + terminal.updateComposingString(''); + } + + terminal.raiseOnInput(inputText); + + if (value == TextEditingValue.empty || inputText == '') { return null; } else { return TextEditingValue.empty; diff --git a/lib/frontend/input_behavior_desktop.dart b/lib/frontend/input_behavior_desktop.dart index 0da00c91..226d387c 100644 --- a/lib/frontend/input_behavior_desktop.dart +++ b/lib/frontend/input_behavior_desktop.dart @@ -1,5 +1,5 @@ import 'package:xterm/frontend/input_behavior_default.dart'; class InputBehaviorDesktop extends InputBehaviorDefault { - const InputBehaviorDesktop(); + InputBehaviorDesktop(); } diff --git a/lib/frontend/input_behavior_mobile.dart b/lib/frontend/input_behavior_mobile.dart index 957e872a..aa78c400 100644 --- a/lib/frontend/input_behavior_mobile.dart +++ b/lib/frontend/input_behavior_mobile.dart @@ -5,7 +5,7 @@ import 'package:xterm/input/keys.dart'; import 'package:xterm/xterm.dart'; class InputBehaviorMobile extends InputBehaviorDefault { - const InputBehaviorMobile(); + InputBehaviorMobile(); final acceptKeyStroke = false; diff --git a/lib/frontend/input_behaviors.dart b/lib/frontend/input_behaviors.dart index 5f4ad586..ff789774 100644 --- a/lib/frontend/input_behaviors.dart +++ b/lib/frontend/input_behaviors.dart @@ -4,9 +4,9 @@ import 'package:xterm/frontend/input_behavior_desktop.dart'; import 'package:xterm/frontend/input_behavior_mobile.dart'; class InputBehaviors { - static const desktop = InputBehaviorDesktop(); + static final desktop = InputBehaviorDesktop(); - static const mobile = InputBehaviorMobile(); + static final mobile = InputBehaviorMobile(); static InputBehavior get platform { if (Platform.I.isMobile) { diff --git a/lib/frontend/input_listener.dart b/lib/frontend/input_listener.dart index 8cf8e0e4..e0b75ed6 100644 --- a/lib/frontend/input_listener.dart +++ b/lib/frontend/input_listener.dart @@ -10,6 +10,7 @@ typedef FocusHandler = void Function(bool); abstract class InputListenerController { void requestKeyboard(); + void setCaretRect(Rect rect); } class InputListener extends StatefulWidget { @@ -123,6 +124,7 @@ class InputListenerState extends State ); } + @override void requestKeyboard() { if (widget.focusNode.hasFocus) { openInputConnection(); @@ -131,6 +133,11 @@ class InputListenerState extends State } } + @override + void setCaretRect(Rect rect) { + _conn?.setCaretRect(rect); + } + void onFocusChange() { if (widget.onFocus != null) { widget.onFocus?.call(widget.focusNode.hasFocus); @@ -184,6 +191,8 @@ class InputListenerState extends State if (newValue != null) { _conn?.setEditingState(newValue); + } else { + _conn?.setEditingState(TextEditingValue.empty); } } @@ -211,14 +220,9 @@ class TerminalTextInputClient extends TextInputClient { void updateEditingValue(TextEditingValue value) { // print('updateEditingValue $value'); - onInput(value); - - // if (_savedValue == null || _savedValue.text == '') { - // onInput(value.text); - // } else if (_savedValue.text.length < value.text.length) { - // final diff = value.text.substring(_savedValue.text.length); - // onInput(diff); - // } + if (value.text != '') { + onInput(value); + } _savedValue = value; // print('updateEditingValue $value'); diff --git a/lib/frontend/oscillator.dart b/lib/frontend/oscillator.dart index 747664cf..fbf4005b 100644 --- a/lib/frontend/oscillator.dart +++ b/lib/frontend/oscillator.dart @@ -36,7 +36,13 @@ class Oscillator with Observable { return _value; } + void restart() { + stop(); + start(); + } + void start() { + _value = true; _shouldRun = true; // only start right away when anyone is listening. // the moment a listener gets registered the Oscillator will start diff --git a/lib/frontend/terminal_painters.dart b/lib/frontend/terminal_painters.dart index 218a7e4d..12a22d10 100644 --- a/lib/frontend/terminal_painters.dart +++ b/lib/frontend/terminal_painters.dart @@ -177,35 +177,13 @@ class TerminalPainter extends CustomPainter { color = color.withOpacity(0.5); } - final styleToUse = (style.textStyleProvider != null) - ? style.textStyleProvider!( - color: color, - fontSize: style.fontSize, - fontWeight: flags.hasFlag(CellFlags.bold) - ? FontWeight.bold - : FontWeight.normal, - fontStyle: flags.hasFlag(CellFlags.italic) - ? FontStyle.italic - : FontStyle.normal, - decoration: flags.hasFlag(CellFlags.underline) - ? TextDecoration.underline - : TextDecoration.none, - ) - : TextStyle( - color: color, - fontSize: style.fontSize, - fontWeight: flags.hasFlag(CellFlags.bold) - ? FontWeight.bold - : FontWeight.normal, - fontStyle: flags.hasFlag(CellFlags.italic) - ? FontStyle.italic - : FontStyle.normal, - decoration: flags.hasFlag(CellFlags.underline) - ? TextDecoration.underline - : TextDecoration.none, - fontFamily: 'monospace', - fontFamilyFallback: style.fontFamily, - ); + final styleToUse = PaintHelper.getStyleToUse( + style, + color, + bold: flags.hasFlag(CellFlags.bold), + italic: flags.hasFlag(CellFlags.italic), + underline: flags.hasFlag(CellFlags.underline), + ); character = textLayoutCache.performAndCacheLayout( String.fromCharCode(codePoint), styleToUse, cellHash); @@ -226,6 +204,10 @@ class CursorPainter extends CustomPainter { final bool focused; final bool blinkVisible; final int cursorColor; + final int textColor; + final String composingString; + final TextLayoutCache textLayoutCache; + final TerminalStyle style; CursorPainter({ required this.visible, @@ -233,11 +215,16 @@ class CursorPainter extends CustomPainter { required this.focused, required this.blinkVisible, required this.cursorColor, + required this.textColor, + required this.composingString, + required this.textLayoutCache, + required this.style, }); @override void paint(Canvas canvas, Size size) { - if (blinkVisible && visible) { + bool isVisible = visible && (blinkVisible || composingString != ''); + if (isVisible) { _paintCursor(canvas); } } @@ -249,7 +236,8 @@ class CursorPainter extends CustomPainter { focused != oldDelegate.focused || visible != oldDelegate.visible || charSize.cellWidth != oldDelegate.charSize.cellWidth || - charSize.cellHeight != oldDelegate.charSize.cellHeight; + charSize.cellHeight != oldDelegate.charSize.cellHeight || + composingString != oldDelegate.composingString; } return true; } @@ -262,5 +250,42 @@ class CursorPainter extends CustomPainter { canvas.drawRect( Rect.fromLTWH(0, 0, charSize.cellWidth, charSize.cellHeight), paint); + + if (composingString != '') { + final styleToUse = PaintHelper.getStyleToUse(style, Color(textColor)); + final character = textLayoutCache.performAndCacheLayout( + composingString, styleToUse, null); + canvas.drawParagraph(character, Offset(0, 0)); + } + } +} + +class PaintHelper { + static TextStyle getStyleToUse( + TerminalStyle style, + Color color, { + bool bold = false, + bool italic = false, + bool underline = false, + }) { + return (style.textStyleProvider != null) + ? style.textStyleProvider!( + color: color, + fontSize: style.fontSize, + fontWeight: bold ? FontWeight.bold : FontWeight.normal, + fontStyle: italic ? FontStyle.italic : FontStyle.normal, + decoration: + underline ? TextDecoration.underline : TextDecoration.none, + ) + : TextStyle( + color: color, + fontSize: style.fontSize, + fontWeight: bold ? FontWeight.bold : FontWeight.normal, + fontStyle: italic ? FontStyle.italic : FontStyle.normal, + decoration: + underline ? TextDecoration.underline : TextDecoration.none, + fontFamily: 'monospace', + fontFamilyFallback: style.fontFamily, + ); } } diff --git a/lib/frontend/terminal_view.dart b/lib/frontend/terminal_view.dart index 313a228e..bffd26a8 100644 --- a/lib/frontend/terminal_view.dart +++ b/lib/frontend/terminal_view.dart @@ -154,6 +154,8 @@ class _TerminalViewState extends State { super.dispose(); } + GlobalKey _keyCursor = GlobalKey(); + @override Widget build(BuildContext context) { return InputListener( @@ -171,6 +173,22 @@ class _TerminalViewState extends State { onWidgetSize(constraints.maxWidth - widget.padding * 2, constraints.maxHeight - widget.padding * 2); + if (_keyCursor.currentContext != null) { + /// this gets set so that the accent selection menu on MacOS pops up + /// at the right spot + final RenderBox cursorRenderObj = + _keyCursor.currentContext!.findRenderObject() as RenderBox; + final offset = cursorRenderObj.localToGlobal(Offset.zero); + InputListener.of(context)!.setCaretRect( + Rect.fromLTWH( + offset.dx, + offset.dy, + _cellSize.cellWidth, + _cellSize.cellHeight, + ), + ); + } + // use flutter's Scrollable to manage scrolling to better integrate // with widgets such as Scrollbar. return NotificationListener( @@ -282,11 +300,14 @@ class _TerminalViewState extends State { ), ), Positioned( + key: _keyCursor, child: CursorView( terminal: widget.terminal, cellSize: _cellSize, focusNode: widget.focusNode, blinkOscillator: blinkOscillator, + style: widget.style, + textLayoutCache: textLayoutCache, ), width: _cellSize.cellWidth, height: _cellSize.cellHeight, @@ -367,6 +388,7 @@ class _TerminalViewState extends State { } void onKeyStroke(RawKeyEvent event) { + blinkOscillator.restart(); // TODO: find a way to stop scrolling immediately after key stroke. widget.inputBehavior.onKeyStroke(event, widget.terminal); widget.terminal.setScrollOffsetFromBottom(0); @@ -395,11 +417,16 @@ class CursorView extends StatefulWidget { final TerminalUiInteraction terminal; final FocusNode? focusNode; final Oscillator blinkOscillator; + final TerminalStyle style; + final TextLayoutCache textLayoutCache; + CursorView({ required this.terminal, required this.cellSize, required this.focusNode, required this.blinkOscillator, + required this.style, + required this.textLayoutCache, }); @override @@ -432,6 +459,10 @@ class _CursorViewState extends State { charSize: widget.cellSize, blinkVisible: widget.blinkOscillator.value, cursorColor: widget.terminal.cursorColor, + textColor: widget.terminal.backgroundColor, + style: widget.style, + composingString: widget.terminal.composingString, + textLayoutCache: widget.textLayoutCache, ), ); } diff --git a/lib/terminal/terminal.dart b/lib/terminal/terminal.dart index 3d49600e..781f15a2 100644 --- a/lib/terminal/terminal.dart +++ b/lib/terminal/terminal.dart @@ -722,4 +722,15 @@ class Terminal with Observable implements TerminalUiInteraction { _selection.init(Position(0, 0)); _selection.update(Position(terminalWidth, bufferHeight)); } + + String _composingString = ''; + + @override + String get composingString => _composingString; + + @override + void updateComposingString(String value) { + _composingString = value; + refresh(); + } } diff --git a/lib/terminal/terminal_isolate.dart b/lib/terminal/terminal_isolate.dart index f89faf79..c6cbf87a 100644 --- a/lib/terminal/terminal_isolate.dart +++ b/lib/terminal/terminal_isolate.dart @@ -31,7 +31,8 @@ enum _IsolateCommand { keyInput, requestNewStateWhenDirty, paste, - terminateBackend + terminateBackend, + updateComposingString } enum _IsolateEvent { @@ -141,6 +142,7 @@ void terminalMain(SendPort port) async { _terminal.showCursor, _terminal.theme.cursor, _terminal.getVisibleLines(), + _terminal.composingString, ); port.send([_IsolateEvent.newState, newState]); _needNotify = true; @@ -151,6 +153,10 @@ void terminalMain(SendPort port) async { break; case _IsolateCommand.terminateBackend: _terminal?.terminateBackend(); + break; + case _IsolateCommand.updateComposingString: + _terminal?.updateComposingString(msg[1]); + break; } } } @@ -194,6 +200,8 @@ class TerminalState { bool consumed = false; + String composingString; + TerminalState( this.scrollOffsetFromBottom, this.scrollOffsetFromTop, @@ -209,6 +217,7 @@ class TerminalState { this.showCursor, this.cursorColor, this.visibleLines, + this.composingString, ); } @@ -509,4 +518,12 @@ class TerminalIsolate with Observable implements TerminalUiInteraction { @override bool get isTerminated => _isTerminated; + + @override + String get composingString => _lastState?.composingString ?? ''; + + @override + void updateComposingString(String value) { + _sendPort?.send([_IsolateCommand.updateComposingString, value]); + } } diff --git a/lib/terminal/terminal_ui_interaction.dart b/lib/terminal/terminal_ui_interaction.dart index 1271efd6..9faba7c0 100644 --- a/lib/terminal/terminal_ui_interaction.dart +++ b/lib/terminal/terminal_ui_interaction.dart @@ -122,4 +122,11 @@ abstract class TerminalUiInteraction with Observable { /// flag that indicates if the backend is already terminated bool get isTerminated; + + /// returns the current composing string. '' when there is no composing going on + String get composingString; + + /// update the composing string. This gets called by the input handling + /// part of the terminal + void updateComposingString(String value); }