From 8c6dcf53a6bcf8eb95536ee1a86d222b9167903a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:37:45 +0800 Subject: [PATCH] iOS terminal: Add touch swipe and floating back button for exit (#14208) * Initial plan * Add iOS edge swipe gesture to exit terminal session Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Improve iOS edge swipe gesture with responsive thresholds and better gesture handling Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Fix: Reset _swipeCurrentX in onHorizontalDragStart to prevent stale state Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Add trackpad support documentation for iOS edge swipe gesture Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Add iOS-style circular back button to terminal page Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Remove trackpad support documentation - not needed with back button Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Filter edge swipe gesture to touch-only input (exclude mouse/trackpad) Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * fix: missing import Signed-off-by: fufesou * fix(ios): terminal swip exit gesture Signed-off-by: fufesou * Update flutter/lib/mobile/pages/terminal_page.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: fufesou Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> Co-authored-by: fufesou Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- flutter/lib/mobile/pages/terminal_page.dart | 105 +++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index 67d77782f..ab34a35ec 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -41,6 +42,9 @@ class _TerminalPageState extends State final GlobalKey _keyboardKey = GlobalKey(); double _keyboardHeight = 0; late bool _showTerminalExtraKeys; + // For iOS edge swipe gesture + double _swipeStartX = 0; + double _swipeCurrentX = 0; // For web only. // 'monospace' does not work on web, use Google Fonts, `??` is only for null safety. @@ -147,7 +151,7 @@ class _TerminalPageState extends State } Widget buildBody() { - return Scaffold( + final scaffold = Scaffold( resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Stack( @@ -192,9 +196,108 @@ class _TerminalPageState extends State ), ), if (_showTerminalExtraKeys) _buildFloatingKeyboard(), + // iOS-style circular close button in top-right corner + if (isIOS) _buildCloseButton(), ], ), ); + + // Add iOS edge swipe gesture to exit (similar to Android back button) + if (isIOS) { + return LayoutBuilder( + builder: (context, constraints) { + final screenWidth = constraints.maxWidth; + // Base thresholds on screen width but clamp to reasonable logical pixel ranges + // Edge detection region: ~10% of width, clamped between 20 and 80 logical pixels + final edgeThreshold = (screenWidth * 0.1).clamp(20.0, 80.0); + // Required horizontal movement: ~25% of width, clamped between 80 and 300 logical pixels + final swipeThreshold = (screenWidth * 0.25).clamp(80.0, 300.0); + + return RawGestureDetector( + behavior: HitTestBehavior.translucent, + gestures: { + HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => HorizontalDragGestureRecognizer( + debugOwner: this, + // Only respond to touch input, exclude mouse/trackpad + supportedDevices: kTouchBasedDeviceKinds, + ), + (HorizontalDragGestureRecognizer instance) { + instance + // Capture initial touch-down position (before touch slop) + ..onDown = (details) { + _swipeStartX = details.localPosition.dx; + _swipeCurrentX = details.localPosition.dx; + } + ..onUpdate = (details) { + _swipeCurrentX = details.localPosition.dx; + } + ..onEnd = (details) { + // Check if swipe started from left edge and moved right + if (_swipeStartX < edgeThreshold && (_swipeCurrentX - _swipeStartX) > swipeThreshold) { + clientClose(sessionId, _ffi); + } + _swipeStartX = 0; + _swipeCurrentX = 0; + } + ..onCancel = () { + _swipeStartX = 0; + _swipeCurrentX = 0; + }; + }, + ), + }, + child: scaffold, + ); + }, + ); + } + + return scaffold; + } + + Widget _buildCloseButton() { + return Positioned( + top: 0, + right: 0, + child: SafeArea( + minimum: const EdgeInsets.only( + top: 16, // iOS standard margin + right: 16, // iOS standard margin + ), + child: Semantics( + button: true, + label: translate('Close'), + child: Container( + width: 44, // iOS standard tap target size + height: 44, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), // Half transparency + shape: BoxShape.circle, + ), + child: Material( + color: Colors.transparent, + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + child: InkWell( + customBorder: const CircleBorder(), + onTap: () { + clientClose(sessionId, _ffi); + }, + child: Tooltip( + message: translate('Close'), + child: const Icon( + Icons.chevron_left, // iOS-style back arrow + color: Colors.white, + size: 28, + ), + ), + ), + ), + ), + ), + ), + ); } Widget _buildFloatingKeyboard() {