Stabilize terminal resizing around input mode changes

This commit is contained in:
sladro 2026-04-10 20:40:46 +08:00
parent 43e7dc61f7
commit 0f72bc207a
2 changed files with 75 additions and 6 deletions

View File

@ -46,6 +46,7 @@ class TerminalPage extends ConsumerStatefulWidget {
class _TerminalPageState extends ConsumerState<TerminalPage> class _TerminalPageState extends ConsumerState<TerminalPage>
with WidgetsBindingObserver { with WidgetsBindingObserver {
static const Duration _historySeedDelay = Duration(milliseconds: 120); static const Duration _historySeedDelay = Duration(milliseconds: 120);
static const Duration _terminalResizeSettleDelay = Duration(milliseconds: 240);
static const List<_QuickTerminalKey> _editingControlKeys = [ static const List<_QuickTerminalKey> _editingControlKeys = [
_QuickTerminalKey(keyId: 'esc', label: 'Esc', input: '\u001b'), _QuickTerminalKey(keyId: 'esc', label: 'Esc', input: '\u001b'),
_QuickTerminalKey(keyId: 'tab', label: 'Tab', input: '\t'), _QuickTerminalKey(keyId: 'tab', label: 'Tab', input: '\t'),
@ -147,6 +148,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
late final TerminalSnapshotStorage _snapshotStorage; late final TerminalSnapshotStorage _snapshotStorage;
late final Listenable _pageStateListenable; late final Listenable _pageStateListenable;
Timer? _historySeedTimer; Timer? _historySeedTimer;
Timer? _terminalResizeSettleTimer;
Timer? _snapshotPersistTimer; Timer? _snapshotPersistTimer;
String? _pendingHistorySeed; String? _pendingHistorySeed;
bool _receivedSocketFrame = false; bool _receivedSocketFrame = false;
@ -156,6 +158,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
bool _awaitingReconnectRestore = false; bool _awaitingReconnectRestore = false;
bool _shouldReconnectOnResume = false; bool _shouldReconnectOnResume = false;
bool _showExpandedControls = false; bool _showExpandedControls = false;
bool _terminalAutoResizeEnabled = true;
_TerminalInputMode _inputMode = _TerminalInputMode.read; _TerminalInputMode _inputMode = _TerminalInputMode.read;
TerminalConnectionState? _lastConnectionState; TerminalConnectionState? _lastConnectionState;
@ -189,7 +192,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
_diagnosticLog.add('ui.terminal.key', normalizedInput); _diagnosticLog.add('ui.terminal.key', normalizedInput);
_coordinator.sendInput(normalizedInput); _coordinator.sendInput(normalizedInput);
}; };
_applyInputMode(); _applyInputMode(freezeResize: false);
unawaited(_bootstrapTerminal()); unawaited(_bootstrapTerminal());
} }
@ -198,6 +201,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_pageStateListenable.removeListener(_handlePageStateChanged); _pageStateListenable.removeListener(_handlePageStateChanged);
_historySeedTimer?.cancel(); _historySeedTimer?.cancel();
_terminalResizeSettleTimer?.cancel();
_snapshotPersistTimer?.cancel(); _snapshotPersistTimer?.cancel();
unawaited(_persistTerminalSnapshot()); unawaited(_persistTerminalSnapshot());
_terminalFocusNode.dispose(); _terminalFocusNode.dispose();
@ -226,6 +230,17 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
} }
} }
@override
void didChangeMetrics() {
if (_inputMode == _TerminalInputMode.edit) {
_cancelTerminalResizeSettle();
_setTerminalAutoResizeEnabled(false);
return;
}
_freezeTerminalResizeUntilSettled();
}
Future<void> _openSiblingTerminal() async { Future<void> _openSiblingTerminal() async {
final project = widget.project; final project = widget.project;
if (project == null) { if (project == null) {
@ -329,8 +344,17 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
_applyInputMode(); _applyInputMode();
} }
void _applyInputMode() { void _applyInputMode({bool freezeResize = true}) {
_coordinator.setBackendResizeEnabled(_inputMode != _TerminalInputMode.edit); _coordinator.setBackendResizeEnabled(_inputMode != _TerminalInputMode.edit);
if (_inputMode == _TerminalInputMode.edit) {
_cancelTerminalResizeSettle();
_setTerminalAutoResizeEnabled(false);
} else if (freezeResize) {
_freezeTerminalResizeUntilSettled();
} else {
_cancelTerminalResizeSettle();
_setTerminalAutoResizeEnabled(true);
}
_terminalFocusNode.canRequestFocus = _inputMode == _TerminalInputMode.edit; _terminalFocusNode.canRequestFocus = _inputMode == _TerminalInputMode.edit;
if (_inputMode == _TerminalInputMode.edit) { if (_inputMode == _TerminalInputMode.edit) {
_terminalFocusNode.requestFocus(); _terminalFocusNode.requestFocus();
@ -340,6 +364,34 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
_terminalFocusNode.unfocus(); _terminalFocusNode.unfocus();
} }
void _freezeTerminalResizeUntilSettled() {
_setTerminalAutoResizeEnabled(false);
_terminalResizeSettleTimer?.cancel();
_terminalResizeSettleTimer = Timer(_terminalResizeSettleDelay, () {
_terminalResizeSettleTimer = null;
if (!mounted || _inputMode == _TerminalInputMode.edit) {
return;
}
_setTerminalAutoResizeEnabled(true);
});
}
void _cancelTerminalResizeSettle() {
_terminalResizeSettleTimer?.cancel();
_terminalResizeSettleTimer = null;
}
void _setTerminalAutoResizeEnabled(bool enabled) {
if (_terminalAutoResizeEnabled == enabled || !mounted) {
return;
}
setState(() {
_terminalAutoResizeEnabled = enabled;
});
}
void _handleTerminalSurfaceTap() { void _handleTerminalSurfaceTap() {
if (_inputMode != _TerminalInputMode.edit) { if (_inputMode != _TerminalInputMode.edit) {
return; return;
@ -721,7 +773,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
controller: _terminalViewController, controller: _terminalViewController,
focusNode: _terminalFocusNode, focusNode: _terminalFocusNode,
autofocus: false, autofocus: false,
autoResize: false, autoResize: _terminalAutoResizeEnabled,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
deleteDetection: true, deleteDetection: true,
readOnly: _inputMode == _TerminalInputMode.read, readOnly: _inputMode == _TerminalInputMode.read,

View File

@ -72,13 +72,30 @@ void main() {
expect(terminalView.deleteDetection, isTrue); expect(terminalView.deleteDetection, isTrue);
}); });
testWidgets('terminal view disables auto resize and text reflow', (tester) async { testWidgets('terminal view freezes auto resize while editing and restores it after returning to read', (tester) async {
await _pumpTerminalPage(tester); await _pumpTerminalPage(tester);
final terminalView = tester.widget<TerminalView>(find.byType(TerminalView)); var terminalView = tester.widget<TerminalView>(find.byType(TerminalView));
expect(terminalView.autoResize, isFalse); expect(terminalView.autoResize, isTrue);
expect(terminalView.terminal.reflowEnabled, isFalse); expect(terminalView.terminal.reflowEnabled, isFalse);
await tester.tap(find.byKey(const Key('terminal_mode_edit_button')));
await tester.pump();
terminalView = tester.widget<TerminalView>(find.byType(TerminalView));
expect(terminalView.autoResize, isFalse);
await tester.tap(find.byKey(const Key('terminal_mode_read_button')));
await tester.pump();
terminalView = tester.widget<TerminalView>(find.byType(TerminalView));
expect(terminalView.autoResize, isFalse);
await tester.pump(const Duration(milliseconds: 260));
terminalView = tester.widget<TerminalView>(find.byType(TerminalView));
expect(terminalView.autoResize, isTrue);
}); });
testWidgets( testWidgets(