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

View File

@ -72,13 +72,30 @@ void main() {
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);
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);
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(