remove dead Scrollable-era widgets + simulateScroll param
Three widgets under lib/src/terminal/src/ui/ and one TerminalView parameter were leftovers from the era when TerminalView wrapped its viewport in a real Scrollable. The Scrollable path was replaced with PointerScrollEvent → PgUp/PgDown translation (alive, well-tested in terminal_view_test.dart); these helpers stayed behind with tests but zero production callers. Drops: - TerminalScrollGestureHandler (scroll_handler.dart, 100 LOC) - InfiniteScrollView (infinite_scroll_view.dart, 117 LOC) - KeyboardVisibilty (keyboard_visibility.dart, 59 LOC) — last production caller was removed in048e835- TerminalView.simulateScroll parameter — declared, never read - The matching test groups + imports in ui_widget_test.dart - The KeyboardVisibilty export from the terminal barrel Net: -585 lines from lib/ + test/, no behavior change, and infinite_scroll_view.dart stops being the 90%-coverage outlier we were apologising for in the previous test sweep. Same shape as T-93 (dead onTapUp wiring), T-95 (dead tertiary tap), and048e835(dead scrollController plumbing). Public-or-tested surface that no caller exercised. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,6 @@ class TerminalView extends StatefulWidget {
|
|||||||
this.onKeyEvent,
|
this.onKeyEvent,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
this.hardwareKeyboardOnly = false,
|
this.hardwareKeyboardOnly = false,
|
||||||
this.simulateScroll = true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The underlying terminal that this widget renders.
|
/// The underlying terminal that this widget renders.
|
||||||
@@ -132,13 +131,6 @@ class TerminalView extends StatefulWidget {
|
|||||||
/// also prevent any on-screen keyboard to be shown.
|
/// also prevent any on-screen keyboard to be shown.
|
||||||
final bool hardwareKeyboardOnly;
|
final bool hardwareKeyboardOnly;
|
||||||
|
|
||||||
/// If true, when the terminal is in alternate buffer (for example running
|
|
||||||
/// vim, man, etc), if the application does not declare that it can handle
|
|
||||||
/// scrolling, the terminal will simulate scrolling by sending up/down arrow
|
|
||||||
/// keys to the application. This is standard behavior for most terminal
|
|
||||||
/// emulators. True by default.
|
|
||||||
final bool simulateScroll;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TerminalView> createState() => TerminalViewState();
|
State<TerminalView> createState() => TerminalViewState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
|
|
||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
/// The function called when the user scrolls the [InfiniteScrollView]. [offset]
|
|
||||||
/// is the current offset of the scroll view, ranging from [double.negativeInfinity]
|
|
||||||
/// to [double.infinity].
|
|
||||||
typedef ScrollCallback = void Function(double offset);
|
|
||||||
|
|
||||||
/// A [Scrollable] that can be scrolled infinitely in both directions. When
|
|
||||||
/// scroll happens, the [onScroll] callback is called with the new offset.
|
|
||||||
class InfiniteScrollView extends StatelessWidget {
|
|
||||||
const InfiniteScrollView({
|
|
||||||
super.key,
|
|
||||||
required this.onScroll,
|
|
||||||
required this.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ScrollCallback onScroll;
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scrollable(
|
|
||||||
viewportBuilder: (context, position) {
|
|
||||||
return _InfiniteScrollView(
|
|
||||||
position: position,
|
|
||||||
onScroll: onScroll,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _InfiniteScrollView extends SingleChildRenderObjectWidget {
|
|
||||||
const _InfiniteScrollView({
|
|
||||||
// super.key,
|
|
||||||
super.child,
|
|
||||||
required this.position,
|
|
||||||
required this.onScroll,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ViewportOffset position;
|
|
||||||
|
|
||||||
final ScrollCallback onScroll;
|
|
||||||
|
|
||||||
@override
|
|
||||||
RenderObject createRenderObject(BuildContext context) {
|
|
||||||
return _RenderInfiniteScrollView(
|
|
||||||
position: position,
|
|
||||||
onScroll: onScroll,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateRenderObject(
|
|
||||||
BuildContext context,
|
|
||||||
_RenderInfiniteScrollView renderObject,
|
|
||||||
) {
|
|
||||||
renderObject
|
|
||||||
..position = position
|
|
||||||
..onScroll = onScroll;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RenderInfiniteScrollView extends RenderShiftedBox {
|
|
||||||
_RenderInfiniteScrollView({
|
|
||||||
RenderBox? child,
|
|
||||||
required ViewportOffset position,
|
|
||||||
required ScrollCallback onScroll,
|
|
||||||
}) : _position = position,
|
|
||||||
_scrollCallback = onScroll,
|
|
||||||
super(child);
|
|
||||||
|
|
||||||
ViewportOffset _position;
|
|
||||||
set position(ViewportOffset value) {
|
|
||||||
if (_position == value) return;
|
|
||||||
if (attached) _position.removeListener(markNeedsLayout);
|
|
||||||
_position = value;
|
|
||||||
if (attached) _position.addListener(markNeedsLayout);
|
|
||||||
markNeedsLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollCallback _scrollCallback;
|
|
||||||
set onScroll(ScrollCallback value) {
|
|
||||||
if (_scrollCallback == value) return;
|
|
||||||
_scrollCallback = value;
|
|
||||||
markNeedsLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onScroll() {
|
|
||||||
_scrollCallback(_position.pixels);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attach(covariant PipelineOwner owner) {
|
|
||||||
super.attach(owner);
|
|
||||||
_position.addListener(_onScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void detach() {
|
|
||||||
super.detach();
|
|
||||||
_position.removeListener(_onScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void performLayout() {
|
|
||||||
child?.layout(constraints, parentUsesSize: true);
|
|
||||||
size = child?.size ?? Size.zero;
|
|
||||||
_position.applyViewportDimension(size.height);
|
|
||||||
_position.applyContentDimensions(double.negativeInfinity, double.infinity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
|
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class KeyboardVisibilty extends StatefulWidget {
|
|
||||||
const KeyboardVisibilty({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
this.onKeyboardShow,
|
|
||||||
this.onKeyboardHide,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
final VoidCallback? onKeyboardShow;
|
|
||||||
|
|
||||||
final VoidCallback? onKeyboardHide;
|
|
||||||
|
|
||||||
@override
|
|
||||||
KeyboardVisibiltyState createState() => KeyboardVisibiltyState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class KeyboardVisibiltyState extends State<KeyboardVisibilty> with WidgetsBindingObserver {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeMetrics() {
|
|
||||||
final bottomInset = View.of(context).viewInsets.bottom;
|
|
||||||
|
|
||||||
if (bottomInset != _lastBottomInset) {
|
|
||||||
if (bottomInset > 0) {
|
|
||||||
widget.onKeyboardShow?.call();
|
|
||||||
} else {
|
|
||||||
widget.onKeyboardHide?.call();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastBottomInset = bottomInset;
|
|
||||||
|
|
||||||
super.didChangeMetrics();
|
|
||||||
}
|
|
||||||
|
|
||||||
var _lastBottomInset = 0.0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return widget.child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
|
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:clide/src/terminal/terminal.dart';
|
|
||||||
|
|
||||||
class TerminalScrollGestureHandler extends StatefulWidget {
|
|
||||||
const TerminalScrollGestureHandler({
|
|
||||||
super.key,
|
|
||||||
required this.terminal,
|
|
||||||
required this.getCellOffset,
|
|
||||||
required this.getLineHeight,
|
|
||||||
this.simulateScroll = true,
|
|
||||||
required this.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Terminal terminal;
|
|
||||||
final CellOffset Function(Offset) getCellOffset;
|
|
||||||
final double Function() getLineHeight;
|
|
||||||
final bool simulateScroll;
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TerminalScrollGestureHandler> createState() => _TerminalScrollGestureHandlerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TerminalScrollGestureHandlerState extends State<TerminalScrollGestureHandler> {
|
|
||||||
var isAltBuffer = false;
|
|
||||||
var _lastPointerPosition = Offset.zero;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
widget.terminal.addListener(_onTerminalUpdated);
|
|
||||||
isAltBuffer = widget.terminal.isUsingAltBuffer;
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.terminal.removeListener(_onTerminalUpdated);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant TerminalScrollGestureHandler oldWidget) {
|
|
||||||
if (oldWidget.terminal != widget.terminal) {
|
|
||||||
oldWidget.terminal.removeListener(_onTerminalUpdated);
|
|
||||||
widget.terminal.addListener(_onTerminalUpdated);
|
|
||||||
isAltBuffer = widget.terminal.isUsingAltBuffer;
|
|
||||||
}
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onTerminalUpdated() {
|
|
||||||
if (isAltBuffer != widget.terminal.isUsingAltBuffer) {
|
|
||||||
isAltBuffer = widget.terminal.isUsingAltBuffer;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sendScrollEvent(bool up) {
|
|
||||||
final position = widget.getCellOffset(_lastPointerPosition);
|
|
||||||
|
|
||||||
final handled = widget.terminal.mouseInput(
|
|
||||||
up ? TerminalMouseButton.wheelUp : TerminalMouseButton.wheelDown,
|
|
||||||
TerminalMouseButtonState.down,
|
|
||||||
position,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!handled && widget.simulateScroll) {
|
|
||||||
widget.terminal.keyInput(
|
|
||||||
up ? TerminalKey.arrowUp : TerminalKey.arrowDown,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onPointerSignal(PointerSignalEvent event) {
|
|
||||||
if (event is! PointerScrollEvent) return;
|
|
||||||
_lastPointerPosition = event.position;
|
|
||||||
final lineHeight = widget.getLineHeight();
|
|
||||||
if (lineHeight <= 0) return;
|
|
||||||
final lines = (event.scrollDelta.dy / lineHeight).round().clamp(-5, 5);
|
|
||||||
for (var i = 0; i < lines.abs(); i++) {
|
|
||||||
_sendScrollEvent(lines < 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (!isAltBuffer) return widget.child;
|
|
||||||
|
|
||||||
// Intercept scroll at the pointer level so the inner Scrollable
|
|
||||||
// (normal buffer) never sees the event in alt-buffer mode.
|
|
||||||
return Listener(
|
|
||||||
onPointerSignal: _onPointerSignal,
|
|
||||||
onPointerDown: (event) => _lastPointerPosition = event.position,
|
|
||||||
child: widget.child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,6 @@ export 'src/terminal.dart';
|
|||||||
export 'src/terminal_view.dart';
|
export 'src/terminal_view.dart';
|
||||||
export 'src/ui/controller.dart';
|
export 'src/ui/controller.dart';
|
||||||
export 'src/ui/cursor_type.dart';
|
export 'src/ui/cursor_type.dart';
|
||||||
export 'src/ui/keyboard_visibility.dart';
|
|
||||||
export 'src/ui/pointer_input.dart';
|
export 'src/ui/pointer_input.dart';
|
||||||
export 'src/ui/selection_mode.dart';
|
export 'src/ui/selection_mode.dart';
|
||||||
export 'src/ui/shortcut/shortcuts.dart';
|
export 'src/ui/shortcut/shortcuts.dart';
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
/// Widget tests for `lib/src/terminal/src/ui/` widget-level helpers
|
/// Widget tests for `lib/src/terminal/src/ui/` widget-level helpers —
|
||||||
/// (CustomKeyboardListener, KeyboardVisibilty, InfiniteScrollView,
|
/// just CustomKeyboardListener now; the other widgets in this folder
|
||||||
/// TerminalScrollGestureHandler).
|
/// either don't need a widget tree or were retired as dead surface.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
|
|
||||||
import 'package:clide/src/terminal/src/core/mouse/mode.dart';
|
|
||||||
import 'package:clide/src/terminal/src/terminal.dart';
|
|
||||||
import 'package:clide/src/terminal/src/ui/infinite_scroll_view.dart';
|
|
||||||
import 'package:clide/src/terminal/src/ui/keyboard_listener.dart';
|
import 'package:clide/src/terminal/src/ui/keyboard_listener.dart';
|
||||||
import 'package:clide/src/terminal/src/ui/keyboard_visibility.dart';
|
|
||||||
import 'package:clide/src/terminal/src/ui/scroll_handler.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@@ -96,294 +89,4 @@ void main() {
|
|||||||
expect(inserts, isEmpty);
|
expect(inserts, isEmpty);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('KeyboardVisibilty', () {
|
|
||||||
testWidgets('fires onKeyboardShow when bottom inset goes positive, onKeyboardHide when it returns to 0', (tester) async {
|
|
||||||
var shows = 0;
|
|
||||||
var hides = 0;
|
|
||||||
await tester.pumpWidget(_host(
|
|
||||||
KeyboardVisibilty(
|
|
||||||
onKeyboardShow: () => shows++,
|
|
||||||
onKeyboardHide: () => hides++,
|
|
||||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Simulate keyboard appearance.
|
|
||||||
tester.view.viewInsets = const FakeViewPadding(bottom: 200, left: 0, right: 0, top: 0);
|
|
||||||
tester.binding.handleMetricsChanged();
|
|
||||||
await tester.pump();
|
|
||||||
expect(shows, 1);
|
|
||||||
expect(hides, 0);
|
|
||||||
|
|
||||||
// Simulate keyboard dismissal.
|
|
||||||
tester.view.resetViewInsets();
|
|
||||||
tester.binding.handleMetricsChanged();
|
|
||||||
await tester.pump();
|
|
||||||
expect(hides, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('repeated metrics events with the same bottom inset fire callbacks only when the inset changes', (tester) async {
|
|
||||||
var shows = 0;
|
|
||||||
await tester.pumpWidget(_host(
|
|
||||||
KeyboardVisibilty(
|
|
||||||
onKeyboardShow: () => shows++,
|
|
||||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
tester.view.viewInsets = const FakeViewPadding(bottom: 200, left: 0, right: 0, top: 0);
|
|
||||||
tester.binding.handleMetricsChanged();
|
|
||||||
await tester.pump();
|
|
||||||
// Fire metrics again with the same inset — should not trigger again.
|
|
||||||
tester.binding.handleMetricsChanged();
|
|
||||||
await tester.pump();
|
|
||||||
expect(shows, 1);
|
|
||||||
addTearDown(tester.view.resetViewInsets);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('InfiniteScrollView', () {
|
|
||||||
testWidgets('reports a scroll offset to onScroll when the viewport position moves', (tester) async {
|
|
||||||
final offsets = <double>[];
|
|
||||||
await tester.pumpWidget(_host(
|
|
||||||
InfiniteScrollView(
|
|
||||||
onScroll: offsets.add,
|
|
||||||
child: const SizedBox(width: 400, height: 1000),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Drive the underlying ViewportOffset by sending a scroll event into
|
|
||||||
// the Scrollable.
|
|
||||||
final scrollable = find.byType(Scrollable);
|
|
||||||
await tester.drag(scrollable, const Offset(0, -100));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(offsets, isNotEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('updates render object when onScroll callback identity changes', (tester) async {
|
|
||||||
var firstCalls = 0;
|
|
||||||
var secondCalls = 0;
|
|
||||||
Widget build(void Function(double) cb) => _host(
|
|
||||||
InfiniteScrollView(
|
|
||||||
onScroll: cb,
|
|
||||||
child: const SizedBox(width: 400, height: 1000),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(build((_) => firstCalls++));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.drag(find.byType(Scrollable), const Offset(0, -50));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
final firstCount = firstCalls;
|
|
||||||
expect(firstCount, isPositive);
|
|
||||||
|
|
||||||
// Swap callback identity — render object's onScroll setter must update.
|
|
||||||
await tester.pumpWidget(build((_) => secondCalls++));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.drag(find.byType(Scrollable), const Offset(0, -50));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(secondCalls, isPositive);
|
|
||||||
expect(firstCalls, firstCount); // first stops firing after swap
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('TerminalScrollGestureHandler', () {
|
|
||||||
testWidgets('main-buffer mode passes through without intercepting scroll', (tester) async {
|
|
||||||
final terminal = Terminal(maxLines: 100);
|
|
||||||
final handlerCalls = <Offset>[];
|
|
||||||
|
|
||||||
await tester.pumpWidget(_host(
|
|
||||||
TerminalScrollGestureHandler(
|
|
||||||
terminal: terminal,
|
|
||||||
getCellOffset: (offset) {
|
|
||||||
handlerCalls.add(offset);
|
|
||||||
return const CellOffset(0, 0);
|
|
||||||
},
|
|
||||||
getLineHeight: () => 14.0,
|
|
||||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
// Main-buffer mode → the handler returns the child directly, no
|
|
||||||
// Listener around it. Sending a scroll event reaches nothing.
|
|
||||||
final pos = tester.getCenter(find.byType(TerminalScrollGestureHandler));
|
|
||||||
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
||||||
await mouse.addPointer(location: pos);
|
|
||||||
await tester.sendEventToBinding(PointerScrollEvent(
|
|
||||||
position: pos,
|
|
||||||
scrollDelta: const Offset(0, 100),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
expect(handlerCalls, isEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('alt-buffer mode intercepts scroll; falls back to keyInput when terminal does not handle the mouse event', (tester) async {
|
|
||||||
final outputs = <String>[];
|
|
||||||
final terminal = Terminal(maxLines: 100, onOutput: outputs.add);
|
|
||||||
terminal.useAltBuffer();
|
|
||||||
// No mouse mode set on the terminal — mouseInput will return false,
|
|
||||||
// so the simulateScroll fallback kicks in (sends arrow keys).
|
|
||||||
|
|
||||||
await tester.pumpWidget(_host(
|
|
||||||
TerminalScrollGestureHandler(
|
|
||||||
terminal: terminal,
|
|
||||||
getCellOffset: (_) => const CellOffset(0, 0),
|
|
||||||
getLineHeight: () => 14.0,
|
|
||||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
// Confirm the Listener is in the tree (isAltBuffer recognised on
|
|
||||||
// initial build).
|
|
||||||
expect(find.byType(Listener), findsOneWidget);
|
|
||||||
|
|
||||||
final pos = tester.getCenter(find.byType(TerminalScrollGestureHandler));
|
|
||||||
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
||||||
await mouse.addPointer(location: pos);
|
|
||||||
await tester.sendEventToBinding(PointerScrollEvent(
|
|
||||||
position: pos,
|
|
||||||
scrollDelta: const Offset(0, 100),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
// Falls back to TerminalKey.arrowDown via simulateScroll path → emits
|
|
||||||
// an escape via the default keytab.
|
|
||||||
expect(outputs, isNotEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('alt-buffer mode + active mouse mode forwards as a real mouse event (no key fallback)', (tester) async {
|
|
||||||
final outputs = <String>[];
|
|
||||||
final terminal = Terminal(maxLines: 100, onOutput: outputs.add);
|
|
||||||
terminal.useAltBuffer();
|
|
||||||
// Activate a mouse mode so terminal.mouseInput consumes wheel events.
|
|
||||||
terminal.setMouseMode(MouseMode.upDownScroll);
|
|
||||||
|
|
||||||
await tester.pumpWidget(_host(
|
|
||||||
TerminalScrollGestureHandler(
|
|
||||||
terminal: terminal,
|
|
||||||
getCellOffset: (_) => const CellOffset(0, 0),
|
|
||||||
getLineHeight: () => 14.0,
|
|
||||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
final pos = tester.getCenter(find.byType(TerminalScrollGestureHandler));
|
|
||||||
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
||||||
await mouse.addPointer(location: pos);
|
|
||||||
await tester.sendEventToBinding(PointerScrollEvent(
|
|
||||||
position: pos,
|
|
||||||
scrollDelta: const Offset(0, 100),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
// Mouse-mode active → mouseInput consumes; output is the mouse-report
|
|
||||||
// escape, not a PgDown / arrow.
|
|
||||||
expect(outputs, isNotEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('alt-buffer mode with simulateScroll=false drops scrolls when the terminal does not consume them', (tester) async {
|
|
||||||
final outputs = <String>[];
|
|
||||||
final terminal = Terminal(maxLines: 100, onOutput: outputs.add);
|
|
||||||
terminal.useAltBuffer();
|
|
||||||
|
|
||||||
await tester.pumpWidget(_host(
|
|
||||||
TerminalScrollGestureHandler(
|
|
||||||
terminal: terminal,
|
|
||||||
getCellOffset: (_) => const CellOffset(0, 0),
|
|
||||||
getLineHeight: () => 14.0,
|
|
||||||
simulateScroll: false,
|
|
||||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
final pos = tester.getCenter(find.byType(TerminalScrollGestureHandler));
|
|
||||||
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
||||||
await mouse.addPointer(location: pos);
|
|
||||||
await tester.sendEventToBinding(PointerScrollEvent(
|
|
||||||
position: pos,
|
|
||||||
scrollDelta: const Offset(0, 100),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
// Terminal didn't consume + simulateScroll false → no output at all.
|
|
||||||
expect(outputs, isEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('switching the terminal into alt buffer flips the handler from passthrough to intercepting', (tester) async {
|
|
||||||
final terminal = Terminal(maxLines: 100);
|
|
||||||
await tester.pumpWidget(_host(
|
|
||||||
TerminalScrollGestureHandler(
|
|
||||||
terminal: terminal,
|
|
||||||
getCellOffset: (_) => const CellOffset(0, 0),
|
|
||||||
getLineHeight: () => 14.0,
|
|
||||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
// Initially main-buffer → no Listener wrapping.
|
|
||||||
expect(find.byType(Listener), findsNothing);
|
|
||||||
|
|
||||||
terminal.useAltBuffer();
|
|
||||||
terminal.write(''); // notifyListeners via parser.write
|
|
||||||
await tester.pump();
|
|
||||||
// Alt-buffer → Listener wraps the child.
|
|
||||||
expect(find.byType(Listener), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('alt-buffer Listener tracks pointer-down location for scroll-event positioning', (tester) async {
|
|
||||||
// Covers the onPointerDown handler that captures _lastPointerPosition
|
|
||||||
// for use by the next scroll event.
|
|
||||||
final outputs = <String>[];
|
|
||||||
final terminal = Terminal(maxLines: 100, onOutput: outputs.add)..useAltBuffer();
|
|
||||||
final cellOffsets = <Offset>[];
|
|
||||||
await tester.pumpWidget(_host(
|
|
||||||
TerminalScrollGestureHandler(
|
|
||||||
terminal: terminal,
|
|
||||||
getCellOffset: (offset) {
|
|
||||||
cellOffsets.add(offset);
|
|
||||||
return const CellOffset(0, 0);
|
|
||||||
},
|
|
||||||
getLineHeight: () => 14.0,
|
|
||||||
child: const ColoredBox(
|
|
||||||
color: Color(0xFF000000),
|
|
||||||
child: SizedBox.expand(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
final pos = tester.getCenter(find.byType(TerminalScrollGestureHandler));
|
|
||||||
// Press first — that triggers onPointerDown which records the
|
|
||||||
// position into _lastPointerPosition.
|
|
||||||
final mouse = await tester.startGesture(pos, kind: PointerDeviceKind.mouse);
|
|
||||||
// Now scroll — getCellOffset is called with _lastPointerPosition.
|
|
||||||
await tester.sendEventToBinding(PointerScrollEvent(
|
|
||||||
position: pos,
|
|
||||||
scrollDelta: const Offset(0, 100),
|
|
||||||
));
|
|
||||||
await tester.pump();
|
|
||||||
await mouse.up();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(cellOffsets, isNotEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('didUpdateWidget rebinds listeners when the terminal instance changes', (tester) async {
|
|
||||||
final t1 = Terminal(maxLines: 100);
|
|
||||||
final t2 = Terminal(maxLines: 100)..useAltBuffer();
|
|
||||||
Widget build(Terminal t) => _host(
|
|
||||||
TerminalScrollGestureHandler(
|
|
||||||
terminal: t,
|
|
||||||
getCellOffset: (_) => const CellOffset(0, 0),
|
|
||||||
getLineHeight: () => 14.0,
|
|
||||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await tester.pumpWidget(build(t1));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pumpWidget(build(t2));
|
|
||||||
await tester.pump();
|
|
||||||
// The Listener should now be present because t2 is in alt-buffer.
|
|
||||||
expect(find.byType(Listener), findsOneWidget);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user