mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-10-19 16:17:03 +00:00
fix(terminal): preserve trackpad scrolling in alternate screen
Prevent the inner terminal scrollable from accepting user scrolling in the alternate buffer, so xterm's existing outer handler receives trackpad gestures after main-screen history. Keep wheel encoding and drag reporting unchanged. Cover trackpad forwarding, subsequent drag coordinates, arrow fallback, and return to local scrollback.
This commit is contained in:
@@ -9,6 +9,7 @@ import 'platform_model.dart';
|
||||
import 'rustdesk_terminal.dart';
|
||||
import 'terminal_copy_shortcut.dart';
|
||||
import 'terminal_mouse_drag_reporter.dart';
|
||||
import 'terminal_scroll_controller.dart';
|
||||
|
||||
part 'terminal_mouse_handler_input.dart';
|
||||
part 'terminal_web_clipboard_gesture.dart';
|
||||
@@ -55,7 +56,7 @@ class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
|
||||
static const _scrollDown = 1;
|
||||
|
||||
final _terminalViewKey = GlobalKey<TerminalViewState>();
|
||||
final _scrollController = ScrollController();
|
||||
late final _scrollController = TerminalScrollController(() => widget.terminal);
|
||||
final _mouseDrag = TerminalMouseDragReporter();
|
||||
late final WheelButtonFixMouseHandler _mouseHandler;
|
||||
TerminalMouseHandler? _previousMouseHandler;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
|
||||
class TerminalScrollController extends ScrollController {
|
||||
TerminalScrollController(this.terminal);
|
||||
|
||||
final Terminal Function() terminal;
|
||||
|
||||
@override
|
||||
ScrollPosition createScrollPosition(ScrollPhysics physics,
|
||||
ScrollContext context, ScrollPosition? oldPosition) {
|
||||
return super.createScrollPosition(
|
||||
_TerminalScrollPhysics(terminal, parent: physics),
|
||||
context,
|
||||
oldPosition,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TerminalScrollPhysics extends ScrollPhysics {
|
||||
const _TerminalScrollPhysics(this.terminal, {super.parent});
|
||||
|
||||
final Terminal Function() terminal;
|
||||
|
||||
@override
|
||||
_TerminalScrollPhysics applyTo(ScrollPhysics? ancestor) =>
|
||||
_TerminalScrollPhysics(terminal, parent: buildParent(ancestor));
|
||||
|
||||
@override
|
||||
bool shouldAcceptUserOffset(ScrollMetrics position) {
|
||||
// A retained main-screen offset can make this inner scrollable win the
|
||||
// trackpad gesture, starving xterm's outer alternate-screen scroll handler.
|
||||
if (terminal().isUsingAltBuffer) return false;
|
||||
return super.shouldAcceptUserOffset(position);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/models/terminal_mouse_handler.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
|
||||
const _captureMouse = '\x1b[?1007l\x1b[?1000h\x1b[?1002h\x1b[?1006h\x1b[?1003h';
|
||||
|
||||
void main() {
|
||||
late Terminal terminal;
|
||||
late TerminalController controller;
|
||||
late List<String> output;
|
||||
|
||||
setUp(() {
|
||||
output = [];
|
||||
terminal = Terminal()..onOutput = output.add;
|
||||
controller = TerminalController();
|
||||
});
|
||||
|
||||
Future<TerminalViewState> mount(WidgetTester tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 30, top: 50),
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
height: 120,
|
||||
child: TerminalMouseInteraction(terminal, controller: controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
terminal.write(List.generate(80, (i) => 'line $i\r\n').join());
|
||||
await tester.pump();
|
||||
final view = tester.state<TerminalViewState>(find.byType(TerminalView));
|
||||
expect(view.widget.scrollController!.offset, greaterThan(0));
|
||||
return view;
|
||||
}
|
||||
|
||||
Future<void> scrollTrackpad(
|
||||
WidgetTester tester, Offset point, double distance) async {
|
||||
final pointer = TestPointer(2, PointerDeviceKind.trackpad);
|
||||
await tester.sendEventToBinding(pointer.panZoomStart(point));
|
||||
await tester.sendEventToBinding(
|
||||
pointer.panZoomUpdate(point, pan: Offset(0, distance)));
|
||||
await tester.sendEventToBinding(
|
||||
pointer.panZoomUpdate(point, pan: Offset(0, distance * 2)));
|
||||
await tester.sendEventToBinding(pointer.panZoomEnd());
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
for (final trackpad in [false, true]) {
|
||||
final device = trackpad ? 'trackpad' : 'wheel';
|
||||
testWidgets('alternate screen reports $device after main-screen history',
|
||||
(tester) async {
|
||||
final view = await mount(tester);
|
||||
terminal.write('\x1b[?1049h$_captureMouse');
|
||||
await tester.pump();
|
||||
final render = view.renderTerminal;
|
||||
final point = render.localToGlobal(
|
||||
Offset(render.cellSize.width * 2.5, render.lineHeight * 1.5));
|
||||
if (trackpad) {
|
||||
await scrollTrackpad(tester, point, render.lineHeight * 3);
|
||||
} else {
|
||||
await tester.sendEventToBinding(PointerScrollEvent(
|
||||
position: point,
|
||||
scrollDelta: Offset(0, -render.lineHeight * 3),
|
||||
));
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
expect(output, isNotEmpty);
|
||||
expect(output, everyElement('\x1b[<64;3;2M'));
|
||||
expect(view.widget.scrollController!.offset, 0);
|
||||
expect(controller.selection, isNull);
|
||||
}, variant: TargetPlatformVariant.only(TargetPlatform.macOS));
|
||||
}
|
||||
|
||||
testWidgets('trackpad scrolling keeps subsequent drag coordinates aligned',
|
||||
(tester) async {
|
||||
final view = await mount(tester);
|
||||
terminal.write('\x1b[?1049h$_captureMouse');
|
||||
await tester.pump();
|
||||
final render = view.renderTerminal;
|
||||
final start = render.localToGlobal(
|
||||
Offset(render.cellSize.width * 2.5, render.lineHeight * 1.5));
|
||||
final end = render.localToGlobal(
|
||||
Offset(render.cellSize.width * 6.5, render.lineHeight * 2.5));
|
||||
await scrollTrackpad(tester, start, render.lineHeight * 3);
|
||||
output.clear();
|
||||
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await mouse.down(start);
|
||||
await mouse.moveTo(end);
|
||||
await mouse.up();
|
||||
await tester.pump(kDoubleTapTimeout);
|
||||
|
||||
expect(output, ['\x1b[<0;3;2M', '\x1b[<32;7;3M', '\x1b[<0;7;3m']);
|
||||
expect(controller.selection, isNull);
|
||||
expect(controller.suspendedPointerInputs, isFalse);
|
||||
}, variant: TargetPlatformVariant.only(TargetPlatform.macOS));
|
||||
|
||||
testWidgets('leaving alternate screen restores local trackpad scrollback',
|
||||
(tester) async {
|
||||
final view = await mount(tester);
|
||||
terminal.write('\x1b[?1049h$_captureMouse');
|
||||
await tester.pump();
|
||||
terminal.write('\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1006l\x1b[?1049l');
|
||||
await tester.pump();
|
||||
final before = view.widget.scrollController!.offset;
|
||||
await scrollTrackpad(
|
||||
tester, view.renderTerminal.localToGlobal(const Offset(40, 40)), 48);
|
||||
|
||||
expect(output, isEmpty);
|
||||
expect(view.widget.scrollController!.offset, lessThan(before));
|
||||
}, variant: TargetPlatformVariant.only(TargetPlatform.macOS));
|
||||
|
||||
testWidgets('alternate screen without mouse capture still simulates arrows',
|
||||
(tester) async {
|
||||
final view = await mount(tester);
|
||||
terminal.write('\x1b[?1049h');
|
||||
await tester.pump();
|
||||
await scrollTrackpad(
|
||||
tester, view.renderTerminal.localToGlobal(const Offset(40, 40)), -48);
|
||||
|
||||
expect(output, isNotEmpty);
|
||||
expect(output, everyElement('\x1b[B'));
|
||||
expect(view.widget.scrollController!.offset, 0);
|
||||
}, variant: TargetPlatformVariant.only(TargetPlatform.macOS));
|
||||
}
|
||||
Reference in New Issue
Block a user