Stabilize terminal resizing around input mode changes
This commit is contained in:
parent
43e7dc61f7
commit
0f72bc207a
@ -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,
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user