mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-04-15 12:21:28 +03:00
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 <linlong1266@gmail.com> * fix(ios): terminal swip exit gesture Signed-off-by: fufesou <linlong1266@gmail.com> * Update flutter/lib/mobile/pages/terminal_page.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> 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 <linlong1266@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
@@ -41,6 +42,9 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
final GlobalKey _keyboardKey = GlobalKey();
|
final GlobalKey _keyboardKey = GlobalKey();
|
||||||
double _keyboardHeight = 0;
|
double _keyboardHeight = 0;
|
||||||
late bool _showTerminalExtraKeys;
|
late bool _showTerminalExtraKeys;
|
||||||
|
// For iOS edge swipe gesture
|
||||||
|
double _swipeStartX = 0;
|
||||||
|
double _swipeCurrentX = 0;
|
||||||
|
|
||||||
// For web only.
|
// For web only.
|
||||||
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
|
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
|
||||||
@@ -147,7 +151,7 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildBody() {
|
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
|
resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
@@ -192,9 +196,108 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_showTerminalExtraKeys) _buildFloatingKeyboard(),
|
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: <Type, GestureRecognizerFactory>{
|
||||||
|
HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
|
||||||
|
() => 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() {
|
Widget _buildFloatingKeyboard() {
|
||||||
|
|||||||
Reference in New Issue
Block a user