diff --git a/lib/src/terminal/src/terminal_view.dart b/lib/src/terminal/src/terminal_view.dart index 878baa0d..e501b477 100644 --- a/lib/src/terminal/src/terminal_view.dart +++ b/lib/src/terminal/src/terminal_view.dart @@ -48,7 +48,6 @@ class TerminalView extends StatefulWidget { this.onKeyEvent, this.readOnly = false, this.hardwareKeyboardOnly = false, - this.simulateScroll = true, }); /// The underlying terminal that this widget renders. @@ -132,13 +131,6 @@ class TerminalView extends StatefulWidget { /// also prevent any on-screen keyboard to be shown. 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 State createState() => TerminalViewState(); } diff --git a/lib/src/terminal/src/ui/infinite_scroll_view.dart b/lib/src/terminal/src/ui/infinite_scroll_view.dart deleted file mode 100644 index 82612859..00000000 --- a/lib/src/terminal/src/ui/infinite_scroll_view.dart +++ /dev/null @@ -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); - } -} diff --git a/lib/src/terminal/src/ui/keyboard_visibility.dart b/lib/src/terminal/src/ui/keyboard_visibility.dart deleted file mode 100644 index 66ea4c5b..00000000 --- a/lib/src/terminal/src/ui/keyboard_visibility.dart +++ /dev/null @@ -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 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; - } -} diff --git a/lib/src/terminal/src/ui/scroll_handler.dart b/lib/src/terminal/src/ui/scroll_handler.dart deleted file mode 100644 index 85180bdf..00000000 --- a/lib/src/terminal/src/ui/scroll_handler.dart +++ /dev/null @@ -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 createState() => _TerminalScrollGestureHandlerState(); -} - -class _TerminalScrollGestureHandlerState extends State { - 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, - ); - } -} diff --git a/lib/src/terminal/terminal.dart b/lib/src/terminal/terminal.dart index 4ce9c56e..0027648f 100644 --- a/lib/src/terminal/terminal.dart +++ b/lib/src/terminal/terminal.dart @@ -27,7 +27,6 @@ export 'src/terminal.dart'; export 'src/terminal_view.dart'; export 'src/ui/controller.dart'; export 'src/ui/cursor_type.dart'; -export 'src/ui/keyboard_visibility.dart'; export 'src/ui/pointer_input.dart'; export 'src/ui/selection_mode.dart'; export 'src/ui/shortcut/shortcuts.dart'; diff --git a/test/terminal/ui/ui_widget_test.dart b/test/terminal/ui/ui_widget_test.dart index 5ea91ace..fb93f53f 100644 --- a/test/terminal/ui/ui_widget_test.dart +++ b/test/terminal/ui/ui_widget_test.dart @@ -1,16 +1,9 @@ -/// Widget tests for `lib/src/terminal/src/ui/` widget-level helpers -/// (CustomKeyboardListener, KeyboardVisibilty, InfiniteScrollView, -/// TerminalScrollGestureHandler). +/// Widget tests for `lib/src/terminal/src/ui/` widget-level helpers — +/// just CustomKeyboardListener now; the other widgets in this folder +/// either don't need a widget tree or were retired as dead surface. 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_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/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -96,294 +89,4 @@ void main() { 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 = []; - 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 = []; - - 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 = []; - 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 = []; - 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 = []; - 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 = []; - final terminal = Terminal(maxLines: 100, onOutput: outputs.add)..useAltBuffer(); - final cellOffsets = []; - 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); - }); - }); }