Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion packages/flterm/lib/src/widgets/terminal_gesture_detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:libghostty/libghostty.dart'
show MouseAction, MouseTracking, Position;
show MouseAction, MouseButton, MouseTracking, Position;
import 'package:meta/meta.dart';

import '../foundation.dart';
Expand Down Expand Up @@ -63,6 +63,16 @@ class _TerminalGestureDetectorState extends State<TerminalGestureDetector> {
onPointerDown: tracked ? _handleTrackedDown : null,
onPointerMove: tracked ? _handleTrackedMove : null,
onPointerUp: tracked ? _handleTrackedUp : null,
// Wheel com mouse tracking ligado (TUIs como claude/vim no alt-buffer) tem
// que virar reporte de mouse pro app, não rolar o scrollback do viewport.
// Sem isso o `Scrollable` filho engole o wheel (e no alt-buffer não há
// scrollback), então o scroll interno do app não funciona. O `Scrollable`
// é neutralizado (`NeverScrollableScrollPhysics`) nesse modo no
// `TerminalView`, evitando dois consumidores disputando o pointer signal.
onPointerSignal: tracked ? _handlePointerSignal : null,
// macOS entrega o trackpad como pan/zoom; encaminha igual ao wheel.
onPointerPanZoomStart: tracked ? _handlePointerPanZoomStart : null,
onPointerPanZoomUpdate: tracked ? _handlePointerPanZoomUpdate : null,
child: TerminalRawGestureDetector(
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
Expand Down Expand Up @@ -277,6 +287,80 @@ class _TerminalGestureDetectorState extends State<TerminalGestureDetector> {
));
}

/// Resíduo fracionário de linha ao encaminhar o wheel (trackpad manda deltas
/// pequenos e frequentes; acumulamos pra não rolar rápido demais).
double _wheelAccum = 0;

/// Pan acumulado do gesto pan/zoom em curso (o [PointerPanZoomUpdateEvent.pan]
/// é acumulado desde o start; derivamos o delta por update).
Offset _panZoomLast = Offset.zero;

/// Wheel via [PointerSignalEvent] — mouse de verdade e, no macOS, o scroll
/// sintetizado do trackpad. Encaminha pro app como reporte de mouse.
void _handlePointerSignal(PointerSignalEvent event) {
if (event is! PointerScrollEvent) return;
if (!_isMouseTracked(HardwareKeyboard.instance.isShiftPressed)) return;
// Mouse = discreto (um notch por evento); trackpad = contínuo (acumula).
_forwardScroll(
event.scrollDelta.dy,
event.localPosition,
discrete: event.kind == PointerDeviceKind.mouse,
);
}

// No macOS o trackpad muitas vezes chega como gesto **pan/zoom** (não como
// PointerScrollEvent), então sem tratar isso o scroll de dois dedos não
// encaminha nada pro app. Mesmo caminho de forward do wheel, contínuo.
void _handlePointerPanZoomStart(PointerPanZoomStartEvent event) {
_panZoomLast = Offset.zero;
_wheelAccum = 0;
}

void _handlePointerPanZoomUpdate(PointerPanZoomUpdateEvent event) {
if (!_isMouseTracked(HardwareKeyboard.instance.isShiftPressed)) return;
final dy = event.pan.dy - _panZoomLast.dy;
_panZoomLast = event.pan;
// Pan tem sinal oposto ao scrollDelta (dedo pra cima = pan.dy negativo =
// ver conteúdo abaixo = scroll down); invertendo, reusa a convenção.
_forwardScroll(-dy, event.localPosition, discrete: false);
}

/// Converte um delta vertical (px, convenção de `scrollDelta`) em passos de
/// linha e encaminha ao app como wheel (botão 4 = cima, 5 = baixo).
/// [discrete] = mouse (≥1 linha/notch, sem acumular); contínuo = trackpad.
void _forwardScroll(
double deltaY,
Offset localPosition, {
required bool discrete,
}) {
final cellHeight = widget.metrics.cellHeight;
if (cellHeight <= 0) return;
final lines = deltaY / cellHeight;
if (lines == 0) return;

final int steps;
if (discrete) {
final mag = lines.abs().round();
steps = (mag < 1 ? 1 : mag) * (lines.isNegative ? -1 : 1);
} else {
_wheelAccum += lines;
steps = _wheelAccum.truncate();
if (steps == 0) return;
_wheelAccum -= steps;
}

// dy < 0 = rolar pra cima = botão 4; > 0 = baixo = botão 5.
final button = steps < 0 ? MouseButton.four : MouseButton.five;
for (var i = 0; i < steps.abs(); i++) {
_binding.handleMouseEvent((
action: MouseAction.press,
button: button,
pixelX: localPosition.dx,
pixelY: localPosition.dy,
));
}
}

void _startAutoScroll() {
if (_autoScrollTimer != null) return;
_autoScrollTimer = Timer.periodic(
Expand Down
9 changes: 8 additions & 1 deletion packages/flterm/lib/src/widgets/terminal_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,14 @@ class _TerminalViewState extends State<TerminalView> {
onLinkActivate: widget.linkSettings.onActivate,
child: Scrollable(
controller: _scrollController,
physics: widget.scrollPhysics,
// Com mouse tracking (TUIs como claude/vim) o wheel é
// encaminhado ao app pelo gesture detector; o scroll do
// viewport é desligado pra os dois não disputarem o
// pointer signal (o que fazia o scroll interno do app não
// funcionar). No alt-buffer não há scrollback pra rolar.
physics: _controller.mouseTracking != .none
? const NeverScrollableScrollPhysics()
: widget.scrollPhysics,
viewportBuilder: (_, offset) => TerminalRenderer(
key: _rendererKey,
theme: _theme,
Expand Down
Loading