fix(flutter): make Adjust Window reliable across desktop platforms (#15853)

* fix(flutter): make Adjust Window reliable across desktop platforms

  - Fix incorrect sizing on scaled displays by calculating the target from the
    rendered canvas scale and platform-specific window coordinate units.
  - Fix adjustments using the wrong monitor by querying the current remote
    window's screen, with the main window as fallback.
  - Fix stale geometry after fullscreen or maximized transitions by refreshing
    metrics before calculating and applying the target frame.
  - Fix fullscreen availability checks on Windows and macOS by predicting the
    restored window borders and caching each macOS window's pre-fullscreen work area.
  - Fix incorrect Linux work areas by handling GNOME Wayland fractional scaling
    and caching compositor/X11 work-area measurements when visibleFrame is wrong.
  - Prevent unsafe adjustments by rejecting invalid, oversized, or implausibly
    small target frames.
  - Avoid failures during window teardown by skipping adjustment when the view,
    screen, or native window frame is unavailable.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): harden Adjust Window handling

  - Use the dynamic Linux resize edge when predicting restored window bounds.
  - Treat GNOME fractional-scaling lookup failures as unknown without repeating
    the lookup for the remote window.
  - Stop adjustment safely when native window calls fail during window teardown.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): correct Linux monitor selection

Update window_size to use monitor height for vertical bounds, preventing incorrect screen selection with vertically stacked displays.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* docs(flutter): simplify Linux screen handling comments

Keep the source rationale concise and move platform measurements and investigation details out of the implementation.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): align Adjust Window resize padding

Use the shared drag-to-resize padding for Linux restored-window predictions so menu validation matches the applied frame dimensions.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): remove Adjust Window screen fallback

Return null when the current window screen is unavailable instead of using the main window's scale factor and work area.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(linux): query Mutter monitor layout mode

  Use DisplayConfig.GetCurrentState instead of inferring scaling from
  experimental features, and handle Ubuntu's UI-scaled logical mode.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): use native maximized state for Wayland cache

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): allow Adjust Window to fill work area

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): avoid racing screen info updates

Signed-off-by: 21pages <sunboeasy@gmail.com>

* refactor(flutter): remove dead Adjust Window web plumbing

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): tolerate near-unity Wayland scale factors

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): harden window screen detection

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(linux): drop deprecated GNOME session detection

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): remove GNOME monitor layout mode flutter cache

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages
2026-08-19 15:08:58 +08:00
committed by GitHub
parent 630b531108
commit 5679670506
11 changed files with 483 additions and 99 deletions

View File

@@ -84,8 +84,6 @@ const double _kPositionEpsilon = 1e-6;
bool get isMainDesktopWindow =>
desktopType == DesktopType.main || desktopType == DesktopType.cm;
String get screenInfo => screenInfo_;
/// Check if the app is running with single view mode.
bool isSingleViewApp() {
return desktopType == DesktopType.cm;

View File

@@ -54,7 +54,6 @@ const String kAppTypeDesktopTerminal = "terminal";
const String kWindowMainWindowOnTop = "main_window_on_top";
const String kWindowRefreshCurrentUser = "refresh_current_user";
const String kWindowGetWindowInfo = "get_window_info";
const String kWindowGetScreenList = "get_screen_list";
// This method is not used, maybe it can be removed.
const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
@@ -324,10 +323,11 @@ double kNewWindowOffset = isWindows
? 30.0
: 50.0;
const kDragToResizeAreaPaddingSize = 5.0;
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
? EdgeInsets.zero
: EdgeInsets.all(5.0)
: EdgeInsets.all(kDragToResizeAreaPaddingSize)
: EdgeInsets.zero;
// https://en.wikipedia.org/wiki/Non-breaking_space
const int $nbsp = 0x00A0;

View File

@@ -780,13 +780,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
windowOnTop(null);
} else if (call.method == kWindowRefreshCurrentUser) {
gFFI.userModel.refreshCurrentUser();
} else if (call.method == kWindowGetWindowInfo) {
final screen = (await window_size.getWindowInfo()).screen;
if (screen == null) {
return '';
} else {
return jsonEncode(screenToMap(screen));
}
} else if (call.method == kWindowGetScreenList) {
return jsonEncode(
(await window_size.getScreenList()).map(screenToMap).toList());

View File

@@ -9,7 +9,6 @@ import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@@ -1334,6 +1333,12 @@ class ScreenAdjustor {
final FFI ffi;
final VoidCallback cbExitFullscreen;
window_size.Screen? _screen;
Size? _waylandMaximizedWorkAreaSize;
Rect? _waylandWorkAreaScreenFrame;
double? _waylandWorkAreaScaleFactor;
Rect? _x11WorkArea;
Rect? _x11WorkAreaScreenFrame;
double? _x11WorkAreaScaleFactor;
ScreenAdjustor({
required this.id,
@@ -1344,9 +1349,18 @@ class ScreenAdjustor {
bool get isFullscreen => stateGlobal.fullscreen.isTrue;
int get windowId => stateGlobal.windowId;
Future<bool?> isWindowMaximized() async {
try {
return await WindowController.fromWindowId(windowId).isMaximized();
} catch (_) {
// The delayed resolution callback may run after the window is disposed.
return null;
}
}
adjustWindow(BuildContext context) {
return futureBuilder(
future: isWindowCanBeAdjusted(),
future: isWindowCanBeAdjusted(context),
hasData: (data) {
final visible = data as bool;
if (!visible) return Offstage();
@@ -1362,36 +1376,201 @@ class ScreenAdjustor {
});
}
doAdjustWindow(BuildContext context) async {
await updateScreen();
if (_screen != null) {
cbExitFullscreen();
double scale = _screen!.scaleFactor;
final wndRect = await WindowController.fromWindowId(windowId).getFrame();
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
// On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
// Linux screen and work-area coordinates can use different units or become
// unreliable across Wayland/X11 state changes, so normalize reported frames
// and cache usable work-area measurements before sizing the window.
Future<void> _updateLinuxWorkAreaCache({
required window_size.Screen screen,
required Rect wndRect,
required bool isWayland,
required bool isX11,
required bool forMenu,
}) async {
if (isWayland &&
(_waylandWorkAreaScreenFrame != screen.frame ||
_waylandWorkAreaScaleFactor != screen.scaleFactor)) {
_waylandMaximizedWorkAreaSize = null;
_waylandWorkAreaScreenFrame = screen.frame;
_waylandWorkAreaScaleFactor = screen.scaleFactor;
}
if (isWayland &&
forMenu &&
!isFullscreen &&
await isWindowMaximized() == true) {
_waylandMaximizedWorkAreaSize = wndRect.size;
}
if (isX11 &&
(_x11WorkAreaScreenFrame != screen.frame ||
_x11WorkAreaScaleFactor != screen.scaleFactor)) {
_x11WorkArea = null;
_x11WorkAreaScreenFrame = screen.frame;
_x11WorkAreaScaleFactor = screen.scaleFactor;
}
if (isX11 && forMenu && !isFullscreen) {
_x11WorkArea = screen.visibleFrame;
}
}
Future<Rect?> _getEffectiveScreenFrame({
required window_size.Screen screen,
required bool isWayland,
required bool isX11,
required bool forMenu,
}) async {
Rect frameRect = screen.visibleFrame;
if (isMacOS && forMenu && isFullscreen) {
List<double>? workArea;
try {
workArea = await kMacOSPermChannel
.invokeListMethod<double>('getMacOSWorkAreaSize');
} catch (_) {
return null;
}
if (workArea == null || workArea.length != 2) {
return null;
}
frameRect = Rect.fromLTWH(
frameRect.left,
frameRect.top,
workArea[0] < frameRect.width ? workArea[0] : frameRect.width,
workArea[1] < frameRect.height ? workArea[1] : frameRect.height,
);
}
final x11WorkArea = _x11WorkArea;
if (isX11 &&
forMenu &&
isFullscreen &&
x11WorkArea != null &&
(x11WorkArea.width < frameRect.width ||
x11WorkArea.height < frameRect.height)) {
frameRect = x11WorkArea;
}
final screenScale = screen.scaleFactor;
if (isWayland && screenScale > 1.01) {
String monitorLayoutMode;
try {
monitorLayoutMode =
await bind.mainGetCommon(key: 'gnome-monitor-layout-mode');
} catch (_) {
monitorLayoutMode = '';
}
if (monitorLayoutMode == 'physical') {
frameRect = Rect.fromLTRB(
frameRect.left / screenScale,
frameRect.top / screenScale,
frameRect.right / screenScale,
frameRect.bottom / screenScale,
);
}
}
return frameRect;
}
Future<Rect?> _getAdjustedWindowFrame(Size mediaSize,
{bool forMenu = false}) async {
final screen = _screen;
if (screen != null) {
// Windows window frames use physical pixels while Flutter view sizes are
// logical. macOS and Linux window frames use the same units as Flutter.
double scale = isWindows ? screen.scaleFactor : 1.0;
final Rect wndRect;
try {
wndRect = await WindowController.fromWindowId(windowId).getFrame();
} catch (e) {
debugPrint("Failed to get frame of window $windowId, it may be hidden");
return null;
}
// On Windows, wndRect is GetWindowRect while mediaSize is GetClientRect.
// https://stackoverflow.com/a/7561083
double magicWidth =
wndRect.right - wndRect.left - mediaSize.width * scale;
double magicHeight =
wndRect.bottom - wndRect.top - mediaSize.height * scale;
final canvasModel = ffi.canvasModel;
// canvasModel.scale is the rendered scale and already applies kIgnoreDpi.
// Use it instead of the remote source resolution.
final isWayland = isLinux && bind.mainCurrentIsWayland();
final isX11 = isLinux && !isWayland;
await _updateLinuxWorkAreaCache(
screen: screen,
wndRect: wndRect,
isWayland: isWayland,
isX11: isX11,
forMenu: forMenu,
);
if (isWindows && forMenu && isFullscreen) {
// desktop_multi_window's hidden title bar keeps 8 physical pixels on
// each horizontal edge and at the bottom, plus up to 1px at the top.
// Fullscreen removes these in WM_NCCALCSIZE, so predict the restored
// frame's worst-case padding when deciding whether to show the menu.
magicWidth = 16.0;
magicHeight = 9.0;
}
double horizontalEdges;
double verticalEdges;
if (forMenu && (isLinux || ((isMacOS || isWindows) && isFullscreen))) {
// Linux Adjust Window unmaximizes; macOS and Windows exit fullscreen
// before resizing. Predict the restored normal-window edges when
// deciding whether to show the menu item.
final resizePadding = isLinux && !kUseCompatibleUiMode
? kDragToResizeAreaPaddingSize
: 0.0;
final windowEdge = kWindowBorderWidth + resizePadding;
horizontalEdges = windowEdge * 2;
verticalEdges = kDesktopRemoteTabBarHeight + windowEdge * 2;
} else {
horizontalEdges = CanvasModel.leftToEdge + CanvasModel.rightToEdge;
verticalEdges = CanvasModel.topToEdge + CanvasModel.bottomToEdge;
}
final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
CanvasModel.leftToEdge +
CanvasModel.rightToEdge) *
horizontalEdges) *
scale +
magicWidth;
final height = (canvasModel.getDisplayHeight() * canvasModel.scale +
CanvasModel.topToEdge +
CanvasModel.bottomToEdge) *
scale +
magicHeight;
final height =
(canvasModel.getDisplayHeight() * canvasModel.scale + verticalEdges) *
scale +
magicHeight;
double left = wndRect.left + (wndRect.width - width) / 2;
double top = wndRect.top + (wndRect.height - height) / 2;
Rect frameRect = _screen!.frame;
if (!isFullscreen) {
frameRect = _screen!.visibleFrame;
final frameRect = await _getEffectiveScreenFrame(
screen: screen,
isWayland: isWayland,
isX11: isX11,
forMenu: forMenu,
);
if (frameRect == null) {
return null;
}
var availableSize = frameRect.size;
if (isWayland && forMenu && _waylandMaximizedWorkAreaSize != null) {
final cachedSize = _waylandMaximizedWorkAreaSize!;
availableSize = Size(
cachedSize.width < availableSize.width
? cachedSize.width
: availableSize.width,
cachedSize.height < availableSize.height
? cachedSize.height
: availableSize.height,
);
}
// A window frame cannot be smaller than its client area. Tolerate small
// floating-point differences; larger negative values mean the native
// frame and Flutter view metrics are not synchronized.
if (magicWidth < -0.1 || magicHeight < -0.1) {
return null;
}
// Reject implausibly small targets to avoid hiding the window.
if (width < 300 || height < 300) {
return null;
}
// The remote size may change after the menu is built. Reject targets
// that exceed the available area.
final exceedsScreen =
width > availableSize.width || height > availableSize.height;
if (exceedsScreen) {
return null;
}
if (left < frameRect.left) {
left = frameRect.left;
@@ -1405,69 +1584,101 @@ class ScreenAdjustor {
if ((top + height) > frameRect.bottom) {
top = frameRect.bottom - height;
}
await WindowController.fromWindowId(windowId)
.setFrame(Rect.fromLTWH(left, top, width, height));
return Rect.fromLTWH(left, top, width, height);
}
return null;
}
doAdjustWindow([BuildContext? context]) async {
// A resolution change is adjusted after a delay, when the menu context may
// already be disposed. Each desktop_multi_window window has its own engine,
// so that engine's first view is the current window.
final views = WidgetsBinding.instance.platformDispatcher.views;
if (context == null && views.isEmpty) {
return;
}
final view = context != null ? View.of(context) : views.first;
await updateScreen();
if (_screen != null) {
final wc = WindowController.fromWindowId(windowId);
final wasFullscreen = isFullscreen;
cbExitFullscreen();
if (wasFullscreen) {
// Wait for the native fullscreen exit to update the window frame.
await Future.delayed(Duration(milliseconds: 700));
await updateScreen();
}
if (isLinux) {
final isMaximized = await isWindowMaximized();
if (isMaximized == null) {
return;
}
if (isMaximized == true) {
// setFrame may be ignored while the native window is maximized.
try {
await wc.unmaximize();
} catch (_) {
return;
}
stateGlobal.setMaximized(false);
// Wait for the window manager and Flutter view metrics to reflect
// the restored window before calculating and setting its frame.
await Future.delayed(Duration(milliseconds: 300));
await updateScreen();
}
}
final mediaSize = MediaQueryData.fromView(view).size;
final frame = await _getAdjustedWindowFrame(mediaSize);
if (frame == null) {
return;
}
try {
await wc.setFrame(frame);
} catch (_) {
return;
}
stateGlobal.setMaximized(false);
}
}
updateScreen() async {
final String info =
isWeb ? screenInfo : await _getScreenInfoDesktop() ?? '';
if (info.isEmpty) {
_screen = null;
} else {
final screenMap = jsonDecode(info);
_screen = window_size.Screen(
Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'],
screenMap['frame']['r'], screenMap['frame']['b']),
Rect.fromLTRB(
screenMap['visibleFrame']['l'],
screenMap['visibleFrame']['t'],
screenMap['visibleFrame']['r'],
screenMap['visibleFrame']['b']),
screenMap['scaleFactor']);
_screen = await _getCurrentScreen();
}
Future<window_size.Screen?> _getCurrentScreen() async {
try {
return (await window_size.getWindowInfo()).screen;
} catch (e) {
debugPrint('Failed to get current window screen: $e');
return null;
}
}
_getScreenInfoDesktop() async {
final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, '');
return v.result;
}
Future<bool> isWindowCanBeAdjusted() async {
Future<bool> isWindowCanBeAdjusted([BuildContext? context]) async {
if (isWeb) {
return false;
}
// Capture the view before awaiting because the menu context may be disposed.
final views = WidgetsBinding.instance.platformDispatcher.views;
if (context == null && views.isEmpty) {
return false;
}
final view = context != null ? View.of(context) : views.first;
final mediaSize = MediaQueryData.fromView(view).size;
final viewStyle =
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
if (viewStyle != kRemoteViewStyleOriginal) {
return false;
}
if (!isWeb) {
final remoteCount = RemoteCountState.find().value;
if (remoteCount != 1) {
return false;
}
final remoteCount = RemoteCountState.find().value;
if (remoteCount != 1) {
return false;
}
await updateScreen();
if (_screen == null) {
return false;
}
final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor;
double selfWidth = _screen!.visibleFrame.width;
double selfHeight = _screen!.visibleFrame.height;
if (isFullscreen) {
selfWidth = _screen!.frame.width;
selfHeight = _screen!.frame.height;
}
final canvasModel = ffi.canvasModel;
final displayWidth = canvasModel.getDisplayWidth();
final displayHeight = canvasModel.getDisplayHeight();
final requiredWidth =
CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge;
final requiredHeight =
CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge;
return selfWidth > (requiredWidth * scale) &&
selfHeight > (requiredHeight * scale);
return await _getAdjustedWindowFrame(mediaSize, forMenu: true) != null;
}
}
@@ -1518,7 +1729,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
_screenAdjustor.updateScreen();
menuChildrenGetter(_IconSubmenuButtonState state) {
final menuChildren = <Widget>[
_screenAdjustor.adjustWindow(context),
@@ -2082,15 +2292,19 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
Future<void> _getLocalResolutionWayland() async {
if (!isWayland) return _getLocalResolution();
final window = await window_size.getWindowInfo();
final screen = window.screen;
if (screen != null) {
setState(() {
_localResolution = Resolution(
screen.frame.width.toInt(),
screen.frame.height.toInt(),
);
});
try {
final window = await window_size.getWindowInfo();
final screen = window.screen;
if (screen != null) {
setState(() {
_localResolution = Resolution(
screen.frame.width.toInt(),
screen.frame.height.toInt(),
);
});
}
} catch (e) {
debugPrint('Failed to get local resolution on Wayland: $e');
}
}
@@ -2162,8 +2376,16 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
return;
}
if (w == rect.width.toInt() && h == rect.height.toInt()) {
if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
widget.screenAdjustor.doAdjustWindow(context);
if (!await widget.screenAdjustor.isWindowCanBeAdjusted()) {
return;
}
if (widget.screenAdjustor.isFullscreen) {
return;
}
if ((await widget.screenAdjustor.isWindowMaximized()) == false) {
// This delayed callback can outlive the menu State, so its context
// is unsafe.
widget.screenAdjustor.doAdjustWindow();
}
}
});

View File

@@ -10,8 +10,6 @@ final isWebDesktop_ = false;
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
String get screenInfo_ => '';
final isWebOnWindows_ = false;
final isWebOnLinux_ = false;
final isWebOnMacOS_ = false;

View File

@@ -1,5 +1,4 @@
import 'dart:js' as js;
import 'dart:html' as html;
// cycle imports, maybe we can improve this
import 'package:flutter_hbb/consts.dart';
@@ -13,8 +12,6 @@ final isWebDesktop_ = !js.context.callMethod('isMobile');
final isDesktop_ = false;
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
final _localOs = js.context.callMethod('getByName', ['local_os', '']);
final isWebOnWindows_ = _localOs == kPeerPlatformWindows;
final isWebOnLinux_ = _localOs == kPeerPlatformLinux;

View File

@@ -36,8 +36,28 @@ class RelativeMouseState {
}
class MainFlutterWindow: NSWindow {
private static let fullscreenWorkAreaSizes = NSMapTable<NSWindow, NSValue>(
keyOptions: [.weakMemory, .objectPointerPersonality],
valueOptions: .strongMemory
)
private static let fullscreenObserver = NotificationCenter.default.addObserver(
forName: NSWindow.willEnterFullScreenNotification,
object: nil,
queue: .main
) { notification in
guard let window = notification.object as? NSWindow,
let screen = window.screen else {
return
}
fullscreenWorkAreaSizes.setObject(
NSValue(size: screen.visibleFrame.size),
forKey: window
)
}
override func awakeFromNib() {
rustdesk_core_main();
_ = MainFlutterWindow.fullscreenObserver
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
self.contentViewController = flutterViewController
@@ -278,6 +298,16 @@ class MainFlutterWindow: NSWindow {
self.disableNativeRelativeMouseMode()
result(true)
case "getMacOSWorkAreaSize":
guard Thread.isMainThread,
let window = registrar.view?.window,
let size = MainFlutterWindow.fullscreenWorkAreaSizes
.object(forKey: window)?.sizeValue else {
result(nil)
break
}
result([Double(size.width), Double(size.height)])
default:
result(FlutterMethodNotImplemented)
}

View File

@@ -1597,9 +1597,9 @@ packages:
dependency: "direct main"
description:
path: "plugins/window_size"
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
url: "https://github.com/google/flutter-desktop-embedding.git"
ref: "51e67ce047c72b26810b99e8473ddb44612fe356"
resolved-ref: "51e67ce047c72b26810b99e8473ddb44612fe356"
url: "https://github.com/21pages/flutter-desktop-embedding.git"
source: git
version: "0.1.0"
xdg_directories:

View File

@@ -61,9 +61,9 @@ dependencies:
flutter_custom_cursor: ^0.0.4
window_size:
git:
url: https://github.com/google/flutter-desktop-embedding.git
url: https://github.com/21pages/flutter-desktop-embedding.git
path: plugins/window_size
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
ref: 51e67ce047c72b26810b99e8473ddb44612fe356
get: ^4.6.5
visibility_detector: ^0.4.0+2
contextmenu: ^3.0.0

View File

@@ -2655,6 +2655,14 @@ pub fn main_get_common(key: String) -> String {
return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string();
#[cfg(not(target_os = "linux"))]
return false.to_string();
} else if key == "gnome-monitor-layout-mode" {
#[cfg(target_os = "linux")]
return match crate::platform::linux::gnome_monitor_layout_mode() {
Some(mode) => mode.as_str().to_owned(),
None => String::new(),
};
#[cfg(not(target_os = "linux"))]
return String::new();
} else if key == "permanent-password-set" {
return ui_interface::is_permanent_password_set().to_string();
} else if key == "local-permanent-password-set" {

View File

@@ -96,6 +96,9 @@ lazy_static::lazy_static! {
};
static ref ACTIVE_USER_LOOKUP_CACHE: std::sync::Mutex<Option<ActiveUserLookupCache>> =
std::sync::Mutex::new(None);
static ref GNOME_MONITOR_LAYOUT_MODE_CACHE: std::sync::Mutex<
Option<(Instant, Option<GnomeMonitorLayoutMode>)>,
> = Default::default();
// https://github.com/rustdesk/rustdesk/issues/13705
// Check if `sudo -E` actually preserves environment.
//
@@ -128,6 +131,141 @@ lazy_static::lazy_static! {
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GnomeMonitorLayoutMode {
Logical,
Physical,
}
impl GnomeMonitorLayoutMode {
pub fn as_str(self) -> &'static str {
match self {
Self::Logical => "logical",
Self::Physical => "physical",
}
}
}
fn gnome_monitor_layout_mode_from_value(value: u32) -> Option<GnomeMonitorLayoutMode> {
// Upstream: https://gitlab.gnome.org/GNOME/mutter/-/blob/main/data/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml
// Ubuntu mode 3: https://git.launchpad.net/ubuntu/+source/mutter/tree/debian/patches/x11-Add-support-for-fractional-scaling-using-Randr.patch
match value {
1 | 3 => Some(GnomeMonitorLayoutMode::Logical),
2 => Some(GnomeMonitorLayoutMode::Physical),
_ => None,
}
}
pub fn gnome_monitor_layout_mode() -> Option<GnomeMonitorLayoutMode> {
if let Ok(cache) = GNOME_MONITOR_LAYOUT_MODE_CACHE.lock() {
if let Some((updated_at, result)) = *cache {
if updated_at.elapsed() < Duration::from_secs(10) {
return result;
}
}
}
let result = (|| {
let is_gnome_desktop = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.split(':')
.any(|desktop| {
desktop.eq_ignore_ascii_case("gnome") || desktop.eq_ignore_ascii_case("unity")
});
let is_gnome_session = std::env::var("DESKTOP_SESSION")
.unwrap_or_default()
.to_ascii_lowercase();
if !is_gnome_desktop && !is_gnome_session.contains("gnome") {
return None;
}
use dbus::{arg::PropMap, blocking::BlockingSender};
let conn = match dbus::blocking::Connection::new_session() {
Ok(conn) => conn,
Err(err) => {
log::warn!("Failed to connect to the session bus for GNOME monitor layout: {err}");
return None;
}
};
let message = match dbus::Message::new_method_call(
"org.gnome.Mutter.DisplayConfig",
"/org/gnome/Mutter/DisplayConfig",
"org.gnome.Mutter.DisplayConfig",
"GetCurrentState",
) {
Ok(message) => message,
Err(err) => {
log::warn!("Failed to create GNOME monitor layout query: {err}");
return None;
}
};
let reply = match conn.send_with_reply_and_block(message, Duration::from_secs(2)) {
Ok(reply) => reply,
Err(err) => {
log::warn!("Failed to query GNOME monitor layout: {err}");
return None;
}
};
let mut args = reply.iter_init();
for _ in 0..3 {
if !args.next() {
log::warn!("GNOME monitor layout reply is missing properties");
return None;
}
}
let properties: PropMap = match args.read() {
Ok(properties) => properties,
Err(err) => {
log::warn!("Failed to read GNOME monitor layout properties: {err}");
return None;
}
};
let Some(value) = dbus::arg::prop_cast::<u32>(&properties, "layout-mode").copied() else {
log::warn!("GNOME monitor layout reply has no layout-mode");
return None;
};
let mode = gnome_monitor_layout_mode_from_value(value);
if mode.is_none() {
log::warn!("GNOME monitor layout reply has unknown layout-mode {value}");
}
mode
})();
if let Ok(mut cache) = GNOME_MONITOR_LAYOUT_MODE_CACHE.lock() {
*cache = Some((Instant::now(), result));
}
result
}
#[cfg(test)]
mod gnome_monitor_layout_tests {
use super::*;
#[test]
fn maps_logical_layouts() {
assert_eq!(
gnome_monitor_layout_mode_from_value(1),
Some(GnomeMonitorLayoutMode::Logical)
);
assert_eq!(
gnome_monitor_layout_mode_from_value(3),
Some(GnomeMonitorLayoutMode::Logical)
);
}
#[test]
fn maps_physical_layout() {
assert_eq!(
gnome_monitor_layout_mode_from_value(2),
Some(GnomeMonitorLayoutMode::Physical)
);
}
#[test]
fn rejects_unknown_layout() {
assert_eq!(gnome_monitor_layout_mode_from_value(4), None);
}
}
#[inline]
fn update_active_user_lookup_cache(desktop: &Desktop) {
if let Ok(mut cache) = ACTIVE_USER_LOOKUP_CACHE.lock() {