Files
rustdesk/flutter/lib/models/terminal_mouse_drag_reporter.dart
fufesou 618bf37deb feat(terminal): add opt-in OSC 52 clipboard writes (#16072)
* feat(terminal): add opt-in OSC 52 clipboard writes

* Remove dup tr

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-05 13:21:49 +08:00

298 lines
8.6 KiB
Dart

import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:xterm/xterm.dart';
const _cellIndexOffset = 1;
const _legacyCodeOffset = 32;
const _leftButtonCode = 0;
const _motionButtonCode = 32;
const _releaseButtonCode = 3;
const _shiftModifierCode = 4;
const _metaModifierCode = 8;
const _controlModifierCode = 16;
const _modifierCodeMask =
_shiftModifierCode | _metaModifierCode | _controlModifierCode;
const _normalCoordinateLimit = 223;
const _utfCoordinateLimit = 2015;
String encodeTerminalMouseReport(
MouseReportMode mode,
int button,
CellOffset position, {
bool release = false,
}) {
final x = position.x + _cellIndexOffset;
final y = position.y + _cellIndexOffset;
final reportedButton =
release ? _releaseButtonCode | (button & _modifierCodeMask) : button;
switch (mode) {
case MouseReportMode.normal:
case MouseReportMode.utf:
final limit = mode == MouseReportMode.normal
? _normalCoordinateLimit
: _utfCoordinateLimit;
final encodedButton =
String.fromCharCode(_legacyCodeOffset + reportedButton);
return '\x1b[M$encodedButton${_legacyCoordinate(x, limit)}'
'${_legacyCoordinate(y, limit)}';
case MouseReportMode.sgr:
final suffix = release ? 'm' : 'M';
return '\x1b[<$button;$x;$y$suffix';
case MouseReportMode.urxvt:
return '\x1b[${_legacyCodeOffset + reportedButton};$x;${y}M';
}
}
String _legacyCoordinate(int value, int limit) =>
value > limit ? '\x00' : String.fromCharCode(_legacyCodeOffset + value);
int _activeModifierCode() {
final keyboard = HardwareKeyboard.instance;
return (keyboard.isShiftPressed ? _shiftModifierCode : 0) |
(keyboard.isAltPressed ? _metaModifierCode : 0) |
(keyboard.isControlPressed ? _controlModifierCode : 0);
}
class TerminalMouseDragReporter {
int? _pointerId;
TerminalController? _controller;
late CellOffset _lastReportedPosition;
var _ownsControllerSuspension = false;
var _releasePending = false;
var _reporting = false;
var _dragged = false;
bool handleDown(
PointerDownEvent event,
Terminal terminal,
TerminalViewState? terminalView, {
bool reportTouchInput = false,
bool deferReport = false,
}) {
if (!_isPrimaryPointer(event, reportTouchInput) ||
!_reportsDrag(terminal.mouseMode)) {
return false;
}
if (terminalView == null || terminalView.widget.readOnly) return false;
final controller = terminalView.widget.controller;
if (controller == null ||
controller.suspendedPointerInputs ||
!controller.pointerInput.inputs.contains(PointerInput.tap)) {
return false;
}
cancel();
_pointerId = event.pointer;
_controller = controller;
_ownsControllerSuspension = true;
_releasePending = !deferReport;
_reporting = !deferReport;
_dragged = false;
controller.setSuspendPointerInput(true);
_clearSelection(controller);
final position = _cellAt(event, terminalView);
_lastReportedPosition = position;
if (!deferReport) {
terminal.textInput(
_report(terminal.mouseReportMode, position),
);
}
return true;
}
bool activateDeferredDown(Terminal terminal) {
if (_pointerId == null ||
_controller == null ||
_releasePending ||
!_reportsDrag(terminal.mouseMode)) {
return false;
}
_releasePending = true;
_reporting = true;
_clearSelection(_controller);
terminal.textInput(
_report(terminal.mouseReportMode, _lastReportedPosition),
);
return true;
}
bool handleMove(
PointerMoveEvent event,
Terminal terminal,
TerminalViewState? terminalView, {
void Function(bool dragged)? beforeRelease,
void Function()? onCancel,
}) {
if (event.pointer != _pointerId) return false;
if (terminalView == null) {
onCancel?.call();
cancel();
return true;
}
final reportsDrag = _reportsDrag(terminal.mouseMode);
if (!_hasPrimaryButton(event)) {
if (_releasePending && reportsDrag) {
_finishRelease(
event,
terminal,
terminalView,
beforeRelease: beforeRelease,
);
} else {
onCancel?.call();
}
cancel();
return true;
}
if (!_reporting || !reportsDrag) {
if (!reportsDrag && _releasePending) {
_releasePending = false;
onCancel?.call();
}
_reporting = false;
// Keep ownership until the matching end event to suppress local selection.
final controller = _controller;
scheduleMicrotask(() => _clearSelection(controller));
return true;
}
final position = _cellAt(event, terminalView);
_recordPosition(position);
terminal.textInput(
_report(terminal.mouseReportMode, position, motion: true),
);
final controller = _controller;
scheduleMicrotask(() => _clearSelection(controller));
return true;
}
bool handleEnd(
PointerEvent event,
Terminal terminal,
TerminalViewState? terminalView, {
void Function(bool dragged)? beforeRelease,
void Function()? onCancel,
}) {
if (event.pointer != _pointerId) return false;
if (terminalView != null &&
_releasePending &&
_reportsDrag(terminal.mouseMode)) {
_finishRelease(
event,
terminal,
terminalView,
beforeRelease: beforeRelease,
);
} else {
onCancel?.call();
}
_clearSelection(_controller);
final controller = _controller;
_pointerId = null;
// Keep xterm's tap recognizer suspended for this pointer event.
scheduleMicrotask(() {
if (_pointerId == null && identical(_controller, controller)) {
_clearSelection(controller);
cancel();
}
});
return true;
}
void cancel() {
final controller = _controller;
if (_ownsControllerSuspension) {
controller?.setSuspendPointerInput(false);
}
_pointerId = null;
_controller = null;
_ownsControllerSuspension = false;
_releasePending = false;
_reporting = false;
_dragged = false;
}
void updateController(TerminalController controller) {
final oldController = _controller;
if (_pointerId == null || oldController == null) {
cancel();
return;
}
if (identical(oldController, controller)) return;
if (_ownsControllerSuspension) {
oldController.setSuspendPointerInput(false);
}
final acceptsPointerInput = !controller.suspendedPointerInputs &&
controller.pointerInput.inputs.contains(PointerInput.tap);
_controller = controller;
_ownsControllerSuspension = acceptsPointerInput;
_reporting = _reporting && acceptsPointerInput;
if (_ownsControllerSuspension) controller.setSuspendPointerInput(true);
_clearSelection(controller);
}
void _reportRelease(Terminal terminal, CellOffset position) {
terminal.textInput(
_report(
terminal.mouseReportMode,
position,
release: true,
),
);
}
void _finishRelease(
PointerEvent event,
Terminal terminal,
TerminalViewState terminalView, {
void Function(bool dragged)? beforeRelease,
}) {
final position =
_reporting ? _cellAt(event, terminalView) : _lastReportedPosition;
if (_reporting) _recordPosition(position);
beforeRelease?.call(_dragged);
_reportRelease(terminal, position);
}
void _recordPosition(CellOffset position) {
_dragged = _dragged || position != _lastReportedPosition;
_lastReportedPosition = position;
}
CellOffset _cellAt(PointerEvent event, TerminalViewState terminalView) {
final renderTerminal = terminalView.renderTerminal;
return renderTerminal.getCellOffset(
renderTerminal.globalToLocal(event.position),
);
}
bool _isPrimaryPointer(PointerEvent event, bool reportTouchInput) =>
(event.kind == PointerDeviceKind.mouse ||
reportTouchInput && event.kind == PointerDeviceKind.touch) &&
_hasPrimaryButton(event);
bool _hasPrimaryButton(PointerEvent event) =>
(event.buttons & kPrimaryButton) == kPrimaryButton;
bool _reportsDrag(MouseMode mode) =>
mode == MouseMode.upDownScrollDrag || mode == MouseMode.upDownScrollMove;
void _clearSelection(TerminalController? controller) {
if (controller == null || controller.selection == null) return;
controller.clearSelection();
}
String _report(
MouseReportMode mode,
CellOffset position, {
bool release = false,
bool motion = false,
}) {
final baseButton = motion ? _motionButtonCode : _leftButtonCode;
final button = baseButton | _activeModifierCode();
return encodeTerminalMouseReport(mode, button, position, release: release);
}
}