Compare commits

...
Author SHA1 Message Date
dijunkun 3d2235ad4e [fix] statically link the Windows CRT in Slint 2026-09-27 03:35:06 +08:00
dijunkun 0b2eb5b106 [fix] bundle missing Slint runtime in Windows packages 2026-09-27 02:52:56 +08:00
dijunkun 94760b2a3c [fix] keep desktop UI readable under dark system themes, refs #103 2026-09-27 02:34:14 +08:00
dijunkun bbbcfb9d6f [fix] correct video latency measurement and smooth connection RTT 2026-09-26 03:15:47 +08:00
dijunkun 9ef64cc00f [fix] reduce release log verbosity and enforce single instance 2026-09-26 02:58:55 +08:00
dijunkun b8389a8aa0 [feat] add connection latency to network statistics 2026-09-26 02:04:18 +08:00
dijunkun 7ac8163931 [feat] add video latency to network statistics 2026-09-26 00:39:05 +08:00
dijunkun 704938a157 [feat] update minirtc with adaptive media FEC 2026-09-25 23:39:23 +08:00
dijunkun 572a018a72 [docs] refresh README guides and desktop and web screenshots 2026-09-25 03:26:57 +08:00
dijunkun 41cab6fa0b [fix] render toolbar off-state slashes correctly in software mode 2026-09-25 03:26:49 +08:00
dijunkun f73a57baaa [fix] restore missing includes for Linux and Windows builds 2026-09-25 02:20:02 +08:00
dijunkun e62d473b64 [perf] reduce desktop build dependencies and binary size 2026-09-25 01:47:20 +08:00
dijunkun 3c5b3f8e38 [fix] add missing JSON include for Windows service runtime 2026-09-25 00:51:08 +08:00
kunkundi 191ba26772 [feat] add Linux headless mode and interactive console 2026-09-25 00:29:24 +08:00
dijunkun c661319890 [feat] add background and manual update checks 2026-09-24 17:36:41 +08:00
dijunkun e0cd4c0567 [feat] add live remote video settings to the toolbar 2026-09-24 15:44:02 +08:00
dijunkun 88da1081ef [release] prepare v1.5.2 release notes 2026-09-24 11:15:58 +08:00
dijunkun b9efb5944c [fix] recover headless capture and use WGC for usbmmidd displays 2026-09-23 14:40:58 +08:00
dijunkun 5f26a0e239 [feat] add force relay connection option to settings 2026-09-22 11:03:51 +08:00
dijunkun 938ce01513 [fix] align password reset input style and limit it to six characters 2026-09-22 10:47:42 +08:00
dijunkun 2235d5cb62 [fix] place the cursor at the end of the password on retry 2026-09-22 10:39:27 +08:00
dijunkun 7e3e050edf [feat] add random password refresh to the main window 2026-09-22 10:33:28 +08:00
dijunkun b610a0ec7e [fix] anchor display menu to the toolbar when network stats are expanded 2026-09-22 10:19:16 +08:00
dijunkun 120368e4f7 [fix] align remote mouse coordinates with the displayed video area 2026-09-22 10:06:41 +08:00
dijunkun b34755c5ee [chore] bundle the usbmmidd_v2 virtual display driver with Windows packages 2026-09-21 01:10:31 +08:00
dijunkun 6387f2e555 [feat] add headless compatibility capture and usbmmidd virtual display plugging 2026-09-21 01:10:30 +08:00
dijunkun fb556a763d [fix] keep Windows privacy screen across display changes and validate covers 2026-09-21 01:10:30 +08:00
dijunkun 276385b816 [fix] keep Windows display slots and input mapping stable across hotplug 2026-09-21 01:09:12 +08:00
dijunkun 1a59a6ac2e [refactor] build host information in the wire library 2026-09-21 00:56:16 +08:00
dijunkun 485e347337 [fix] render CPU NV12 native frames rejected by the native renderer 2026-09-21 00:51:50 +08:00
dijunkun 89b39803e8 [fix] reuse stale sessions and stored passwords when reconnecting 2026-09-21 00:51:50 +08:00
dijunkun 4797af9260 [fix] correct Windows remote control session routing and key mapping 2026-09-19 03:59:35 +08:00
dijunkun 000b2e44e3 [fix] restore Windows cursor capture and web input control 2026-09-18 23:26:06 +08:00
dijunkun 26217f9d18 [fix] reset mouse input display when restoring initial capture display 2026-09-18 22:40:51 +08:00
dijunkun c2ea90c740 [chore] remove unused ImGui code and old release notes 2026-09-18 19:54:47 +08:00
dijunkun 0eadcf2855 [fix] diagnose and serialize Linux image dependency installation 2026-09-17 12:42:44 +08:00
dijunkun e263aab10d [fix] keep Windows privacy screen active when running as administrator 2026-09-17 09:55:34 +08:00
dijunkun 9abdfbf684 [release] prepare v1.5.1 release notes 2026-09-17 02:56:49 +08:00
dijunkun 31a0b86cee [fix] refine network stats layout and alignment 2026-09-17 02:45:35 +08:00
dijunkun fb34f42744 [fix] show encryption status and simplify network stats labels 2026-09-17 02:01:55 +08:00
dijunkun 4302ab291c [fix] overlay control bar tooltips on expanded network stats 2026-09-17 01:39:09 +08:00
dijunkun 4bf92c9e2c [fix] use reliable transport for mouse input and cursor state 2026-09-17 01:31:54 +08:00
dijunkun 28de4cf938 [fix] improve Windows taskbar icon clarity at small sizes 2026-09-16 23:19:12 +08:00
dijunkun ce7a3c178e [fix] correct WGC and GDI cursor capture and positioning 2026-09-16 23:03:36 +08:00
dijunkun d9116200fe [fix] restore remote cursors on Windows without a physical mouse 2026-09-16 22:20:36 +08:00
dijunkun 1d9242515a [fix] restore remote cursor visibility during Windows privacy screen 2026-09-16 20:27:59 +08:00
dijunkun ad62e60b9a [fix] restore shared capture on Windows secure desktop 2026-09-16 15:31:27 +08:00
dijunkun c4aca303c8 [fix] restore cursor visibility on Windows lock screen 2026-09-16 14:48:39 +08:00
dijunkun 57bb91dba3 [fix] prevent duplicate cursors during Windows window dragging 2026-09-16 13:51:35 +08:00
dijunkun 57cab3979e [fix] initialize logging before startup operations and update MiniRTC 2026-09-16 12:54:07 +08:00
dijunkun d8ccdbaa38 [fix] add capture delivery diagnostics 2026-09-16 12:54:06 +08:00
dijunkun f3e67d7d6e [fix] recognize macOS window resize cursors 2026-09-15 16:44:19 +08:00
dijunkun d648585891 [fix] restore Linux X11 remote keyboard capture 2026-09-15 16:23:42 +08:00
dijunkun e40c7fca98 [fix] disable privacy screen for legacy and web clients 2026-09-15 13:04:53 +08:00
dijunkun f6324416ec [fix] update MiniRTC legacy DTLS-SRTP compatibility 2026-09-15 12:20:00 +08:00
dijunkun 0977bd4195 [fix] update MiniRTC SRTP replay handling and bandwidth accounting 2026-09-15 03:38:29 +08:00
dijunkun f4e094cb35 [fix] update MiniRTC packet validation and SRTP buffering 2026-09-15 03:16:18 +08:00
dijunkun 1fe706c5fc [fix] update MiniRTC SRTP initialization 2026-09-15 03:03:47 +08:00
dijunkun 5fd928ddba [fix] update MiniRTC DTLS-SRTP key layout and profiles 2026-09-15 02:53:25 +08:00
dijunkun 977899b240 [feat] enable iOS punching and preserve connection errors 2026-09-15 02:37:30 +08:00
dijunkun 7fc1344756 [feat] enable enhanced UDP punching on desktop 2026-09-15 02:37:30 +08:00
dijunkun 73e3bf709d [perf] exclude optional libaom from default builds 2026-09-13 06:43:29 +08:00
dijunkun 6d90c28513 [perf] strip packaged Slint runtime symbols 2026-09-13 06:39:26 +08:00
dijunkun c944db6519 [perf] scope desktop build variant and version defines 2026-09-13 06:39:23 +08:00
dijunkun fc921c9ca1 [perf] avoid redundant iOS native archive rebuilds 2026-09-13 06:36:05 +08:00
dijunkun ba2afc982b [fix] repair macOS Intel OpenH264 CI build 2026-09-13 06:04:23 +08:00
dijunkun c6635fbdc8 [feat] add Linux X11 privacy screen and automatic recovery 2026-09-13 03:49:17 +08:00
dijunkun 3c403f6162 [fix] update minirtc for SIMD and frame rate fixes 2026-09-13 02:40:44 +08:00
dijunkun b54d32ef41 [fix] disable and uncheck hardware codec setting in unsupported builds 2026-09-13 01:40:44 +08:00
dijunkun 28e9626e0d [fix] preserve Windows privacy startup and server window 2026-09-13 01:23:30 +08:00
kunkundi a6e325f190 [feat] add macOS privacy screen and keep remote sessions running 2026-09-13 00:38:40 +08:00
dijunkun 23bf2ff8b8 [feat] add compact tooltips for all control bar buttons 2026-09-12 22:39:44 +08:00
dijunkun 56ebfdf8e7 [feat] add dedicated privacy screen controls 2026-09-12 22:24:09 +08:00
kunkundi 99ae0fc811 [fix] avoid false Windows privacy screen obstruction failures 2026-09-11 17:43:04 +08:00
dijunkun 9af7417c46 [feat] add Windows screen capture method setting 2026-09-11 16:41:16 +08:00
dijunkun ceb0a1654a [feat] add Windows privacy screen and prefer DXGI capture 2026-09-11 16:11:50 +08:00
dijunkun 4f200095f4 [fix] improve OpenGL video clarity when downscaling 2026-09-09 17:28:48 +08:00
dijunkun bd0a7aa2f6 [fix] prevent system audio capture hangs across desktop platforms 2026-09-09 11:34:47 +08:00
dijunkun b87ab025be [feat] use server-issued ICE configuration across clients 2026-09-08 17:07:16 +08:00
dijunkun 5b60695a52 [fix] restore Linux Dock icons in packaged builds 2026-09-08 15:03:32 +08:00
dijunkun 0002bcea74 [fix] hide macOS Dock icon when closing to the tray 2026-09-08 00:22:08 +08:00
dijunkun 58225de7ea [release] prepare v1.5.0 release notes and iOS version 2026-09-07 23:24:53 +08:00
dijunkun 5f37d0e753 [docs] update MiniRTC submodule documentation 2026-09-07 23:13:49 +08:00
dijunkun b518ea82e2 [docs] refresh README screenshots and platform guides 2026-09-07 22:53:30 +08:00
dijunkun abc8f907f3 [fix] restore iOS video after returning to the foreground 2026-09-07 21:31:08 +08:00
dijunkun d498d3f0ef [feat] show video resolution in the iOS floating status bar 2026-09-07 21:17:54 +08:00
dijunkun 55806d9e82 [fix] allow iOS relative mouse control outside the video area 2026-09-07 21:14:53 +08:00
dijunkun 2d3f1bb475 [fix] prevent iOS relative cursor jumps after movement 2026-09-07 21:11:27 +08:00
dijunkun 5d97d5a393 [fix] keep iOS floating controls clear of the Dynamic Island 2026-09-07 21:04:14 +08:00
dijunkun 19218a57f9 [fix] prevent stale device online status on iOS 2026-09-07 20:57:30 +08:00
dijunkun af729144fe [fix] prevent stale device online status 2026-09-07 20:02:19 +08:00
dijunkun d381cbaa9b [fix] show system tray icon on application startup 2026-09-07 17:42:25 +08:00
dijunkun 5aedb09c55 [feat] enable CUDA/OpenGL video rendering on Linux 2026-09-07 17:31:48 +08:00
dijunkun fe1178e26f [fix] move connection cleanup off the UI thread 2026-09-07 10:54:18 +08:00
dijunkun e0cd761bdc [fix] restore Windows lock-screen control and improve capture performance, fixes #98 2026-09-07 05:29:42 +08:00
dijunkun 2000654792 [fix] prevent Windows startup crashes from OpenGL failures, fixes #97 2026-09-07 04:20:54 +08:00
dijunkun 19800ef350 [fix] update MiniRTC iOS UPnP dependencies 2026-09-07 02:57:40 +08:00
dijunkun d10b65cc19 [fix] update MiniRTC relay to P2P upgrades 2026-09-07 02:30:26 +08:00
dijunkun 3a0179968c [fix] update MiniRTC ICE connectivity 2026-09-07 02:04:34 +08:00
dijunkun 55f389b834 [fix] update MiniRTC recovery tuning 2026-09-04 18:45:57 +08:00
dijunkun 8a58166c59 [fix] update MiniRtc video recovery timing 2026-09-04 17:35:14 +08:00
dijunkun aa2ee8ce7f [fix] update MiniRtc video recovery handling 2026-09-04 17:07:39 +08:00
dijunkun b150d46501 [fix] align MiniRtc media interfaces and timestamps 2026-09-04 15:45:26 +08:00
dijunkun 010064d5b7 [feat] optimize Windows native video encoding 2026-09-04 02:23:16 +08:00
dijunkun db3b26efe7 [feat] optimize macOS native video encoding 2026-09-04 02:06:44 +08:00
dijunkun cd1b53ee65 [chore] standardize source file headers 2026-09-04 02:06:36 +08:00
dijunkun b031a3953e [feat] optimize macOS native video rendering 2026-09-04 01:28:13 +08:00
dijunkun 3f537ae63c [feat] optimize Windows native video rendering 2026-09-03 18:01:09 +08:00
300 changed files with 22291 additions and 23056 deletions
+63 -15
View File
@@ -149,6 +149,11 @@ jobs:
xmake run --root crossdesk_wire_test
xmake run --root keyboard_state_wire_test
- name: Install Linux headless runtime dependencies
run: |
apt-get update
apt-get install -y --no-install-recommends xvfb xfce4 dbus
- name: Package
run: |
chmod +x ${{ matrix.package_script }}
@@ -271,7 +276,7 @@ jobs:
- name: Build CrossDesk
run: |
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=false -y
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=false -vyD
xmake b -vy crossdesk
- name: Package CrossDesk app
@@ -311,6 +316,8 @@ jobs:
name: Build on iOS (arm64)
runs-on: macos-26
timeout-minutes: 120
outputs:
version: ${{ steps.ios_version.outputs.version }}
env:
IOS_DEPLOYMENT_TARGET: "16.0"
EXPECTED_IOS_SDK: "26.5"
@@ -369,6 +376,26 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5
- name: Read independent iOS version
id: ios_version
shell: bash
run: |
IOS_VERSION_BASE="$(xcodebuild \
-project apps/ios/CrossDeskMobile.xcodeproj \
-scheme CrossDeskMobile \
-configuration Release \
-destination 'generic/platform=iOS' \
-showBuildSettings CODE_SIGNING_ALLOWED=NO | \
awk '$1 == "MARKETING_VERSION" && $2 == "=" { print $3 }')"
if [[ ! "${IOS_VERSION_BASE}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid iOS marketing version: ${IOS_VERSION_BASE}" >&2
exit 1
fi
VERSION_NUM="v${IOS_VERSION_BASE}-${VERSION_NUM##*-}"
echo "IOS_VERSION_BASE=${IOS_VERSION_BASE}" >> "${GITHUB_ENV}"
echo "VERSION_NUM=${VERSION_NUM}" >> "${GITHUB_ENV}"
echo "version=${VERSION_NUM}" >> "${GITHUB_OUTPUT}"
- name: Initialize submodules
run: git submodule update --init --recursive
@@ -439,6 +466,11 @@ jobs:
fi
xcrun lipo "${EXECUTABLE_PATH}" -verify_arch arm64
APP_VERSION=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "${INFO_PLIST}")
if [[ "${APP_VERSION}" != "${IOS_VERSION_BASE}" ]]; then
echo "Unexpected iOS app version: expected ${IOS_VERSION_BASE}, got ${APP_VERSION}" >&2
exit 1
fi
MINIMUM_OS=$(/usr/libexec/PlistBuddy -c 'Print :MinimumOSVersion' "${INFO_PLIST}")
if [[ "${MINIMUM_OS}" != "${IOS_DEPLOYMENT_TARGET}" ]]; then
echo "Unexpected minimum iOS version: expected ${IOS_DEPLOYMENT_TARGET}, got ${MINIMUM_OS}" >&2
@@ -655,7 +687,10 @@ jobs:
$portableFiles = @(
@("crossdesk.exe", "CrossDesk.exe"),
@("crossdesk_service.exe", "crossdesk_service.exe"),
@("crossdesk_session_helper.exe", "crossdesk_session_helper.exe")
@("crossdesk_session_helper.exe", "crossdesk_session_helper.exe"),
@("crossdesk_privacy_cursor_helper.exe", "crossdesk_privacy_cursor_helper.exe"),
@("crossdesk_privacy_window.dll", "crossdesk_privacy_window.dll"),
@("slint_cpp.dll", "slint_cpp.dll")
)
foreach ($file in $portableFiles) {
@@ -669,6 +704,13 @@ jobs:
Copy-Item (Join-Path $buildDir "*.dll") $portableDir -Force
# Bundled Amyuni usbmmidd_v2 virtual display driver (headless hosts)
$driverDir = Join-Path $buildDir "usbmmidd_v2"
if (!(Test-Path (Join-Path $driverDir "deviceinstaller64.exe"))) {
throw "Missing bundled usbmmidd_v2 driver package: $driverDir"
}
Copy-Item $driverDir (Join-Path $portableDir "usbmmidd_v2") -Recurse -Force
foreach ($file in $portableFiles) {
$packagedFile = Join-Path $portableDir $file[1]
if (!(Test-Path $packagedFile)) {
@@ -749,6 +791,16 @@ jobs:
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
- name: Prepare release notes
run: |
cat > "$RUNNER_TEMP/release-notes.md" << 'EOF'
Binary release only. Source code is not included.
## Code signing policy
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
EOF
- name: Rename artifacts
run: |
mkdir -p release
@@ -756,7 +808,7 @@ jobs:
cp artifacts/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
cp artifacts/crossdesk-ios-arm64-unsigned-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-ios-arm64-unsigned-${{ steps.version.outputs.VERSION_WITH_V }}.zip
cp artifacts/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}/* release/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}.zip
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
@@ -767,17 +819,13 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.VERSION_WITH_V }}
target_commitish: ${{ github.sha }}
name: Release ${{ steps.version.outputs.VERSION_WITH_V }}
draft: false
prerelease: false
files: release/*
generate_release_notes: false
body: |
Binary release only. Source code is not included.
## Code signing policy
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
body_path: ${{ runner.temp }}/release-notes.md
- name: Create or update 'latest' tag
run: |
@@ -795,12 +843,7 @@ jobs:
prerelease: false
files: release/*
generate_release_notes: false
body: |
Binary release only. Source code is not included.
## Code signing policy
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
body_path: ${{ runner.temp }}/release-notes.md
- name: Upload artifacts to server
uses: burnett01/rsync-deployments@5.2
@@ -851,6 +894,11 @@ jobs:
}
}
EOF
jq --arg release_name "Release ${{ steps.version.outputs.VERSION_WITH_V }}" \
--rawfile release_notes "$RUNNER_TEMP/release-notes.md" \
'.releaseName = $release_name | .releaseNotes = $release_notes' \
version.json > version-with-notes.json
mv version-with-notes.json version.json
cat version.json
- name: Upload version.json to server
+176 -374
View File
@@ -1,429 +1,232 @@
# CrossDesk
<div align="center">
<img src="apps/desktop/resources/linux/crossdesk_128x128.png" width="80" alt="CrossDesk 标志" />
<h1>CrossDesk</h1>
<p>轻量、跨平台的远程桌面,让电脑、浏览器和 iPhone / iPad 连接到同一张桌面。</p>
<p><a href="https://www.crossdesk.cn/">官方网站</a> · <a href="https://github.com/kunkundi/crossdesk/releases">下载客户端</a> · <a href="https://web.crossdesk.cn/">打开 Web 客户端</a> · <a href="README_EN.md">English</a></p>
</div>
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
<div align="center">
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-brightgreen.svg)](https://www.crossdesk.cn/)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/web-client)
[![Build Status](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml/badge.svg)](https://github.com/kunkundi/crossdesk/actions)
[![Docker Pulls](https://img.shields.io/docker/pulls/crossdesk/crossdesk-server)](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
[![GitHub issues](https://img.shields.io/github/issues/kunkundi/crossdesk.svg)](https://github.com/kunkundi/crossdesk/issues)
[![GitHub stars](https://img.shields.io/github/stars/kunkundi/crossdesk.svg?style=social)](https://github.com/kunkundi/crossdesk/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/kunkundi/crossdesk.svg?style=social)](https://github.com/kunkundi/crossdesk/forks)
[![Platforms](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20iOS-blue)](#download)
[![Release](https://img.shields.io/github/v/release/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/releases)
[![Build](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml/badge.svg)](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml)
[![License: GPL v3](https://img.shields.io/badge/license-GPL--3.0-blue)](LICENSE)
[![Stars](https://img.shields.io/github/stars/kunkundi/crossdesk?style=social)](https://github.com/kunkundi/crossdesk/stargazers)
[ [English](README_EN.md) / 中文 ]
</div>
---
CrossDesk 支持 Windows、macOS、Linux 之间的远程访问,也可从浏览器或原生 iOS 客户端控制电脑。项目基于 [MiniRTC](https://github.com/kunkundi/minirtc),提供实时音视频传输、键鼠控制、文件传输与自托管能力,仍在持续开发中。
> 本文按当前仓库代码整理。截图展示当前 Slint 桌面界面,使用演示 ID、密码、在线状态和连接记录;缩略图来自公开页面;下载包的功能与系统要求请同时核对对应 Release 说明。原生 iOS 客户端的构建、签名方式见下文。
[界面预览](#preview) · [下载与安装](#download) · [快速连接](#quick-start) · [会话操作](#session) · [设置](#settings) · [iOS](#ios) · [自托管](#self-hosting) · [源码构建](docs/BUILD.md) · [常见问题](docs/FAQ.md)
<a id="preview"></a>
## 界面预览
### PC 客户端
### 桌面客户端
![sup_example](https://github.com/user-attachments/assets/eeb64fbe-1f07-4626-be1c-b77396beb905)
主窗口集中展示本机 ID、连接密码、远程连接入口和近期连接记录。
<p align="center">
<img src="docs/images/desktop-main-zh.png" width="800" alt="当前桌面客户端示例:已连接服务器,三台在线设备均显示页面缩略图" />
</p>
### Web 客户端
在浏览器中输入远程设备 ID 和密码,无需在控制端安装桌面客户端。
<p align="center">
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
<img src="docs/images/web-client.png" width="800" alt="CrossDesk Web 客户端当前连接首页" />
</p>
---
图片来源与更新方式见 [截图说明](docs/images/README.md)。
## 简介
## 主要功能
CrossDesk 是一个轻量级的跨平台远程桌面软件,支持 Web 端控制远程设备。
CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频传输库的实验性应用。MiniRTC 是一个轻量级的跨平台实时音视频传输库。它具有网络透传([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)),视频软硬编解码(H264/AV1),音频编解码([Opus](https://github.com/xiph/opus)),信令交互,网络拥塞控制,传输加密([SRTP](https://tools.ietf.org/html/rfc3711))等基础能力。
---
## 系统要求
| 平台 | 最低版本 |
| 能力 | 当前支持 |
| --- | --- |
| **Windows** | Windows 10 及以上 (64 位) |
| **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 |
| **Linux** | Ubuntu 20.04 及以上 |
| 跨平台控制 | Windows / macOS / Linux 桌面互控;浏览器与原生 iOS 作为控制端 |
| 实时画面与声音 | H.264 / AV1、硬件编解码、远端声音播放;会话中可调整画质、30 / 60 fps 与画面偏好 |
| 多设备与多显示器 | 近期连接、设备别名、会话标签与远端显示器切换 |
| 输入与协作 | 键鼠输入、远端光标同步、组合键、文本剪贴板同步、文件传输 |
| 网络与部署 | P2P 直连、TURN 中继与强制中继、SRTP 媒体加密、自托管信令与中继服务 |
| 隐私屏 | 被连接时自动开启,或在会话工具栏切换;可用性取决于被控端系统与采集方式 |
| Windows 锁屏控制 | 通过 CrossDesk Service 支持锁屏、登录界面及安全桌面的输入转发 |
| Linux 无头运行 | 复用已有桌面和应用,或创建独立 Xvfb 虚拟桌面;终端控制台可查看状态、修改密码和设置,见 [运行说明](docs/HEADLESS.md) |
---
各客户端的入口和能力有所不同;桌面会话操作见下文,iOS 的具体功能见 [开发说明](apps/ios/README.md)。
## 使用
<a id="download"></a>
在菜单栏“对端ID”处输入远端桌面的ID,点击“→”即可发起远程连接。
## 下载与安装
![usage1](https://github.com/user-attachments/assets/3a4bb59f-c84c-44d2-9a20-11790aac510e)
前往 [GitHub Releases](https://github.com/kunkundi/crossdesk/releases) 或 [官方网站](https://www.crossdesk.cn/),按系统与 CPU 架构选择安装包。
如果远端桌面设置了连接密码,则本端需填写正确的连接密码才能成功发起远程连接。
| 平台 | 当前源码 / CI 基线 | 安装方式 |
| --- | --- | --- |
| Windows | Windows 10+,x64 | `.exe` 安装包;便携构建也受支持 |
| macOS | macOS 14.0+,Intel / Apple Silicon | 选择 x64 或 arm64 的 `.pkg` |
| Linux | Ubuntu 20.04+,amd64 / arm64,glibc 2.31 基线 | `.deb` 安装包 |
| iOS / iPadOS | iOS 16.0+,arm64 真机 | 原生客户端;使用 Xcode 构建并签名,CI 产物为未签名应用 |
| Web | 支持 WebRTC 的浏览器 | 访问 [web.crossdesk.cn](https://web.crossdesk.cn/) |
![password](https://github.com/user-attachments/assets/1beadcce-640d-4f5c-8e77-51917b5294d5)
Linux 下载后,在下载目录中将文件名替换为实际安装包名称:
发起连接前,可在设置中自定义配置项,如语言、视频编码格式等。
![settings](https://github.com/user-attachments/assets/8bc5468d-7bbb-4e30-95bd-da1f352ac08c)
```bash
sudo apt install "./crossdesk-linux-amd64-<version>.deb"
```
### 使用 Web 客户端
**macOS 首次运行:** 按应用提示,在“系统设置 → 隐私与安全性”中授予 CrossDesk **屏幕录制**(新系统可能显示为“屏幕与系统音频录制”)及 **辅助功能**权限,再按系统提示重新打开应用。前者用于捕获桌面,后者用于远程键鼠输入。
浏览器访问 [CrossDesk Web Client](https://web.crossdesk.cn/)。
输入 **远程设备 ID** 与 **密码**,点击连接即可接入远程设备。如图,**iOS Safari 远程控制 Win11**:
<a id="quick-start"></a>
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
## 快速连接
### Windows 服务(CrossDesk Service)
### 从另一台电脑连接
CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服务名为 `CrossDeskService`。该服务用于锁屏、登录界面和安全桌面等受保护场景下的远程控制增强能力,包括:
1. **准备被控电脑。** 安装并运行 CrossDesk,等待底部显示“已连接服务器”。在左侧“本桌面”查看 **本机 ID** 和 **密码**,提供给控制端。
2. **输入对端 ID。** 在控制端右侧“远程桌面 → 对端 ID”输入被控电脑的 ID,点击 **→**。
3. **完成密码验证。** 按弹窗输入被控电脑当前的连接密码,确认后等待远程画面出现。需要保存密码时勾选“记住密码”。
4. **再次连接。** 成功连接过的设备会出现在“近期连接”中,可通过卡片重连,也可编辑别名或删除记录。
- 上报远端当前是否处于锁屏、登录、凭据输入或安全桌面状态;
- 支持从控制端发送 `Ctrl+Alt+Del`(SAS);
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
本机密码旁的眼睛按钮用于显示 / 隐藏密码,刷新按钮用于生成新的随机密码,铅笔按钮用于手动修改。密码统一为 **6 位数字或英文字母**。修改或刷新时需保持连接服务器;等待修改成功、客户端重新连接后,再复制当前密码。已保存旧密码的控制端需重新输入。
Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
### 从浏览器连接
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe`、`crossdesk_service.exe` 和 `crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell:
1. 保持被控电脑上的 CrossDesk 运行,并确认已连接服务器。
2. 打开 [Web 客户端](https://web.crossdesk.cn/),输入 **远程设备 ID** 和 **密码**,点击“连接”。
3. 连接后使用页面提供的显示器、鼠标模式和键盘控件操作远端;手机和平板也可通过浏览器访问。
如果使用自建服务器,需要让控制端与被控端接入同一服务;Web 部署配置见 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
<a id="session"></a>
## 会话中的常用操作
远程窗口通过会话标签切换设备,控制栏提供以下操作;控制栏收起时先点击展开按钮。将鼠标悬停在图标上可查看提示。
| 入口 | 用途 |
| --- | --- |
| 显示器 | 切换远端显示器 |
| 键盘 | 发送组合键;Windows 的 `Ctrl+Alt+Del` 依赖远端服务 |
| 鼠标 | 控制 / 释放远端鼠标 |
| 扬声器 | 播放声音 / 静音 |
| 文件夹 | 选择文件发送到远端,并查看传输进度 |
| 画面设置 | 在当前会话中调整画面质量、采集帧率与画面偏好 |
| 网络状态 | 查看流量、丢包率、帧率、分辨率、直连 / 中继及加密状态 |
| 隐私屏 | 开启或关闭被控电脑的隐私屏;悬停查看当前状态与可用操作 |
| 全屏 | 进入 / 退出全屏 |
| 断开连接 | 结束当前远程会话 |
桌面端可同步文本剪贴板。接收文件的位置可在“设置 → 文件保存路径”调整。关闭主窗口会隐藏窗口并保持程序运行;需要完全退出时使用系统托盘或菜单栏中的退出入口。
### 在会话中调整画面
连接成功后,点击工具栏中的 **画面设置**(滑杆图标),直接选择低 / 中 / 高画质、**30 / 60 fps**,以及帧率优先 / 画质优先 / 平衡。选项会发送给当前被控端,无需断开重连;实际画面效果取决于网络、设备与编码器能力。旧版被控端不支持时,菜单会提示并禁用选项。
<p align="center">
<img src="docs/images/desktop-stream-video-settings-zh.png" width="800" alt="当前远程会话的画面设置菜单:画面质量、采集帧率与画面偏好;背景为公开页面演示图" />
</p>
<a id="settings"></a>
## 客户端设置与更新
点击主窗口右上角 **☰ → 设置**,修改后点击“确认”保存。编码与连接选项建议在建立连接前调整;会话进行中部分选项不可修改。设置列表可向下滚动,查看自托管、启动方式与文件保存路径。画质、帧率和画面偏好在远程会话工具栏中调整。
<p align="center">
<img src="docs/images/desktop-settings-zh.png" width="720" alt="当前客户端设置:编码格式、硬件编解码、中继、强制中继与自动隐私屏" />
</p>
| 设置 | 说明 |
| --- | --- |
| 语言 | 中文、English、Русский |
| 采集方式(Windows) | 自动 / DXGI / WGC / GDI;无显示器主机使用“自动”启用自动适配 |
| 视频编码格式 | H.264 / AV1;硬件编解码是否可用取决于平台、硬件与构建选项 |
| 启用中继服务 | 允许借助 TURN 中继建立连接;P2P 连接失败时先检查此项 |
| 强制中继连接 | 启用中继服务后可勾选,让连接通过中继建立;关闭中继服务会同时关闭此项 |
| 被连接时开启隐私屏 | 设置本机被远程连接时是否自动启用隐私屏;会话中可从控制端工具栏切换 |
| 自托管配置 | 填写服务器地址和信令服务端口,并勾选启用 |
| 开机自启 / 启用守护进程 | 调整启动与进程守护行为,按界面提示重启后生效 |
| 文件保存路径 | 选择接收文件的目录 |
### 检查更新
客户端启动后会在后台检查版本。发现新版本时,主窗口菜单入口会显示更新提示;打开 **☰ → 关于** 可查看版本,也可点击 **检查更新** 手动检查。发现更新后可查看更新说明并前往下载页面。
<p align="center">
<img src="docs/images/desktop-about-zh.png" width="720" alt="关于窗口与检查更新按钮,版本号和检查结果为演示数据" />
</p>
<a id="ios"></a>
## 原生 iOS / iPadOS 客户端
仓库的 [`apps/ios`](apps/ios/README.md) 提供原生控制端,与桌面端使用同一套 MiniRTC 协议。它可以控制远端电脑;当前不提供将 iPhone / iPad 作为被控桌面的功能。
1. 按 [iOS 开发说明](apps/ios/README.md) 使用 Xcode 构建并签名,安装到 iOS 16+ 的真机。
2. 在首页输入对端 ID,点击连接并填写密码;成功连接后可从“最近连接”中重连。
3. 在设置中选择鼠标模式:**相对位置**像触控板一样移动光标,**绝对位置**将触摸位置直接映射到远端画面。
4. 点击悬浮图标展开菜单,可切换显示器、显示键盘、控制声音或发送文件。收到的文件位于应用的 `Documents/Received`,可通过传输状态旁的分享按钮导出。
当前支持接收远端文本到 iOS 剪贴板;界面暂未提供主动发送本机剪贴板的按钮。
当前支持单指点击、双指右键、长按拖动与捏合缩放;缩放后的双指平移用于移动视图。原生端与浏览器端分别维护,使用入口和手势可能不同。
<a id="windows-service"></a>
## Windows 锁屏与登录界面
**CrossDesk Service**(服务名 `CrossDeskService`)提供锁屏、登录、凭据输入及安全桌面的状态上报、`Ctrl+Alt+Del` 发送和键鼠输入转发。
安装版会注册按需启动的服务,客户端启动时尝试启动它;本机没有 CrossDesk 客户端进程后服务会自动退出。便携版可在提示窗口或“设置 → 锁屏控制服务”中安装服务,需要管理员权限。该服务依赖客户端运行。
<details>
<summary>手动部署与服务管理命令</summary>
保留完整安装目录;自行部署构建产物时,将 `CrossDesk.exe`、`crossdesk_service.exe`、`crossdesk_session_helper.exe` 及同次构建输出的全部 `.dll` 放在同一目录。在该目录打开管理员 PowerShell,按需执行:
```powershell
# 安装
.\CrossDesk.exe --service-install
# 启动
.\CrossDesk.exe --service-start
# 查看状态
.\CrossDesk.exe --service-status
# 停止
.\CrossDesk.exe --service-ping
# 停止或卸载服务
.\CrossDesk.exe --service-stop
# 卸载
.\CrossDesk.exe --service-uninstall
```
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
</details>
---
出现“远端 Windows 服务不可用”时,检查被控电脑的安装和服务状态;普通桌面连接仍可使用,受保护界面的控制能力会受限。
## 如何编译
### 构建依赖
- [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/) 3.21 及以上(系统版本过低时由 xmake 自动安装)
### Linux
Linux 构建支持 Ubuntu 20.04 及以上版本的 amd64 和 arm64。发布安装包以
Ubuntu 20.04/glibc 2.31 为兼容基线。先安装基础编译依赖:
```bash
sudo apt-get update
sudo apt-get install -y \
git curl unzip build-essential gcc-10 g++-10 python3-pip \
pkg-config binutils dpkg-dev \
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
libasound2-dev libsndio-dev libpulse-dev \
libgl1-mesa-dev
```
Ubuntu 20.04 自带的 CMake 3.16 不满足要求时,xmake 会自动下载并使用
CMake 3.21 或更高版本,无需手动替换系统 CMake。
Ubuntu 20.04 默认的 GCC 9 缺少 Slint C++ API 所需的部分 C++20 标准库,
因此上述命令同时安装并在后续配置中指定 GCC 10。
需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖:
```bash
sudo apt-get install -y libdbus-1-dev libdrm-dev
# Ubuntu 22.04 及以上也可以直接安装系统开发包
# sudo apt-get install -y libpipewire-0.3-dev libspa-0.2-dev
```
下载子模块并编译 Release 版本:
```bash
git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
cd crossdesk
# 已经克隆过仓库时执行这一行
git submodule update --init --recursive
# Ubuntu 20.04 没有 PipeWire 0.3 开发包,安装仅用于编译的头文件 SDK。
# 已安装发行版 PipeWire 0.3 开发包时可跳过此步骤。
sudo ./docker/linux-build/install-pipewire-sdk.sh
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
xmake b -vy crossdesk
```
这里应使用 `--toolchain=gcc-10`,以确保 CrossDesk 及 xmake 在配置阶段构建的
libyuv 等第三方包都使用 GCC 10。只指定 `--cc`/`--cxx` 时,依赖安装可能仍回退到
系统默认的 `/bin/cc` 和 `/bin/c++`。`-c` 会清除旧配置,切换编译器时必须保留。
amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`,arm64 构建结果位于 `build/linux/arm64/release/crossdesk`。
启用 Wayland 和 DRM 的配置示例:
```bash
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
--toolchain=gcc-10 -y
xmake b -vy crossdesk
```
生成 Debian 安装包(必须在对应架构上先完成 Release 编译):
```bash
# amd64
./apps/desktop/scripts/linux/pkg_amd64.sh 1.0.0
# arm64
./apps/desktop/scripts/linux/pkg_arm64.sh 1.0.0
```
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`。
PipeWire 不属于强制运行时依赖:程序在运行时检测宿主系统的 PipeWire 0.3,
没有该运行库时仍可使用 X11(以及构建时启用的 DRM)捕获。
### 通用编译选项
```text
--USE_CUDA=true/false: 启用 CUDA 硬件编解码,默认不启用
--USE_WAYLAND=true/false: 在 Linux 上启用 Wayland/PipeWire 捕获,默认不启用
--USE_DRM=true/false: 在 Linux 上启用 DRM 捕获,默认不启用
--CROSSDESK_PORTABLE=true/false: 构建便携版本,默认不启用
--CROSSDESK_VERSION=xxx: 指定 CrossDesk 的版本
# 示例
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
运行:
```bash
xmake r crossdesk
```
### 无 CUDA 环境下的开发支持
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 20.04 兼容构建镜像](https://hub.docker.com/r/crossdesk/ubuntu20.04)。该镜像内置必要的构建依赖,可生成兼容 glibc 2.31 的单一 Linux 安装包。
进入容器,下载工程后执行:
```bash
export CUDA_PATH=/usr/local/cuda
export XMAKE_GLOBALDIR=/data
xmake f --USE_CUDA=true
xmake b --root -vy crossdesk
```
对于**未安装 CUDA 环境的 Windows 开发者**,执行下面的命令安装 CUDA 编译环境:
```powershell
xmake require -vy "cuda 12.6.3"
```
安装完成后执行:
```powershell
xmake require --info "cuda 12.6.3"
```
输出如下:
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
根据上述输出获取到 CUDA 的安装目录,即 installdir 指向的位置。将 CUDA_PATH 加入系统环境变量,或在终端中输入:
```powershell
set CUDA_PATH=path_to_cuda_installdir
```
重新执行:
```powershell
xmake f --USE_CUDA=true
xmake b -vy crossdesk
```
#### 注意
运行时如果客户端状态栏显示 **未连接服务器**,请先在 [CrossDesk 官方网站](https://www.crossdesk.cn/) 安装客户端,以便在环境中安装所需的证书文件。
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
### 关于 Xmake
#### 安装 Xmake
使用 curl:
```bash
curl -fsSL https://xmake.io/shget.text | bash
```
使用 wget:
```bash
wget https://xmake.io/shget.text -O - | bash
```
使用 powershell:
```powershell
irm https://xmake.io/psget.text | iex
```
#### 编译选项
```text
# 切换编译模式
xmake f -m debug/release
# 可选编译参数
-r :重新构建目标
-v :显示详细的构建日志
-y :自动确认提示
# 示例
xmake b -vy crossdesk
```
#### 运行选项
```bash
# 使用调试模式运行
xmake r -d crossdesk
```
更多使用方法可参考 [Xmake 官方文档](https://xmake.io/guide/quick-start.html)。
---
<a id="self-hosting"></a>
## 自托管服务器
### 服务端
可自行部署信令服务与 TURN 中继,并在桌面客户端 **☰ → 设置 → 自托管配置** 中填写连接信息。控制端与被控端都需要启用同一套配置。
推荐使用 Docker 部署 CrossDesk Server。
- [自托管指南](docs/SELF_HOSTING.md):当前 Compose 部署入口、客户端设置、证书信任与排查。
- [CrossDesk Server](https://github.com/kunkundi/crossdesk-server):服务端源码与发布配置。
- [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client):浏览器端源码与自托管说明。
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=xxx.xxx.xxx.xxx \
-e INTERNAL_IP=xxx.xxx.xxx.xxx \
-e CROSSDESK_SERVER_PORT=xxxx \
-e COTURN_PORT=xxxx \
-e MIN_PORT=xxxxx \
-e MAX_PORT=xxxxx \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
## 开发与反馈
上述命令中,用户需注意的参数如下:
- [源码构建](docs/BUILD.md):Windows、macOS、Linux、可选编译参数与打包。
- [iOS 开发说明](apps/ios/README.md):原生端构建和真机验证。
- [GUI 架构](docs/gui-architecture.md):桌面 UI、平台实现与工程结构。
- [常见问题](docs/FAQ.md):连接失败、黑屏、文件位置与编译问题。
- [提交问题](https://github.com/kunkundi/crossdesk/issues):请附上两端系统、客户端版本、连接方式和可复现步骤。
#### 配置参数
## 致谢与许可
- EXTERNAL_IP:服务器公网 IP , 对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器地址**
- INTERNAL_IP:服务器内网 IP
- CROSSDESK_SERVER_PORT:自托管服务使用的端口,对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器端口**
- COTURN_PORT: COTURN 服务使用的端口, 对应 CrossDesk 客户端**自托管服务器配置**中填写的**中继服务端口**
- MIN_PORT/MAX_PORT:COTURN 服务使用的端口范围,例如:MIN_PORT=50000, MAX_PORT=60000,范围可根据客户端数量调整。
- `-v /var/lib/crossdesk:/var/lib/crossdesk`:持久化数据库和证书文件到宿主机
- `-v /var/log/crossdesk:/var/log/crossdesk`:持久化日志文件到宿主机
感谢 [HelloGitHub](https://hellogithub.com/)、[阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 与 [LinuxDo](https://linux.do) 社区的推荐、交流与支持。
#### 部署示例
CrossDesk 使用 [GPL-3.0](LICENSE) 许可。隐私说明见 [隐私政策](PRIVACY.md#中文)。
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=114.114.114.114 \
-e INTERNAL_IP=10.0.0.1 \
-e CROSSDESK_SERVER_PORT=9099 \
-e COTURN_PORT=3478 \
-e MIN_PORT=50000 \
-e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
#### 注意事项
- **服务器需开放端口:COTURN_PORT/udp,COTURN_PORT/tcp,MIN_PORT-MAX_PORT/udp,CROSSDESK_SERVER_PORT/tcp。**
- 如果不挂载 volume,容器删除后数据会丢失
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
#### 目录权限
如果 Docker 自动创建的目录权限不足(属于 root),容器内用户无法写入,会导致:
- 证书生成失败,容器启动脚本会报错退出
- 数据库目录创建失败,程序会抛出异常并崩溃
- 日志目录创建失败,日志文件无法写入(但程序可能继续运行)
在启动容器前手动设置权限:
```bash
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
```
### 客户端
1. 点击右上角设置进入设置页面。<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
2. 点击`自托管服务器配置`按钮。<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**),点击确认按钮。
4. 勾选`自托管服务器配置`选项,点击确认按钮保存设置。如果服务端使用的是正式证书,则到此步骤为止,客户端即可显示已连接服务器。
5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
**Windows:** 使用管理员权限打开 PowerShell 执行:
```powershell
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
**Linux:**
```bash
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
**macOS:**
```bash
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
### 自托管 Web 客户端
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
---
## 常见问题
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md)。
---
## 致谢
- 感谢 [HelloGitHub](https://hellogithub.com/) 的推荐与关注。
- 感谢 [阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 的收录与推荐。
- 感谢 [LinuxDo](https://linux.do) 社区的关注、交流与支持,为 CrossDesk 项目的完善提供了帮助。
---
## Code signing policy
### Code signing policy
CrossDesk 使用 SignPath.io 为本仓库构建的官方 Windows 发布包提供代码签名。
@@ -431,4 +234,3 @@ CrossDesk 使用 SignPath.io 为本仓库构建的官方 Windows 发布包提供
- **Committers and reviewers(代码提交与审核):** [kunkundi](https://github.com/kunkundi)
- **Approvers(签名审批):** [kunkundi](https://github.com/kunkundi)
- **隐私政策:** [查看 CrossDesk 隐私政策](PRIVACY.md#中文)
+175 -390
View File
@@ -1,451 +1,236 @@
# CrossDesk
<div align="center">
<img src="apps/desktop/resources/linux/crossdesk_128x128.png" width="80" alt="CrossDesk logo" />
<h1>CrossDesk</h1>
<p>A lightweight, cross-platform remote desktop for computers, browsers, and iPhone / iPad.</p>
<p><a href="https://www.crossdesk.cn/">Website</a> · <a href="https://github.com/kunkundi/crossdesk/releases">Download</a> · <a href="https://web.crossdesk.cn/">Open Web Client</a> · <a href="README.md">中文</a></p>
</div>
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
<div align="center">
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-brightgreen.svg)](https://www.crossdesk.cn/)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/web-client)
[![Build Status](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml/badge.svg)](https://github.com/kunkundi/crossdesk/actions)
[![Docker Pulls](https://img.shields.io/docker/pulls/crossdesk/crossdesk-server)](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
[![GitHub issues](https://img.shields.io/github/issues/kunkundi/crossdesk.svg)](https://github.com/kunkundi/crossdesk/issues)
[![GitHub stars](https://img.shields.io/github/stars/kunkundi/crossdesk.svg?style=social)](https://github.com/kunkundi/crossdesk/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/kunkundi/crossdesk.svg?style=social)](https://github.com/kunkundi/crossdesk/forks)
[![Platforms](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20iOS-blue)](#download)
[![Release](https://img.shields.io/github/v/release/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/releases)
[![Build](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml/badge.svg)](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml)
[![License: GPL v3](https://img.shields.io/badge/license-GPL--3.0-blue)](LICENSE)
[![Stars](https://img.shields.io/github/stars/kunkundi/crossdesk?style=social)](https://github.com/kunkundi/crossdesk/stargazers)
[ [中文](README.md) / English ]
</div>
---
CrossDesk connects Windows, macOS, and Linux desktops, with browser and native iOS clients for controlling computers. Built on [MiniRTC](https://github.com/kunkundi/minirtc), it supports real-time video and audio, keyboard and mouse control, file transfer, and self-hosting. The project is under active development.
> This guide follows the current repository source. Desktop screenshots render the current Slint UI with sample IDs, passwords, online statuses, and recent devices; thumbnails show public pages. Check each release's notes for the features and requirements of its binaries. Native iOS build and signing instructions are below.
[Preview](#preview) · [Download](#download) · [Quick start](#quick-start) · [Session controls](#session) · [Settings](#settings) · [iOS](#ios) · [Self-hosting](#self-hosting) · [Build](docs/BUILD_EN.md) · [FAQ](docs/FAQ.md#english)
<a id="preview"></a>
## Preview
### PC Client
### Desktop client
![sup_example](https://github.com/user-attachments/assets/3f17d8f3-7c4a-4b63-bae4-903363628687)
### Web Client
The main window brings together your device ID, connection password, remote connection entry, and recent devices.
<p align="center">
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
<img src="docs/images/desktop-main-en.png" width="800" alt="Current desktop UI example showing a connected server and three online devices with page thumbnails" />
</p>
---
### Web client
## Introduction
Enter a remote device ID and password in your browser without installing the desktop app on the controlling device.
CrossDesk is a lightweight cross-platform remote desktop software.
<p align="center">
<img src="docs/images/web-client.png" width="800" alt="Current CrossDesk Web Client connection page in Chinese" />
</p>
CrossDesk is an experimental application of [MiniRTC](https://github.com/kunkundi/minirtc.git), a lightweight cross-platform real-time audio and video transmission library. MiniRTC provides fundamental capabilities including network traversal ([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)), video software/hardware encoding and decoding (H264/AV1), audio encoding/decoding ([Opus](https://github.com/xiph/opus)), signaling interaction, network congestion control, and transmission encryption ([SRTP](https://tools.ietf.org/html/rfc3711)).
See the [screenshot notes](docs/images/README.md) for image sources and refresh instructions.
---
## Features
## System Requirements
| Platform | Minimum Version |
| Capability | Current support |
| --- | --- |
| **Windows** | Windows 10 or later (64-bit) |
| **macOS** | macOS Intel 15.0 or later *(versions between 14.0 and 15.0 can be built manually for compatibility)*<br>macOS Apple Silicon 14.0 or later |
| **Linux** | Ubuntu 20.04 or later |
| Cross-platform control | Windows / macOS / Linux desktop control; browser and native iOS controllers |
| Live video and audio | H.264 / AV1, hardware codecs, and remote audio; adjust quality, 30 / 60 fps, and video preference during a session |
| Devices and displays | Recent connections, device aliases, session tabs, and remote display switching |
| Input and sharing | Keyboard/mouse input, remote cursor synchronization, shortcuts, text clipboard synchronization, and file transfer |
| Networking and deployment | Direct P2P connections, TURN relay and forced relay, SRTP media encryption, and self-hosted signaling/relay services |
| Privacy screen | Enable automatically when connected to, or toggle from the session toolbar; availability depends on the host OS and capture method |
| Windows protected desktops | CrossDesk Service forwards input on lock screens, sign-in screens, and secure desktops |
| Linux headless operation | Reuse existing desktops and applications or create separate Xvfb desktops; use the terminal console to check status and change passwords or settings; see [setup](docs/HEADLESS_EN.md) |
---
Controls and capabilities vary by client. Desktop controls are described below; see the [iOS guide](apps/ios/README.md) for native mobile features.
## Usage
<a id="download"></a>
Enter the remote desktop ID in the menu bar’s “Remote ID” field and click “→” to initiate a remote connection.
## Download and install
![usage1](https://github.com/user-attachments/assets/3a4bb59f-c84c-44d2-9a20-11790aac510e)
Choose a package matching your OS and CPU architecture from [GitHub Releases](https://github.com/kunkundi/crossdesk/releases) or the [website](https://www.crossdesk.cn/).
If the remote desktop requires a connection password, you must enter the correct password on your side to successfully establish the connection.
| Platform | Current source / CI baseline | Installation |
| --- | --- | --- |
| Windows | Windows 10+, x64 | `.exe` installer; portable builds are also supported |
| macOS | macOS 14.0+, Intel / Apple Silicon | x64 or arm64 `.pkg` |
| Linux | Ubuntu 20.04+, amd64 / arm64, glibc 2.31 baseline | `.deb` package |
| iOS / iPadOS | iOS 16.0+, arm64 physical device | Native controller; build and sign with Xcode, or sign the unsigned CI app |
| Web | A WebRTC-capable browser | Open [web.crossdesk.cn](https://web.crossdesk.cn/) |
![password](https://github.com/user-attachments/assets/1beadcce-640d-4f5c-8e77-51917b5294d5)
On Linux, replace the example filename with the downloaded package name:
Before connecting, you can customize configuration options in the settings, such as language and video encoding format.
```bash
sudo apt install "./crossdesk-linux-amd64-<version>.deb"
```
![settings](https://github.com/user-attachments/assets/8bc5468d-7bbb-4e30-95bd-da1f352ac08c)
**First launch on macOS:** follow the app prompts to grant **Screen Recording** (called **Screen & System Audio Recording** on newer systems) and **Accessibility** under **System Settings → Privacy & Security**. Reopen the app if prompted. These permissions allow desktop capture and remote keyboard/mouse input respectively.
### Using the Web Client
<a id="quick-start"></a>
Visit [CrossDesk Web Client](https://web.crossdesk.cn/).
Enter the **Remote Device ID** and **Password**, then click Connect to access the remote device. As shown, **iOS Safari remotely controlling Windows 11**:
## Quick start
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
### Connect from another computer
### Windows Service (CrossDesk Service)
1. **Prepare the host.** Install and run CrossDesk on the computer to control. Wait for the bottom status bar to show that the server is connected. Share the **Local ID** and **Password** shown under **Local Desktop** with the controller.
2. **Enter the remote ID.** On the controlling computer, enter the host's ID in **Remote Desktop → Remote ID**, then click **→**.
3. **Verify the password.** Enter the host's current connection password when prompted and confirm. Select **Remember password** if you want to save it.
4. **Reconnect later.** Previously connected devices appear under **Recent Connections**. Reconnect from a card, edit its alias, or remove its record.
CrossDesk provides a local helper service on Windows named **CrossDesk Service**. Its service name is `CrossDeskService`. The service improves remote control in protected Windows states such as the lock screen, sign-in UI, credential UI, and secure desktop. It provides:
The eye button shows or hides the local password, the refresh button generates a new random password, and the pencil button lets you edit it. Passwords use **6 ASCII letters or digits**. Stay connected to the server when editing or refreshing a password. Wait for the change to succeed and the client to reconnect, then copy the current password. Controllers that saved the old password must enter it again.
- Remote status reporting for lock screen, sign-in, credential, and secure desktop states.
- Remote `Ctrl+Alt+Del` (SAS) delivery.
- Keyboard and mouse input forwarding while the remote Windows device is on the lock screen, sign-in UI, or secure desktop.
### Connect from a browser
The Windows installer bundles `crossdesk_service.exe` and `crossdesk_session_helper.exe`, then registers the service as an on-demand Windows service during installation. When the CrossDesk client starts, it tries to start the installed service automatically. When no CrossDesk client process is running on the machine, the service exits automatically. Uninstalling the client also stops and removes the service.
1. Keep CrossDesk running and connected to the server on the host computer.
2. Open the [Web Client](https://web.crossdesk.cn/), enter the **Remote Device ID** and **Password**, and click **Connect**.
3. Use the page's display, mouse-mode, and keyboard controls during the session. Phone and tablet browsers can also connect.
For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_service.exe`, and `crossdesk_session_helper.exe` are placed in the same directory. Open PowerShell with administrator privileges to install or uninstall the service:
For self-hosting, connect both sides to the same service. Browser deployment is documented in [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client).
<a id="session"></a>
## Session controls
Switch devices using session tabs. Expand the control bar if it is collapsed, and hover over icons to see their tooltips.
| Control | Action |
| --- | --- |
| Display | Switch the remote monitor |
| Keyboard | Send shortcuts; Windows `Ctrl+Alt+Del` requires the remote service |
| Mouse | Enable / release remote mouse control |
| Speaker | Play / mute remote audio |
| Folder | Select a file to send and view transfer progress |
| Video settings | Adjust video quality, capture frame rate, and video preference during the current session |
| Network statistics | Inspect traffic, packet loss, frame rate, resolution, direct / relay mode, and encryption status |
| Privacy screen | Enable or disable the host's privacy screen; hover to see its status and available actions |
| Fullscreen | Enter / exit fullscreen |
| Disconnect | End the current remote session |
Desktop clients synchronize text clipboard contents. Configure the receiving directory under **Settings → File Save Path**. Closing the main window hides it and keeps the app running; use the system tray or menu-bar exit command to quit completely.
### Adjust video during a session
After connecting, open **Video settings** (the sliders icon) in the toolbar. Choose low / medium / high quality, **30 / 60 fps**, and frame-rate priority / quality priority / balanced adaptation. Changes are sent to the current host without reconnecting; actual results depend on the network, device, and encoder. For older hosts that do not support live video settings, the menu explains the limitation and disables the options.
<p align="center">
<img src="docs/images/desktop-stream-video-settings-en.png" width="800" alt="Current session video menu with quality, capture frame rate, and video preference; the background is a public page used for demonstration" />
</p>
<a id="settings"></a>
## Client settings and updates
Open **☰ → Settings** in the top-right corner and click **OK** to save. Configure codecs and connections before connecting; some settings are disabled during an active session. Scroll down for self-hosting, startup behavior, and the file save path. Adjust quality, frame rate, and video preference from the remote session toolbar.
<p align="center">
<img src="docs/images/desktop-settings-en.png" width="720" alt="Current client settings with codec, hardware codecs, TURN, forced relay, and automatic privacy screen options" />
</p>
| Setting | Purpose |
| --- | --- |
| Language | 中文, English, Русский |
| Capture Method (Windows) | Auto / DXGI / WGC / GDI; choose Auto for automatic headless adaptation |
| Video Encode Format | H.264 / AV1; hardware codec availability depends on the platform, device, and build options |
| Enable TURN Service | Allow relay-assisted connections; check this setting if P2P fails |
| Force Relay | Require relay connections when TURN is enabled; turning TURN off also disables this option |
| Privacy screen on connect | Automatically enable privacy when this computer is controlled remotely; the controller can toggle it from the session toolbar |
| Self-Hosted Config | Set the server address and signaling port, then enable the checkbox |
| Auto Start / Enable Daemon | Configure startup and process supervision; restart as indicated by the UI |
| File Save Path | Choose where received files are saved |
### Check for updates
The client checks for updates in the background after launch. A new version adds an update indicator to the main window menu. Open **☰ → About** to view the version or click **Check for updates** to check manually. When an update is available, you can view its release notes and open the download page.
<p align="center">
<img src="docs/images/desktop-about-en.png" width="720" alt="About dialog with the Check for updates button; version and check result are demonstration data" />
</p>
<a id="ios"></a>
## Native iOS / iPadOS client
[`apps/ios`](apps/ios/README.md) contains a native controller using the same MiniRTC protocol as the desktop client. It controls remote computers; it does not expose an iPhone or iPad as a remotely controlled desktop.
1. Follow the [iOS guide](apps/ios/README.md) to build and sign the app with Xcode, then install it on an iOS 16+ physical device.
2. Enter a remote ID on the home screen, connect, and enter its password. Successful connections become available in the recent devices list.
3. Choose a mouse mode in Settings: **Relative Position** works like a trackpad; **Absolute Position** maps touches directly to the remote screen.
4. Tap the floating icon to expand the menu, then switch displays, open the keyboard, toggle audio, or send files. Received files are stored in `Documents/Received` and can be exported using the share button beside the transfer status.
The app currently receives remote text into the iOS clipboard. Its UI does not yet expose a button to send the local clipboard.
Gestures include one-finger click, two-finger right-click, long-press drag, and pinch-to-zoom. When zoomed in, two-finger panning moves the viewport. The native and browser clients are maintained separately, so their controls and gestures may differ. The current native UI uses Chinese labels.
<a id="windows-service"></a>
## Windows lock screens and sign-in
**CrossDesk Service** (`CrossDeskService`) reports protected desktop states and forwards `Ctrl+Alt+Del`, keyboard, and mouse input on lock screens, sign-in screens, credential prompts, and secure desktops.
The installer registers an on-demand service. CrossDesk tries to start it on launch, and the service exits when no local CrossDesk client is running. Portable builds offer service installation in the prompt or **Settings → Lock Screen Service**, with administrator privileges. The service depends on a running client.
<details>
<summary>Manual deployment and service commands</summary>
Keep the complete installation directory. For manual deployment, place `CrossDesk.exe`, `crossdesk_service.exe`, `crossdesk_session_helper.exe`, and all `.dll` files from the same build together. Open an administrator PowerShell in that directory and run the commands you need:
```powershell
# install
.\CrossDesk.exe --service-install
# start
.\CrossDesk.exe --service-start
# check status
.\CrossDesk.exe --service-status
.\CrossDesk.exe --service-ping
# stop
# Stop or uninstall the service
.\CrossDesk.exe --service-stop
# uninstall
.\CrossDesk.exe --service-uninstall
```
If the remote Windows service is not installed, not running, or temporarily unavailable, the basic remote desktop connection still works, but remote control on the lock screen, sign-in UI, and secure desktop is limited. The client will show “Remote Windows service unavailable”.
</details>
---
If **Remote Windows service unavailable** appears, check installation and service status on the host. Ordinary desktop connections remain available, while control of protected screens is limited.
## How to build
<a id="self-hosting"></a>
### Build requirements
## Self-hosting
- [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/) 3.21 or later (installed automatically by xmake when the system version is too old)
Host your own signaling and TURN services, then enter their connection details under **☰ → Settings → Self-Hosted Config**. Enable the same server configuration on both the controller and host.
### Linux
- [Self-hosting guide](docs/SELF_HOSTING_EN.md): current Compose deployment, client settings, certificate trust, and troubleshooting.
- [CrossDesk Server](https://github.com/kunkundi/crossdesk-server): server source and release configuration.
- [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client): browser source and deployment instructions.
Linux builds support Ubuntu 20.04 or later on amd64 and arm64. Release packages
use Ubuntu 20.04/glibc 2.31 as their compatibility baseline. Install the base
build dependencies first:
## Development and feedback
```bash
sudo apt-get update
sudo apt-get install -y \
git curl unzip build-essential gcc-10 g++-10 python3-pip \
pkg-config binutils dpkg-dev \
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
libasound2-dev libsndio-dev libpulse-dev \
libgl1-mesa-dev
```
- [Build from source](docs/BUILD_EN.md): Windows, macOS, Linux, optional flags, and packaging.
- [iOS development](apps/ios/README.md): native build and physical-device checks.
- [GUI architecture](docs/gui-architecture.md): desktop UI and platform organization.
- [FAQ](docs/FAQ.md#english): connection failures, blank video, received files, and build issues.
- [Report an issue](https://github.com/kunkundi/crossdesk/issues): include both operating systems, client versions, connection mode, and reproduction steps.
When Ubuntu 20.04's bundled CMake 3.16 is too old, xmake automatically
downloads and uses CMake 3.21 or later; replacing the system CMake manually is
not required.
## Acknowledgements and license
Ubuntu 20.04's default GCC 9 lacks parts of the C++20 standard library needed
by Slint's C++ API, so the commands above install GCC 10 and the configuration
below selects it explicitly.
Thanks to [HelloGitHub](https://hellogithub.com/), [Ruanyf Weekly](https://github.com/ruanyf/weekly), and the [LinuxDo](https://linux.do) community for featuring CrossDesk and contributing feedback.
Install the optional dependencies when Wayland capture or DRM capture is enabled:
CrossDesk is licensed under [GPL-3.0](LICENSE). See the [Privacy Policy](PRIVACY.md#english).
```bash
sudo apt-get install -y libdbus-1-dev libdrm-dev
### Code signing policy
# Ubuntu 22.04 and later can alternatively use the distribution packages.
# sudo apt-get install -y libpipewire-0.3-dev libspa-0.2-dev
```
Clone the submodules and build a release binary:
```bash
git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
cd crossdesk
# Run this when the repository has already been cloned
git submodule update --init --recursive
# Ubuntu 20.04 has no PipeWire 0.3 development package. Install the
# header-only SDK used during compilation. Skip this when the distribution's
# PipeWire 0.3 development packages are already installed.
sudo ./docker/linux-build/install-pipewire-sdk.sh
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
xmake b -vy crossdesk
```
Use `--toolchain=gcc-10` so that both CrossDesk and third-party packages such
as libyuv, which xmake builds during configuration, use GCC 10. Specifying only
`--cc`/`--cxx` may still let dependency builds fall back to the system
`/bin/cc` and `/bin/c++`. Keep `-c` when switching compilers to clear the old
configuration.
The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64 binary is written to `build/linux/arm64/release/crossdesk`.
Example configuration with Wayland and DRM capture enabled:
```bash
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
--toolchain=gcc-10 -y
xmake b -vy crossdesk
```
Build a Debian package after compiling a release binary on the matching architecture:
```bash
# amd64
./apps/desktop/scripts/linux/pkg_amd64.sh 1.0.0
# arm64
./apps/desktop/scripts/linux/pkg_arm64.sh 1.0.0
```
The package scripts install the shared Slint runtime in the private `/usr/lib/crossdesk` directory, so users do not need to install `libslint_cpp.so` separately.
PipeWire is not a mandatory runtime dependency. CrossDesk detects the host's
PipeWire 0.3 runtime dynamically and can still use X11 capture (and DRM when
enabled at build time) when it is unavailable.
### Common build options
```text
--USE_CUDA=true/false: Enable CUDA hardware codec acceleration, disabled by default
--USE_WAYLAND=true/false: Enable Wayland/PipeWire capture on Linux, disabled by default
--USE_DRM=true/false: Enable DRM capture on Linux, disabled by default
--CROSSDESK_PORTABLE=true/false: Build the portable variant, disabled by default
--CROSSDESK_VERSION=xxx: Set the CrossDesk version
# Example
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
Run:
```bash
xmake r crossdesk
```
### Development Without CUDA Environment
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 20.04 compatibility build image](https://hub.docker.com/r/crossdesk/ubuntu20.04) is provided.
This image contains the required build dependencies and produces a single Linux package compatible with the glibc 2.31 baseline.
After entering the container, download the project and run:
```bash
export CUDA_PATH=/usr/local/cuda
export XMAKE_GLOBALDIR=/data
xmake f --USE_CUDA=true
xmake b --root -vy crossdesk
```
For **Windows developers without a CUDA environment** installed, run the following command to install the CUDA build environment:
```powershell
xmake require -vy "cuda 12.6.3"
```
After the installation is complete, execute:
```powershell
xmake require --info "cuda 12.6.3"
```
The output will look like this:
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
From the output above, locate the CUDA installation directory — this is the path pointed to by installdir.
Add this path to your system environment variable CUDA_PATH, or set it in the terminal using:
```powershell
set CUDA_PATH=path_to_cuda_installdir
```
Then re-run:
```powershell
xmake f --USE_CUDA=true
xmake b -vy crossdesk
```
#### Notice
If the client status bar shows **Disconnected** during runtime, please first install the client from the [CrossDesk official website](https://www.crossdesk.cn/) to ensure the required certificate files are available in the environment.
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
### About Xmake
#### Installing Xmake
You can install Xmake using one of the following methods:
Using curl:
```bash
curl -fsSL https://xmake.io/shget.text | bash
```
Using wget:
```bash
wget https://xmake.io/shget.text -O - | bash
```
Using powershell:
```powershell
irm https://xmake.io/psget.text | iex
```
#### Build Options
```text
# Switch build mode
xmake f -m debug/release
# Optional build parameters
-r : Rebuild the target
-v : Show detailed build logs
-y : Automatically confirm prompts
# Example
xmake b -vy crossdesk
```
#### Run Options
```bash
# Run in debug mode
xmake r -d crossdesk
```
For more information, please refer to the [official Xmake documentation](https://xmake.io/guide/quick-start.html).
---
## Self-Hosted Server
### Server
It is recommended to deploy CrossDesk Server using Docker.
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=xxx.xxx.xxx.xxx \
-e INTERNAL_IP=xxx.xxx.xxx.xxx \
-e CROSSDESK_SERVER_PORT=xxxx \
-e COTURN_PORT=xxxx \
-e MIN_PORT=xxxxx \
-e MAX_PORT=xxxxx \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
The parameters you need to pay attention to are as follows:
#### Parameters
- **EXTERNAL_IP**: The server’s public IP. This corresponds to **Server Address** in the CrossDesk client’s **Self-Hosted Server Configuration**.
- **INTERNAL_IP**: The server’s internal IP.
- **CROSSDESK_SERVER_PORT**: The port used by the self-hosted service. This corresponds to **Server Port** in the CrossDesk client’s **Self-Hosted Server Configuration**.
- **COTURN_PORT**: The port used by the COTURN service. This corresponds to **Relay Service Port** in the CrossDesk client’s **Self-Hosted Server Configuration**.
- **MIN_PORT / MAX_PORT**: The port range used by the COTURN service. Example: `MIN_PORT=50000`, `MAX_PORT=60000`. Adjust the range depending on the number of clients.
- `-v /var/lib/crossdesk:/var/lib/crossdesk`: Persists database and certificate files on the host machine.
- `-v /var/log/crossdesk:/var/log/crossdesk`: Persists log files on the host machine.
#### Example
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=114.114.114.114 \
-e INTERNAL_IP=10.0.0.1 \
-e CROSSDESK_SERVER_PORT=9099 \
-e COTURN_PORT=3478 \
-e MIN_PORT=50000 \
-e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
#### Notes
- **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORT–MAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.**
- If you don’t mount volumes, all data will be lost when the container is removed.
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`. As the default certificates are self-signed and cannot guarantee security, it is strongly recommended to apply for a trusted certificate from a cloud provider, deploy it to this directory, and restart the service.
- The database file will be automatically created and stored at `/var/lib/crossdesk/db/crossdesk-server.db`.
- Log files will be created and stored at `/var/log/crossdesk/`.
#### Directory permissions
If the directories automatically created by Docker belong to root and have insufficient write permissions, the container user may not be able to write to them. This can cause:
- Certificate generation failure, leading to startup script errors and container exit.
- Database directory creation failure, causing the program to throw exceptions and crash.
- Log directory creation failure, preventing logs from being written (though the program may continue running).
Manually set permissions before starting the container:
```bash
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
```
#### TLS certificate
Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/your/certs** directory.
### Client
1. Click the settings icon in the top-right corner to enter the settings page.<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
2. Click `Self-Hosted Server Configuration` button.<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**) and click OK button.
4. Check the `Self-hosted server configuration` option and click the OK button to save the settings. If the server is using a valid (official) certificate, the process ends here and the client will show that it is connected to the server.
5. If the default certificate is used (skip this step if an official certificate is used), download the self-signed root certificate `api.crossdesk.cn_root.crt` from the server directory /var/lib/crossdesk/certs/ to the machine running the client, and install the certificate by executing the following command:
**Windows:** Open PowerShell with administrator privileges and execute:
```powershell
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
**Linux:**
```bash
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
**macOS:**
```bash
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
### Self-Hosted Web Client
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client).
---
## FAQ
See [FAQ](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md).
---
## Acknowledgements
- Thanks to [HelloGitHub](https://hellogithub.com/) for the recommendation and exposure.
- Thanks to [Ruanyf Weekly](https://github.com/ruanyf/weekly) for featuring CrossDesk.
- Thanks to the [LinuxDo](https://linux.do) community for the attention, discussions, and support that helped improve CrossDesk.
---
## Code signing policy
CrossDesk uses SignPath.io to code-sign official Windows releases built from this repository.
CrossDesk uses SignPath.io to sign official Windows releases built from this repository.
**Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).**
- **Committers and reviewers:** [kunkundi](https://github.com/kunkundi)
- **Approvers:** [kunkundi](https://github.com/kunkundi)
- **Privacy policy:** [Read the CrossDesk Privacy Policy](PRIVACY.md#english)
@@ -0,0 +1,13 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
IDI_CROSSDESK_LOGO ICON "crossdesk.ico"
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Privacy Window"
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_privacy_window.dll"
#define CROSSDESK_FILE_TYPE VFT_DLL
#include "version_info.rcinc"
@@ -0,0 +1,12 @@
Copyright 2014-2021 Amyuni Technologies Inc.
https://www.amyuni.com
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
By using this software, you accept to be prompted with an advertisement page which is not always under the control of the author. A fully commercial version is available for users who do not wisth to see those advertisements.
Permission is granted to anyone to use this software for any purpose subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software.
If you use this software in a product, an acknowledgment in the product documentation would be required.
2. Altered versions must be plainly marked as such, and must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any distribution.
@@ -0,0 +1,50 @@
Procedure to add a virtual monitor to your system (Windows 10 Only!)
- Download our virtual display driver from https://www.amyuni.com/downloads/usbmmidd_v2.zip
- Unpack the zip file to an empty folder, e.g. c:\temp\usbmmidd
- Make sure you read the License.txt file as with any other software product
- Open a command prompt window as Administrator (you cannot add a device to your system unless you "Run As Administrator")
- Run the following commands:
- cd c:\temp\usbmmid (or whatever destination folder you chose)
- deviceinstaller64 install usbmmidd.inf usbmmidd (If you are on a 32-bit system, replace "deviceinstaller64" by "deviceinstaller")
- Make sure you see the message that the drivers are signed by Amyuni Technologies Inc. This is a confirmation that the drivers went through Microsoft driver signing procedure and are virus free
- Starting with version 2, the virtual monitor supports the following display resolutions by default:
1024 x 768 pixels
1360 x 768 pixels
1440 x 900 pixels
1600 x 900 pixels
1600 x 1200 pixels
1920 x 1080 pixels [Default Resolution]
1920 x 1200 pixels
2560 x 1440 pixels
3840 x 2160 pixels
After the first step above is executed, you can modify the list of supported resolutions by changing the following registry key:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WUDF\Services\usbmmIdd\Parameters\Monitors
Up to 10 resolutions can be defined through this registry key. The keys should be named from "0" to "9". The (Default) registry value defines the default monitor resolution.
Note that the list of resolutions can also be found in the usbmmidd.inf file that accompanies the printer driver. The inf file should not be modified in any way, otherwise the virtual display driver digital signature is invalidated.
Once you are satisfied with the list of resolutions, you can activate the virtual monitor by running:
- deviceinstaller64 enableidd 1
With version 2, rou can run this command multiple times to add up to 4 virtual monitors to your system.
The included batch file usbmmidd.bat runs these commands automatically and selects between the 32- or 64-bit version of deviceinstaller.
If you are not comfortable opening a command prompt and entering these commands, right-click on usbmmidd.bat and "Run As Administrator."
That's it. You now have a high definition monitor in your Display Settings. You can change the display resolution, position the monitor, extend the desktop or do whatever you do with a real monitor.
To deactivate the virtual monitor, run the command:
deviceinstaller64 enableidd 0
If ou have added more than one monitor, you should run this command multiple times to remove all monitors.
to reactivate a virtual monitor:
deviceinstaller64 enableidd 1
To completely remove the drivers from your system, you can either go through the Device Manager and uninstall "USB Mobile Monitor Virtual Display", or run the commands:
deviceinstaller64 stop usbmmidd
deviceinstaller64 remove usbmmid
@@ -0,0 +1,16 @@
@cd /d "%~dp0"
@goto %PROCESSOR_ARCHITECTURE%
@exit
:AMD64
@cmd /c deviceinstaller64.exe install usbmmidd.inf usbmmidd
@cmd /c deviceinstaller64.exe enableidd 1
@goto end
:x86
@cmd /c deviceinstaller.exe install usbmmidd.inf usbmmidd
@cmd /c deviceinstaller.exe enableidd 1
:end
@pause
+16 -4
View File
@@ -1,6 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
# Keep readelf and ldd output stable for the runtime dependency checks below.
export LC_ALL=C
if [[ $# -lt 3 || $# -gt 4 ]]; then
echo "Usage: $0 <debian-arch> <xmake-arch> <version> [extra-recommendation]" >&2
exit 2
@@ -18,6 +21,10 @@ DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
TRAY_RUNTIME_RECOMMENDS="libayatana-appindicator3-1 | libappindicator3-1"
# Every published package includes the prerequisites for all Linux headless
# paths, including a complete independent desktop. Older Ubuntu releases ship
# dbus-run-session in dbus; newer releases split it into dbus-daemon.
HEADLESS_RUNTIME_DEPS="xvfb, xfce4, dbus-daemon | dbus"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
@@ -84,7 +91,7 @@ if [[ ! -x "$BUILD_BINARY" ]]; then
exit 1
fi
for command_name in readelf ldd dpkg-deb; do
for command_name in readelf ldd dpkg-deb strip; do
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "Required packaging command is missing: $command_name" >&2
exit 1
@@ -125,6 +132,9 @@ if [[ -n "$SLINT_NEEDED" ]]; then
exit 1
fi
install -m 0755 "$SLINT_LIBRARY" "$PRIVATE_DIR/$SLINT_NEEDED"
# Keep dynamic exports/relocations while removing unneeded symbols from the
# staged copy. Never strip Xmake's cached runtime in place.
strip --strip-unneeded "$PRIVATE_DIR/$SLINT_NEEDED"
fi
cat > "$BIN_DIR/$PKG_NAME" <<'EOF'
@@ -158,10 +168,11 @@ Architecture: $DEBIAN_ARCH
Maintainer: $MAINTAINER
Description: $DESCRIPTION
Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
libxrender1, libxft2, libxrandr2, libxfixes3, libxcursor1, libxcb1, libxcb-randr0,
libxrender1, libxft2, libxrandr2, libxfixes3, libxcursor1, libxi6, libxcb1, libxcb-randr0,
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1,
$HEADLESS_RUNTIME_DEPS
Recommends: $RECOMMENDS
Priority: optional
Section: utils
@@ -169,11 +180,12 @@ EOF
cat > "$DESKTOP_DIR/$PKG_NAME.desktop" <<EOF
[Desktop Entry]
Version=$DEB_VERSION
Version=1.0
Name=$APP_NAME
Comment=$DESCRIPTION
Exec=/usr/bin/$PKG_NAME
Icon=$PKG_NAME
StartupWMClass=$PKG_NAME
Terminal=false
Type=Application
Categories=Utility;
+6 -2
View File
@@ -11,7 +11,7 @@ EXPECTED_ARCH="$2"
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
for command_name in codesign install_name_tool lipo otool; do
for command_name in codesign install_name_tool lipo otool strip; do
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "Required bundling command is missing: $command_name" >&2
exit 1
@@ -123,7 +123,11 @@ if ! otool -l "$APP_EXECUTABLE" | awk '
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP_EXECUTABLE"
fi
# install_name_tool invalidates the linker's ad-hoc signature. Sign nested code
# Strip only the staged runtime, retaining exported symbols for Slint's C ABI.
# The package cache remains intact for debugging and other consumers.
strip -S -x "$FRAMEWORKS_DIR/$SLINT_NAME"
# install_name_tool and strip invalidate the linker's ad-hoc signature. Sign nested code
# first, then seal the complete app bundle so local packages remain launchable.
codesign --force --sign - "$FRAMEWORKS_DIR/$SLINT_NAME"
codesign --force --deep --sign - "$APP_BUNDLE"
+63 -2
View File
@@ -88,8 +88,16 @@ installApp:
; Bundle service-side binaries required by the Windows service flow
File "..\..\..\..\build\windows\x64\release\crossdesk_service.exe"
File "..\..\..\..\build\windows\x64\release\crossdesk_session_helper.exe"
; Bundle runtime DLLs from the release output directory
File "..\..\..\..\build\windows\x64\release\*.dll"
File "..\..\..\..\build\windows\x64\release\crossdesk_privacy_cursor_helper.exe"
File "..\..\..\..\build\windows\x64\release\crossdesk_privacy_window.dll"
; Require the Slint runtime so an incomplete build cannot be packaged.
File "..\..\..\..\build\windows\x64\release\slint_cpp.dll"
; Bundle the remaining runtime DLLs from the release output directory
File /x crossdesk_privacy_window.dll /x slint_cpp.dll "..\..\..\..\build\windows\x64\release\*.dll"
; Amyuni usbmmidd_v2 virtual display driver for headless hosts. xmake
; copies apps\desktop\resources\windows\usbmmidd_v2 into the release
; output directory after building crossdesk; its License.txt ships with it.
File /r "..\..\..\..\build\windows\x64\release\usbmmidd_v2"
Call RegisterInstalledService
@@ -140,13 +148,19 @@ cancelUninstall:
uninstallApp:
Call un.UnregisterInstalledService
Call un.RemoveUsbmmiddDriver
; Delete main executable and uninstaller
Delete "$INSTDIR\CrossDesk.exe"
Delete "$INSTDIR\crossdesk_service.exe"
Delete "$INSTDIR\crossdesk_session_helper.exe"
Delete "$INSTDIR\crossdesk_privacy_cursor_helper.exe"
Delete "$INSTDIR\crossdesk_privacy_window.dll"
Delete "$INSTDIR\uninstall.exe"
; Leave the install directory before removing it
SetOutPath "$TEMP"
; Recursively delete installation directory
RMDir /r "$INSTDIR"
@@ -244,3 +258,50 @@ unregister_with_sc:
Abort
${EndIf}
FunctionEnd
; The session helper (or an elevated CrossDesk) installs the bundled usbmmidd
; driver on first headless use and records HKLM\SOFTWARE\CrossDesk\
; UsbmmiddDriverInstalled in the 64-bit registry view. Only a driver CrossDesk
; installed is removed here; one set up by the user for other software stays.
; deviceinstaller64.exe resolves usbmmIdd.dll relative to the working
; directory, so run it from the driver folder. Failures are logged, not fatal:
; the driver can still be removed from Device Manager.
Function un.RemoveUsbmmiddDriver
IfFileExists "$INSTDIR\usbmmidd_v2\deviceinstaller64.exe" 0 done
SetRegView 64
ClearErrors
ReadRegDWORD $0 HKLM "Software\${PRODUCT_NAME}" "UsbmmiddDriverInstalled"
${If} ${Errors}
${OrIf} $0 != 1
SetRegView lastused
Return
${EndIf}
DetailPrint "Removing the usbmmidd virtual display driver installed by CrossDesk"
SetOutPath "$INSTDIR\usbmmidd_v2"
; Unplug every virtual monitor first (the driver allows up to four).
StrCpy $1 0
unplug_loop:
nsExec::ExecToLog '"$INSTDIR\usbmmidd_v2\deviceinstaller64.exe" enableidd 0'
Pop $2
IntOp $1 $1 + 1
${If} $2 = 0
${AndIf} $1 < 4
Goto unplug_loop
${EndIf}
nsExec::ExecToLog '"$INSTDIR\usbmmidd_v2\deviceinstaller64.exe" stop usbmmidd'
Pop $2
nsExec::ExecToLog '"$INSTDIR\usbmmidd_v2\deviceinstaller64.exe" remove usbmmidd'
Pop $2
${If} $2 != 0
DetailPrint "usbmmidd driver removal returned $2; remove 'USB Mobile Monitor Virtual Display' from Device Manager if it remains"
${EndIf}
SetOutPath "$TEMP"
DeleteRegValue HKLM "Software\${PRODUCT_NAME}" "UsbmmiddDriverInstalled"
DeleteRegKey /ifempty HKLM "Software\${PRODUCT_NAME}"
SetRegView lastused
done:
FunctionEnd
+125 -20
View File
@@ -6,6 +6,7 @@
#endif
#endif
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>
@@ -15,14 +16,24 @@
#ifdef _WIN32
#include <cstdio>
#include "platform/windows/gui/slint_backend.h"
#include "service_host.h"
#endif
#include "config_center.h"
#include "daemon.h"
#include "path_manager.h"
#include "platform/single_instance.h"
#include "rd_log.h"
#include "render.h"
#ifdef __linux__
#include "SimpleIni.h"
#include "platform/linux/headless/headless_console.h"
#include "platform/linux/headless/headless_session.h"
#include "platform/linux/privacy/privacy_guard.h"
#endif
#ifdef _WIN32
namespace {
@@ -173,27 +184,37 @@ int HandleServiceCliCommand(const std::string& command) {
} // namespace
#endif
int main(int argc, char* argv[]) {
#ifdef _WIN32
if (argc > 1 && IsServiceCliCommand(argv[1])) {
return HandleServiceCliCommand(argv[1]);
static bool IsDaemonChild(int argc, char* argv[]) {
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--child") == 0) return true;
}
#endif
return false;
}
// check if running as child process
bool is_child = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--child") == 0) {
is_child = true;
break;
}
static crossdesk::platform::InstanceResult AcquireInstance(
crossdesk::platform::SingleInstanceGuard& guard,
crossdesk::platform::InstanceRole role) {
std::string error;
const auto result = guard.TryAcquire(role, error);
if (result == crossdesk::platform::InstanceResult::kAlreadyRunning) {
std::cerr << "CrossDesk is already running for this user.\n";
} else if (result == crossdesk::platform::InstanceResult::kError) {
std::cerr << "CrossDesk cannot start: " << error << '\n';
}
return result;
}
if (is_child) {
static int RunApplication(
int argc, char* argv[],
crossdesk::platform::SingleInstanceGuard& client_instance) {
// The instance guards are held before any configuration or logger is opened.
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
crossdesk::InitLogger(path_manager->GetLogPath().string());
if (IsDaemonChild(argc, argv)) {
// child process: run render directly
crossdesk::Render render;
render.Run();
return 0;
return render.Run();
}
#ifdef _WIN32
@@ -201,19 +222,38 @@ int main(int argc, char* argv[]) {
#endif
bool enable_daemon = false;
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
if (path_manager) {
std::string cache_path = path_manager->GetCachePath().string();
crossdesk::ConfigCenter config_center(cache_path + "/config.ini");
enable_daemon = config_center.IsEnableDaemon();
}
#ifdef __linux__
// The headless supervisor owns the display and application lifetime. A
// detached daemon would outlive its display; systemd can restart the whole
// session instead.
if (std::getenv("CROSSDESK_HEADLESS_ACTIVE") ||
crossdesk::HeadlessConsole::Instance().active()) enable_daemon = false;
#endif
if (enable_daemon) {
// The launcher lease stays in main for the supervisor's entire lifetime.
// Each --child must acquire its own client lease, so it is neither blocked
// by its supervisor nor able to bypass the single-client restriction.
client_instance.Release();
// start daemon with restart monitoring
Daemon daemon("CrossDesk");
// define main loop function: run render and stop daemon on normal exit
Daemon::MainLoopFunc main_loop = [&daemon]() {
Daemon::MainLoopFunc main_loop = [&daemon, &client_instance]() {
// Executable discovery can fail and make the supervisor run the GUI in
// process. This fallback must retain the same client ownership rule.
if (AcquireInstance(client_instance,
crossdesk::platform::InstanceRole::kClient) !=
crossdesk::platform::InstanceResult::kAcquired) {
daemon.stop();
return;
}
crossdesk::Render render;
render.Run();
daemon.stop();
@@ -226,6 +266,71 @@ int main(int argc, char* argv[]) {
// run without daemon: direct execution
crossdesk::Render render;
render.Run();
return 0;
}
return render.Run();
}
int main(int argc, char* argv[]) {
#ifdef __linux__
// Run before GUI, configuration, logging or display initialization.
if (argc == 2 &&
std::strcmp(argv[1], crossdesk::kLinuxPrivacyGuardArgument) == 0) {
return crossdesk::RunLinuxPrivacyGuard();
}
auto& console = crossdesk::HeadlessConsole::Instance();
if (argc == 2 && std::strcmp(argv[1], "--headless-help") == 0) {
crossdesk::PathManager paths("CrossDesk");
CSimpleIniA config;
config.SetUnicode(true);
config.LoadFile((paths.GetCachePath() / "config.ini").string().c_str());
console.SetLanguage(static_cast<int>(config.GetLongValue("Settings", "language", 0)));
std::cout << console.Text("console_cli_help");
return 0;
}
#endif
#ifdef _WIN32
if (argc == 2 &&
std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) {
return crossdesk::RunSlintRendererProbe();
}
if (argc > 1 && IsServiceCliCommand(argv[1])) {
EnsureConsoleForCli();
crossdesk::platform::SingleInstanceGuard cli_instance;
if (AcquireInstance(cli_instance, crossdesk::platform::InstanceRole::kServiceCli) !=
crossdesk::platform::InstanceResult::kAcquired) return 1;
crossdesk::PathManager paths("CrossDesk");
crossdesk::InitLogger(paths.GetLogPath().string(), "crossdesk-service-cli");
return HandleServiceCliCommand(argv[1]);
}
#endif
using crossdesk::platform::InstanceResult;
using crossdesk::platform::InstanceRole;
crossdesk::platform::SingleInstanceGuard launcher_instance;
crossdesk::platform::SingleInstanceGuard client_instance;
if (!IsDaemonChild(argc, argv)) {
const auto result = AcquireInstance(launcher_instance, InstanceRole::kLauncher);
if (result != InstanceResult::kAcquired)
return result == InstanceResult::kAlreadyRunning ? 0 : 1;
}
const auto result = AcquireInstance(client_instance, InstanceRole::kClient);
if (result != InstanceResult::kAcquired)
return result == InstanceResult::kAlreadyRunning ? 0 : 1;
#ifdef __linux__
const int exit_code = crossdesk::RunWithLinuxDisplay(
argc, argv, [&] { return RunApplication(argc, argv, client_instance); }, [&] {
crossdesk::PathManager paths("CrossDesk");
if (console.Enable(paths.GetLogPath(), paths.GetCachePath() / "config.ini")) return true;
std::cerr << "无法初始化日志或控制台,启动失败。\n";
return false;
});
if (console.active()) {
console.Notify(exit_code == 0 ? console.Text("console_exited")
: console.Text("console_stopped") + " " + std::to_string(exit_code) +
"; " + console.Text("console_log_file") + ": " + console.diagnostic_path());
}
return exit_code;
#else
return RunApplication(argc, argv, client_instance);
#endif
}
@@ -1,63 +0,0 @@
#include "rounded_corner_button.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled,
ImU32 normal_col, ImU32 hover_col, ImU32 active_col,
ImU32 border_col) {
ImGuiWindow* current_window = ImGui::GetCurrentWindow();
if (current_window->SkipItems) return false;
const ImGuiStyle& style = ImGui::GetStyle();
ImGuiID button_id = current_window->GetID(label);
ImVec2 cursor_pos = current_window->DC.CursorPos;
ImVec2 button_size = ImGui::CalcItemSize(size, 0.0f, 0.0f);
ImRect button_rect(cursor_pos, ImVec2(cursor_pos.x + button_size.x,
cursor_pos.y + button_size.y));
ImGui::ItemSize(button_rect);
if (!ImGui::ItemAdd(button_rect, button_id)) return false;
bool is_hovered = false, is_held = false;
bool is_pressed = false;
if (enabled) {
is_pressed =
ImGui::ButtonBehavior(button_rect, button_id, &is_hovered, &is_held);
}
if (normal_col == 0) normal_col = ImGui::GetColorU32(ImGuiCol_Button);
if (hover_col == 0) hover_col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
if (active_col == 0) active_col = ImGui::GetColorU32(ImGuiCol_ButtonActive);
if (border_col == 0) border_col = ImGui::GetColorU32(ImGuiCol_Border);
ImU32 fill_color = normal_col;
if (is_held && is_hovered)
fill_color = active_col;
else if (is_hovered)
fill_color = hover_col;
if (!enabled) fill_color = IM_COL32(120, 120, 120, 180);
ImDrawList* window_draw_list = ImGui::GetWindowDrawList();
window_draw_list->AddRectFilled(button_rect.Min, button_rect.Max, fill_color,
rounding, round_flags);
if (style.FrameBorderSize > 0.0f) {
window_draw_list->AddRect(button_rect.Min, button_rect.Max, border_col,
rounding, round_flags, style.FrameBorderSize);
}
ImU32 text_color =
ImGui::GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled);
const char* label_end = ImGui::FindRenderedTextEnd(label);
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(text_color));
ImGui::RenderTextClipped(button_rect.Min, button_rect.Max, label, label_end,
nullptr, ImVec2(0.5f, 0.5f), &button_rect);
ImGui::PopStyleColor();
return is_pressed;
}
} // namespace crossdesk
@@ -1,20 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _ROUNDED_CORNER_BUTTON_H_
#define _ROUNDED_CORNER_BUTTON_H_
#include "imgui.h"
#include "imgui_internal.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled = true,
ImU32 normal_col = 0, ImU32 hover_col = 0,
ImU32 active_col = 0, ImU32 border_col = 0);
} // namespace crossdesk
#endif
@@ -0,0 +1,88 @@
#include "task_queue_lock_free.h"
#include <concurrentqueue.h>
#include <exception>
#include <new>
#include <stdexcept>
#include <utility>
namespace crossdesk {
struct TaskQueueLockFree::QueueStorage {
moodycamel::ConcurrentQueue<TaskItem> tasks;
// All submissions share one producer so different caller threads keep FIFO.
moodycamel::ProducerToken producer{tasks};
};
TaskQueueLockFree::TaskQueueLockFree()
: queue_(std::make_unique<QueueStorage>()),
worker_([this] { WorkerThread(); }) {}
TaskQueueLockFree::~TaskQueueLockFree() { Stop(); }
std::future<void> TaskQueueLockFree::PostTask(std::function<void()> task) {
TaskItem item;
item.run = std::move(task);
auto result = item.completion.get_future();
{
std::lock_guard lock(mutex_);
if (stopping_) {
throw std::runtime_error("Cannot post to a stopped task queue");
}
// Count before publishing: the worker may dequeue as soon as enqueue returns.
++pending_tasks_;
if (!queue_->tasks.enqueue(queue_->producer, std::move(item))) {
--pending_tasks_;
throw std::bad_alloc();
}
}
wake_.notify_one();
return result;
}
void TaskQueueLockFree::Stop() {
// Concurrent Stop callers all wait for the same worker shutdown to finish.
std::call_once(stop_once_, [this] {
{
std::lock_guard lock(mutex_);
stopping_ = true;
}
wake_.notify_all();
worker_.join();
});
}
void TaskQueueLockFree::WorkerThread() {
while (true) {
TaskItem item;
while (queue_->tasks.try_dequeue(item)) {
--pending_tasks_;
std::exception_ptr error;
try {
item.run();
} catch (...) {
error = std::current_exception();
}
// Release captured resources before marking the task complete.
item.run = nullptr;
if (error) {
item.completion.set_exception(error);
} else {
item.completion.set_value();
}
}
std::unique_lock lock(mutex_);
wake_.wait(lock, [this] { return stopping_ || pending_tasks_ > 0; });
// A stop request must not discard tasks submitted while the worker was idle.
if (stopping_ && pending_tasks_ == 0) {
return;
}
}
}
} // namespace crossdesk
@@ -0,0 +1,55 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _TASK_QUEUE_LOCK_FREE_H_
#define _TASK_QUEUE_LOCK_FREE_H_
#include <atomic>
#include <condition_variable>
#include <cstddef>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <thread>
namespace crossdesk {
// A single worker executes tasks in submission order. The concurrent queue
// stores tasks; a mutex coordinates submissions, worker wakeups and shutdown.
class TaskQueueLockFree {
public:
TaskQueueLockFree();
~TaskQueueLockFree();
// Returns completion or the task's exception. Throws if the queue is stopped.
std::future<void> PostTask(std::function<void()> task);
// Rejects new tasks, finishes accepted tasks and joins the worker.
// May be called repeatedly, but never from the queue's own tasks.
void Stop();
private:
struct TaskItem {
std::function<void()> run;
std::promise<void> completion;
};
void WorkerThread();
struct QueueStorage;
std::unique_ptr<QueueStorage> queue_;
std::atomic<std::size_t> pending_tasks_{0};
std::mutex mutex_;
std::condition_variable wake_;
bool stopping_ = false;
std::once_flag stop_once_;
std::thread worker_;
};
} // namespace crossdesk
#endif
+211 -269
View File
@@ -1,10 +1,20 @@
#include "config_center.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <optional>
#include <vector>
#include "SimpleIni.h"
#include "autostart.h"
#include "rd_log.h"
namespace crossdesk {
struct ConfigCenter::IniStorage : CSimpleIniA {};
namespace {
bool IsValidTurnModeValue(long value) {
@@ -12,39 +22,38 @@ bool IsValidTurnModeValue(long value) {
value <= static_cast<long>(ConfigCenter::TURN_MODE::FORCE_TCP);
}
bool IsValidVideoAdaptationPolicyValue(long value) {
using Policy = ConfigCenter::VIDEO_ADAPTATION_POLICY;
return value >= static_cast<long>(Policy::FRAME_RATE_PRIORITY) &&
value <= static_cast<long>(Policy::BALANCED);
}
} // namespace
ConfigCenter::ConfigCenter(const std::string& config_path)
: config_path_(config_path) {
ini_.SetUnicode(true);
: config_path_(config_path), ini_(std::make_unique<IniStorage>()) {
ini_->SetUnicode(true);
Load();
}
ConfigCenter::~ConfigCenter() {}
int ConfigCenter::Load() {
SI_Error rc = ini_.LoadFile(config_path_.c_str());
SI_Error rc = ini_->LoadFile(config_path_.c_str());
if (rc < 0) {
Save();
return -1;
}
bool persist_config_migration = false;
persist_config_migration |= ini_.Delete(section_, "video_content_type");
persist_config_migration |= ini_.Delete(section_, "screen_content");
// These options now belong only to the active remote connection. Discard
// legacy local defaults so they cannot affect this host or new connections.
persist_config_migration |= ini_->Delete(section_, "video_quality");
persist_config_migration |= ini_->Delete(section_, "video_frame_rate");
persist_config_migration |= ini_->Delete(section_, "video_adaptation_policy");
persist_config_migration |= ini_->Delete(section_, "video_content_type");
persist_config_migration |= ini_->Delete(section_, "screen_content");
persist_config_migration |=
ini_.Delete(section_, "enable_desktop_quality_optimization");
ini_->Delete(section_, "enable_desktop_quality_optimization");
persist_config_migration |=
ini_.Delete(section_, "enable_minimize_to_tray");
ini_->Delete(section_, "enable_minimize_to_tray");
const long language_value =
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
ini_->GetLongValue(section_, "language", static_cast<long>(language_));
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
language_value > static_cast<long>(LANGUAGE::RUSSIAN)) {
language_ = LANGUAGE::ENGLISH;
@@ -52,33 +61,23 @@ int ConfigCenter::Load() {
language_ = static_cast<LANGUAGE>(language_value);
}
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
section_, "video_quality", static_cast<long>(video_quality_)));
video_frame_rate_ = static_cast<VIDEO_FRAME_RATE>(ini_.GetLongValue(
section_, "video_frame_rate", static_cast<long>(video_frame_rate_)));
const long video_adaptation_policy_value = ini_.GetLongValue(
section_, "video_adaptation_policy",
static_cast<long>(video_adaptation_policy_));
if (IsValidVideoAdaptationPolicyValue(video_adaptation_policy_value)) {
video_adaptation_policy_ =
static_cast<VIDEO_ADAPTATION_POLICY>(video_adaptation_policy_value);
} else {
LOG_WARN("Invalid video adaptation policy [{}], using quality priority",
video_adaptation_policy_value);
video_adaptation_policy_ = VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY;
}
const long screen_capture_method_value = ini_->GetLongValue(
section_, "screen_capture_method",
static_cast<long>(ScreenCaptureMethod::Auto));
screen_capture_method_ = IsValidScreenCaptureMethod(screen_capture_method_value)
? static_cast<ScreenCaptureMethod>(
screen_capture_method_value)
: ScreenCaptureMethod::Auto;
video_encode_format_ = static_cast<VIDEO_ENCODE_FORMAT>(
ini_.GetLongValue(section_, "video_encode_format",
ini_->GetLongValue(section_, "video_encode_format",
static_cast<long>(video_encode_format_)));
hardware_video_codec_ = ini_.GetBoolValue(section_, "hardware_video_codec",
hardware_video_codec_ = ini_->GetBoolValue(section_, "hardware_video_codec",
hardware_video_codec_);
const char* turn_mode_value = ini_.GetValue(section_, "turn_mode", nullptr);
const char* turn_mode_value = ini_->GetValue(section_, "turn_mode", nullptr);
if (turn_mode_value != nullptr && strlen(turn_mode_value) > 0) {
const long parsed_turn_mode = ini_.GetLongValue(
const long parsed_turn_mode = ini_->GetLongValue(
section_, "turn_mode", static_cast<long>(turn_mode_));
if (IsValidTurnModeValue(parsed_turn_mode)) {
turn_mode_ = static_cast<TURN_MODE>(parsed_turn_mode);
@@ -88,19 +87,24 @@ int ConfigCenter::Load() {
turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
}
} else {
const bool legacy_enable_turn = ini_.GetBoolValue(
const bool legacy_enable_turn = ini_->GetBoolValue(
section_, "enable_turn", turn_mode_ != TURN_MODE::DISABLED);
turn_mode_ = legacy_enable_turn ? TURN_MODE::AUTO_UDP_TCP
: TURN_MODE::DISABLED;
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
ini_->SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
persist_config_migration = true;
}
// Migrate the former user switch. Keep true in the file for older builds,
// while the current application always requests encrypted native media.
if (!ini_->GetBoolValue(section_, "enable_srtp", false)) {
ini_->SetBoolValue(section_, "enable_srtp", true);
persist_config_migration = true;
}
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
enable_self_hosted_ =
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
ini_->GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
const char* signal_server_host_value =
ini_.GetValue(section_, "signal_server_host", nullptr);
ini_->GetValue(section_, "signal_server_host", nullptr);
if (signal_server_host_value != nullptr &&
strlen(signal_server_host_value) > 0) {
signal_server_host_ = signal_server_host_value;
@@ -108,33 +112,25 @@ int ConfigCenter::Load() {
signal_server_host_ = "";
}
const char* signal_server_port_value =
ini_.GetValue(section_, "signal_server_port", nullptr);
ini_->GetValue(section_, "signal_server_port", nullptr);
if (signal_server_port_value != nullptr &&
strlen(signal_server_port_value) > 0) {
signal_server_port_ =
static_cast<int>(ini_.GetLongValue(section_, "signal_server_port", 0));
static_cast<int>(ini_->GetLongValue(section_, "signal_server_port", 0));
} else {
signal_server_port_ = 0;
}
const char* coturn_server_port_value =
ini_.GetValue(section_, "coturn_server_port", nullptr);
if (coturn_server_port_value != nullptr &&
strlen(coturn_server_port_value) > 0) {
coturn_server_port_ =
static_cast<int>(ini_.GetLongValue(section_, "coturn_server_port", 0));
} else {
coturn_server_port_ = 0;
}
enable_autostart_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_->GetBoolValue(section_, "enable_autostart", enable_autostart_);
enable_daemon_ = ini_->GetBoolValue(section_, "enable_daemon", enable_daemon_);
enable_privacy_screen_.store(
ini_->GetBoolValue(section_, "enable_privacy_screen", true));
portable_service_prompt_suppressed_ =
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
ini_->GetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
const char* file_transfer_save_path_value =
ini_.GetValue(section_, "file_transfer_save_path", nullptr);
ini_->GetValue(section_, "file_transfer_save_path", nullptr);
if (file_transfer_save_path_value != nullptr &&
strlen(file_transfer_save_path_value) > 0) {
file_transfer_save_path_ = file_transfer_save_path_value;
@@ -142,48 +138,89 @@ int ConfigCenter::Load() {
file_transfer_save_path_ = "";
}
if (persist_config_migration && ini_.SaveFile(config_path_.c_str()) < 0) {
if (persist_config_migration && CommitIni() < 0) {
return -1;
}
return 0;
}
int ConfigCenter::CommitIni() {
// Write beside the destination and rename only after a complete save. A
// failed write must not truncate the last usable configuration.
static std::atomic<unsigned long> sequence{0};
const std::string temporary = config_path_ + ".tmp-" +
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) +
"-" + std::to_string(sequence++);
std::error_code error;
if (ini_->SaveFile(temporary.c_str()) < 0) {
LOG_ERROR("Failed to write configuration: {}", config_path_);
std::filesystem::remove(temporary, error);
return -1;
}
const auto previous = std::filesystem::status(config_path_, error);
if (!error && std::filesystem::is_regular_file(previous)) {
std::filesystem::permissions(temporary, previous.permissions(), error);
if (error) {
LOG_ERROR("Failed to preserve configuration permissions: {}", error.message());
std::filesystem::remove(temporary, error);
return -1;
}
}
std::filesystem::rename(temporary, config_path_, error);
if (!error) return 0;
LOG_ERROR("Failed to replace configuration {}: {}", config_path_, error.message());
std::filesystem::remove(temporary, error);
return -1;
}
int ConfigCenter::StoreValues(
std::initializer_list<std::pair<const char*, std::string>> values) {
std::vector<std::pair<const char*, std::optional<std::string>>> previous;
for (const auto& value : values) {
const char* old = ini_->GetValue(section_, value.first, nullptr);
previous.emplace_back(value.first, old ? std::optional<std::string>(old) : std::nullopt);
ini_->SetValue(section_, value.first, value.second.c_str());
}
if (CommitIni() == 0) return 0;
for (const auto& value : previous) {
if (value.second) ini_->SetValue(section_, value.first, value.second->c_str());
else ini_->Delete(section_, value.first);
}
return -1;
}
int ConfigCenter::Save() {
ini_.SetLongValue(section_, "language", static_cast<long>(language_));
ini_.SetLongValue(section_, "video_quality",
static_cast<long>(video_quality_));
ini_.SetLongValue(section_, "video_frame_rate",
static_cast<long>(video_frame_rate_));
ini_.SetLongValue(section_, "video_adaptation_policy",
static_cast<long>(video_adaptation_policy_));
ini_.SetLongValue(section_, "video_encode_format",
ini_->SetLongValue(section_, "language", static_cast<long>(language_));
ini_->SetLongValue(section_, "screen_capture_method",
static_cast<long>(screen_capture_method_));
ini_->SetLongValue(section_, "video_encode_format",
static_cast<long>(video_encode_format_));
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
ini_.SetBoolValue(section_, "enable_turn",
ini_->SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
ini_->SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
ini_->SetBoolValue(section_, "enable_turn",
turn_mode_ != TURN_MODE::DISABLED);
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
ini_->SetBoolValue(section_, "enable_srtp", true);
ini_->SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
// only save when self hosted
if (enable_self_hosted_) {
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_.SetLongValue(section_, "signal_server_port",
ini_->SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_->SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
}
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
ini_->SetBoolValue(section_, "enable_autostart", enable_autostart_);
ini_->SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_->SetBoolValue(section_, "enable_privacy_screen",
enable_privacy_screen_.load());
ini_->SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
ini_.SetValue(section_, "file_transfer_save_path",
ini_->SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
SI_Error rc = CommitIni();
if (rc < 0) {
return -1;
}
@@ -194,220 +231,124 @@ int ConfigCenter::Save() {
// setters
int ConfigCenter::SetLanguage(LANGUAGE language) {
const int value = static_cast<int>(language);
if (value < 0 || value > 2 || StoreValues({{"language", std::to_string(value)}}) != 0) return -1;
language_ = language;
ini_.SetLongValue(section_, "language", static_cast<long>(language_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetVideoQuality(VIDEO_QUALITY video_quality) {
video_quality_ = video_quality;
ini_.SetLongValue(section_, "video_quality",
static_cast<long>(video_quality_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
int ConfigCenter::SetScreenCaptureMethod(ScreenCaptureMethod method) {
if (!IsValidScreenCaptureMethod(static_cast<long>(method))) {
return -1;
}
ini_->SetLongValue(section_, "screen_capture_method", static_cast<long>(method));
if (CommitIni() < 0) {
ini_->SetLongValue(section_, "screen_capture_method",
static_cast<long>(screen_capture_method_));
return -1;
}
screen_capture_method_ = method;
return 0;
}
int ConfigCenter::SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate) {
video_frame_rate_ = video_frame_rate;
ini_.SetLongValue(section_, "video_frame_rate",
static_cast<long>(video_frame_rate_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
int ConfigCenter::SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT format) {
if (format != VIDEO_ENCODE_FORMAT::H264 && format != VIDEO_ENCODE_FORMAT::AV1) return -1;
if (StoreValues({{"video_encode_format", std::to_string(static_cast<int>(format))}}) != 0) return -1;
video_encode_format_ = format;
return 0;
}
int ConfigCenter::SetVideoAdaptationPolicy(
VIDEO_ADAPTATION_POLICY policy) {
video_adaptation_policy_ = policy;
ini_.SetLongValue(section_, "video_adaptation_policy",
static_cast<long>(video_adaptation_policy_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
int ConfigCenter::SetHardwareVideoCodec(bool enabled) {
if (enabled && !IsHardwareVideoCodecAvailable()) return -1;
if (StoreValues({{"hardware_video_codec", enabled ? "true" : "false"}}) != 0) return -1;
hardware_video_codec_ = enabled;
return 0;
}
int ConfigCenter::SetVideoEncodeFormat(
VIDEO_ENCODE_FORMAT video_encode_format) {
video_encode_format_ = video_encode_format;
ini_.SetLongValue(section_, "video_encode_format",
static_cast<long>(video_encode_format_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
int ConfigCenter::SetTurnMode(TURN_MODE mode) {
if (!IsValidTurnModeValue(static_cast<long>(mode))) return -1;
if (StoreValues({{"turn_mode", std::to_string(static_cast<int>(mode))},
{"enable_turn", mode == TURN_MODE::DISABLED ? "false" : "true"}}) != 0) return -1;
turn_mode_ = mode;
return 0;
}
int ConfigCenter::SetHardwareVideoCodec(bool hardware_video_codec) {
hardware_video_codec_ = hardware_video_codec;
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetTurnMode(TURN_MODE turn_mode) {
if (!IsValidTurnModeValue(static_cast<long>(turn_mode))) {
return -1;
}
turn_mode_ = turn_mode;
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
ini_.SetBoolValue(section_, "enable_turn",
turn_mode_ != TURN_MODE::DISABLED);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetTurn(bool enable_turn) {
int ConfigCenter::SetTurn(bool enable_turn, bool force_relay) {
if (!enable_turn) {
return SetTurnMode(TURN_MODE::DISABLED);
}
if (turn_mode_ == TURN_MODE::DISABLED) {
if (!force_relay) {
return SetTurnMode(TURN_MODE::AUTO_UDP_TCP);
}
return SetTurnMode(turn_mode_);
// Use TURN/UDP by default, preserving an explicitly configured TCP relay.
return SetTurnMode(turn_mode_ == TURN_MODE::FORCE_TCP ? TURN_MODE::FORCE_TCP
: TURN_MODE::FORCE_UDP);
}
int ConfigCenter::SetSrtp(bool enable_srtp) {
enable_srtp_ = enable_srtp;
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
int ConfigCenter::SetServerHost(const std::string& host) {
if (host.empty() || host.size() > 253 || host.find_first_of("/\\@?#") != std::string::npos ||
std::any_of(host.begin(), host.end(), [](unsigned char c) { return c <= 32 || c == 127; })) return -1;
if (StoreValues({{"signal_server_host", host}}) != 0) return -1;
signal_server_host_ = host;
return 0;
}
int ConfigCenter::SetServerHost(const std::string& signal_server_host) {
signal_server_host_ = signal_server_host;
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
int ConfigCenter::SetServerPort(int port) {
if (port < 1 || port > 65535 || StoreValues({{"signal_server_port", std::to_string(port)}}) != 0) return -1;
signal_server_port_ = port;
return 0;
}
int ConfigCenter::SetServerPort(int signal_server_port) {
signal_server_port_ = signal_server_port;
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
int ConfigCenter::SetSelfHosted(bool enabled) {
if (enabled && (signal_server_host_.empty() || signal_server_port_ < 1 || signal_server_port_ > 65535)) return -1;
if (StoreValues({{"enable_self_hosted", enabled ? "true" : "false"}}) != 0) return -1;
enable_self_hosted_ = enabled;
return 0;
}
int ConfigCenter::SetCoturnServerPort(int coturn_server_port) {
coturn_server_port_ = coturn_server_port;
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
enable_self_hosted_ = enable_self_hosted;
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
// load from config if self hosted is enabled
if (enable_self_hosted_) {
const char* signal_server_host_value =
ini_.GetValue(section_, "signal_server_host", nullptr);
if (signal_server_host_value != nullptr &&
strlen(signal_server_host_value) > 0) {
signal_server_host_ = signal_server_host_value;
}
const char* signal_server_port_value =
ini_.GetValue(section_, "signal_server_port", nullptr);
if (signal_server_port_value != nullptr &&
strlen(signal_server_port_value) > 0) {
signal_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "signal_server_port", 0));
}
const char* coturn_server_port_value =
ini_.GetValue(section_, "coturn_server_port", nullptr);
if (coturn_server_port_value != nullptr &&
strlen(coturn_server_port_value) > 0) {
coturn_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "coturn_server_port", 0));
}
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
}
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetAutostart(bool enable_autostart) {
enable_autostart_ = enable_autostart;
bool success = false;
if (enable_autostart) {
success = EnableAutostart("CrossDesk");
} else {
success = DisableAutostart("CrossDesk");
}
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
if (!success) {
int ConfigCenter::SetAutostart(bool enabled) {
const bool previous = IsAutostartEnabled("CrossDesk");
if (enabled != previous && !(enabled ? EnableAutostart("CrossDesk") : DisableAutostart("CrossDesk"))) {
LOG_ERROR("SetAutostart failed");
return -1;
}
if (StoreValues({{"enable_autostart", enabled ? "true" : "false"}}) != 0) {
if (enabled != previous) {
if (previous) EnableAutostart("CrossDesk");
else DisableAutostart("CrossDesk");
}
return -1;
}
enable_autostart_ = enabled;
return 0;
}
int ConfigCenter::SetDaemon(bool enable_daemon) {
enable_daemon_ = enable_daemon;
int ConfigCenter::SetDaemon(bool enabled) {
if (StoreValues({{"enable_daemon", enabled ? "true" : "false"}}) != 0) return -1;
enable_daemon_ = enabled;
return 0;
}
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
int ConfigCenter::SetPrivacyScreen(bool enable_privacy_screen) {
const bool previous = enable_privacy_screen_.load();
if (previous == enable_privacy_screen) return 0;
ini_->SetBoolValue(section_, "enable_privacy_screen", enable_privacy_screen);
if (CommitIni() < 0) {
ini_->SetBoolValue(section_, "enable_privacy_screen", previous);
LOG_ERROR("Failed to save automatic privacy screen preference");
return -1;
}
// RTC callbacks read only the value committed by the settings thread.
enable_privacy_screen_.store(enable_privacy_screen);
LOG_INFO("Privacy screen on incoming connection: {}", enable_privacy_screen);
return 0;
}
int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
portable_service_prompt_suppressed_ = suppressed;
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
ini_->SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
SI_Error rc = CommitIni();
if (rc < 0) {
return -1;
}
@@ -419,25 +360,26 @@ int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
ConfigCenter::VIDEO_QUALITY ConfigCenter::GetVideoQuality() const {
return video_quality_;
}
ConfigCenter::VIDEO_FRAME_RATE ConfigCenter::GetVideoFrameRate() const {
return video_frame_rate_;
}
ConfigCenter::VIDEO_ADAPTATION_POLICY
ConfigCenter::GetVideoAdaptationPolicy() const {
return video_adaptation_policy_;
ScreenCaptureMethod ConfigCenter::GetScreenCaptureMethod() const {
return screen_capture_method_;
}
ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
return video_encode_format_;
}
bool ConfigCenter::IsHardwareVideoCodecAvailable() {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
return true;
#else
return false;
#endif
}
bool ConfigCenter::IsHardwareVideoCodec() const {
return hardware_video_codec_;
return IsHardwareVideoCodecAvailable() && hardware_video_codec_;
}
ConfigCenter::TURN_MODE ConfigCenter::GetTurnMode() const {
@@ -448,7 +390,11 @@ bool ConfigCenter::IsEnableTurn() const {
return turn_mode_ != TURN_MODE::DISABLED;
}
bool ConfigCenter::IsEnableSrtp() const { return enable_srtp_; }
bool ConfigCenter::IsForceRelay() const {
return turn_mode_ == TURN_MODE::FORCE_UDP || turn_mode_ == TURN_MODE::FORCE_TCP;
}
bool ConfigCenter::IsEnableSrtp() const { return true; }
std::string ConfigCenter::GetSignalServerHost() const {
return signal_server_host_;
@@ -456,8 +402,6 @@ std::string ConfigCenter::GetSignalServerHost() const {
int ConfigCenter::GetSignalServerPort() const { return signal_server_port_; }
int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; }
std::string ConfigCenter::GetDefaultServerHost() const {
return signal_server_host_default_;
}
@@ -466,32 +410,30 @@ int ConfigCenter::GetDefaultSignalServerPort() const {
return server_port_default_;
}
int ConfigCenter::GetDefaultCoturnServerPort() const {
return coturn_server_port_default_;
}
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
bool ConfigCenter::IsEnablePrivacyScreen() const {
return enable_privacy_screen_.load();
}
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
return portable_service_prompt_suppressed_;
}
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
if (path.size() >= 512 || std::any_of(path.begin(), path.end(), [](unsigned char c) { return c < 32 || c == 127; })) return -1;
std::lock_guard<std::mutex> lock(file_path_mutex_);
if (StoreValues({{"file_transfer_save_path", path}}) != 0) return -1;
file_transfer_save_path_ = path;
ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
std::string ConfigCenter::GetFileTransferSavePath() const {
std::lock_guard<std::mutex> lock(file_path_mutex_);
return file_transfer_save_path_;
}
} // namespace crossdesk
+21 -27
View File
@@ -7,22 +7,20 @@
#ifndef _CONFIG_CENTER_H_
#define _CONFIG_CENTER_H_
#include <atomic>
#include <initializer_list>
#include <memory>
#include <mutex>
#include <utility>
#include <string>
#include "SimpleIni.h"
#include "screen_capturer/screen_capture_method.h"
namespace crossdesk {
class ConfigCenter {
public:
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
enum class VIDEO_ADAPTATION_POLICY {
FRAME_RATE_PRIORITY = 0,
QUALITY_PRIORITY = 1,
BALANCED = 2
};
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
enum class TURN_MODE {
DISABLED = 0,
@@ -37,43 +35,40 @@ class ConfigCenter {
// write config
int SetLanguage(LANGUAGE language);
int SetVideoQuality(VIDEO_QUALITY video_quality);
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
int SetVideoAdaptationPolicy(VIDEO_ADAPTATION_POLICY policy);
int SetScreenCaptureMethod(ScreenCaptureMethod method);
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
int SetHardwareVideoCodec(bool hardware_video_codec);
int SetTurnMode(TURN_MODE turn_mode);
int SetTurn(bool enable_turn);
int SetSrtp(bool enable_srtp);
int SetTurn(bool enable_turn, bool force_relay);
int SetServerHost(const std::string& signal_server_host);
int SetServerPort(int signal_server_port);
int SetCoturnServerPort(int coturn_server_port);
int SetSelfHosted(bool enable_self_hosted);
int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon);
int SetPrivacyScreen(bool enable_privacy_screen);
int SetPortableServicePromptSuppressed(bool suppressed);
int SetFileTransferSavePath(const std::string& path);
// read config
LANGUAGE GetLanguage() const;
VIDEO_QUALITY GetVideoQuality() const;
VIDEO_FRAME_RATE GetVideoFrameRate() const;
VIDEO_ADAPTATION_POLICY GetVideoAdaptationPolicy() const;
ScreenCaptureMethod GetScreenCaptureMethod() const;
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
// Whether this build includes a hardware video codec backend.
static bool IsHardwareVideoCodecAvailable();
bool IsHardwareVideoCodec() const;
TURN_MODE GetTurnMode() const;
bool IsEnableTurn() const;
bool IsForceRelay() const;
bool IsEnableSrtp() const;
std::string GetSignalServerHost() const;
int GetSignalServerPort() const;
int GetCoturnServerPort() const;
std::string GetDefaultServerHost() const;
int GetDefaultSignalServerPort() const;
int GetDefaultCoturnServerPort() const;
bool IsSelfHosted() const;
bool IsEnableAutostart() const;
bool IsEnableDaemon() const;
bool IsEnablePrivacyScreen() const;
bool IsPortableServicePromptSuppressed() const;
std::string GetFileTransferSavePath() const;
@@ -81,28 +76,27 @@ class ConfigCenter {
int Save();
private:
int CommitIni();
int StoreValues(std::initializer_list<std::pair<const char*, std::string>> values);
mutable std::mutex file_path_mutex_;
std::string config_path_;
CSimpleIniA ini_;
struct IniStorage;
std::unique_ptr<IniStorage> ini_;
const char* section_ = "Settings";
LANGUAGE language_ = LANGUAGE::CHINESE;
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
VIDEO_ADAPTATION_POLICY video_adaptation_policy_ =
VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY;
ScreenCaptureMethod screen_capture_method_ = ScreenCaptureMethod::Auto;
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
bool hardware_video_codec_ = false;
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
bool enable_srtp_ = false;
std::string signal_server_host_ = "";
std::string signal_server_host_default_ = "api.crossdesk.cn";
int signal_server_port_ = 0;
int server_port_default_ = 9099;
int coturn_server_port_ = 0;
int coturn_server_port_default_ = 3478;
bool enable_self_hosted_ = false;
bool enable_autostart_ = false;
bool enable_daemon_ = false;
std::atomic<bool> enable_privacy_screen_{true};
bool portable_service_prompt_suppressed_ = false;
std::string file_transfer_save_path_ = "";
};
@@ -42,6 +42,9 @@ class KeyboardCapturer : public DeviceController {
virtual int Hook(OnKeyAction on_key_action, void* user_ptr) = 0;
virtual int Unhook() = 0;
// Backends with an asynchronous capture loop override this so the UI can
// fall back to window events if that loop stops unexpectedly.
virtual bool IsHookActive() const { return true; }
virtual int SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code = 0,
bool extended = false) = 0;
@@ -1,82 +1,38 @@
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
#define CROSSDESK_GUI_APPLICATION_STATE_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _APPLICATION_STATE_H_
#define _APPLICATION_STATE_H_
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
namespace crossdesk::gui_detail {
struct MainWindowState {
float title_bar_width_ = 640;
float title_bar_height_ = 30;
float title_bar_button_width_ = 30;
float title_bar_button_height_ = 30;
bool exit_ = false;
const int sdl_refresh_ms_ = 16;
bool main_window_minimized_ = false;
int main_window_width_real_ = 720;
int main_window_height_real_ = 540;
float main_window_dpi_scaling_w_ = 1.0f;
float main_window_dpi_scaling_h_ = 1.0f;
float dpi_scale_ = 1.0f;
float main_window_width_default_ = 640;
float main_window_height_default_ = 480;
float main_window_width_ = 640;
float main_window_height_ = 480;
float main_window_width_last_ = 640;
float main_window_height_last_ = 480;
float local_window_width_ = 320;
float local_window_height_ = 235;
float remote_window_width_ = 320;
float remote_window_height_ = 235;
float local_child_window_width_ = 266;
float local_child_window_height_ = 180;
float remote_child_window_width_ = 266;
float remote_child_window_height_ = 180;
float main_window_text_y_padding_ = 10;
float main_child_window_x_padding_ = 27;
float main_child_window_y_padding_ = 45;
float status_bar_height_ = 22;
float connection_status_window_width_ = 200;
float connection_status_window_height_ = 150;
float notification_window_width_ = 200;
float notification_window_height_ = 80;
float about_window_width_ = 300;
float about_window_height_ = 170;
float update_notification_window_width_ = 400;
float update_notification_window_height_ = 320;
};
// Application-global interaction flags that coordinate SDL events with media
// Application-global interaction flags that coordinate UI events with media
// and remote-control devices.
struct InteractionState {
bool start_mouse_controller_ = false;
bool mouse_controller_is_started_ = false;
bool start_screen_capturer_ = false;
bool screen_capturer_is_started_ = false;
bool start_speaker_capturer_ = false;
bool speaker_capturer_is_started_ = false;
bool start_keyboard_capturer_ = false;
bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false;
bool keyboard_capturer_uses_window_events_ = false;
bool foucs_on_main_window_ = false;
bool focus_on_stream_window_ = false;
bool audio_capture_ = false;
int screen_width_ = 1280;
int screen_height_ = 720;
int selected_display_ = 0;
std::string connect_button_label_ = "Connect";
char input_password_tmp_[7] = "";
char input_password_[7] = "";
std::string random_password_;
char new_password_[7] = "";
char remote_id_display_[12] = "";
unsigned char audio_buffer_[720]{};
int audio_len_ = 0;
@@ -84,39 +40,24 @@ struct InteractionState {
bool need_to_rejoin_ = false;
std::chrono::steady_clock::time_point last_rejoin_check_time_ =
std::chrono::steady_clock::now();
bool just_created_ = false;
std::string controlled_remote_id_;
std::string focused_remote_id_;
std::string remote_client_id_;
};
struct UpdateState {
nlohmann::json latest_version_info_ = nlohmann::json{};
bool update_available_ = false;
std::string latest_version_;
std::string release_name_;
std::string release_notes_;
std::string release_date_;
bool show_new_version_icon_ = false;
bool show_new_version_icon_in_menu_ = true;
double new_version_icon_last_trigger_time_ = 0.0;
double new_version_icon_render_start_time_ = 0.0;
};
struct StreamWindowState {
bool need_to_create_stream_window_ = false;
bool stream_window_created_ = false;
bool stream_window_inited_ = false;
bool window_maximized_ = false;
bool control_mouse_ = false;
int stream_window_width_default_ = 1280;
int stream_window_height_default_ = 720;
float stream_window_width_ = 1280;
float stream_window_height_ = 720;
int stream_window_width_real_ = 1280;
int stream_window_height_real_ = 720;
float stream_window_dpi_scaling_w_ = 1.0f;
float stream_window_dpi_scaling_h_ = 1.0f;
};
struct ServerWindowState {
@@ -127,60 +68,22 @@ struct ServerWindowState {
std::atomic<bool> need_to_destroy_server_window_{false};
bool server_window_created_ = false;
bool server_window_inited_ = false;
int server_window_width_default_ = 250;
int server_window_height_default_ = 150;
float server_window_width_ = 250;
float server_window_height_ = 150;
float server_window_title_bar_height_ = 30.0f;
int server_window_normal_width_ = 250;
int server_window_normal_height_ = 150;
float server_window_dpi_scaling_w_ = 1.0f;
float server_window_dpi_scaling_h_ = 1.0f;
float window_rounding_ = 6.0f;
float window_rounding_default_ = 6.0f;
bool server_window_collapsed_ = false;
bool server_window_collapsed_dragging_ = false;
float server_window_collapsed_drag_start_mouse_x_ = 0.0f;
float server_window_collapsed_drag_start_mouse_y_ = 0.0f;
int server_window_collapsed_drag_start_win_x_ = 0;
int server_window_collapsed_drag_start_win_y_ = 0;
bool server_window_dragging_ = false;
float server_window_drag_start_mouse_x_ = 0.0f;
float server_window_drag_start_mouse_y_ = 0.0f;
int server_window_drag_start_win_x_ = 0;
int server_window_drag_start_win_y_ = 0;
};
struct UiState {
bool label_inited_ = false;
bool connect_button_pressed_ = false;
bool password_validating_ = false;
uint32_t password_validating_time_ = 0;
bool show_settings_window_ = false;
bool show_self_hosted_server_config_window_ = false;
bool rejoin_ = false;
bool local_id_copied_ = false;
bool show_password_ = true;
bool show_about_window_ = false;
bool show_connection_status_window_ = false;
bool show_reset_password_window_ = false;
bool show_update_notification_window_ = false;
bool fullscreen_button_pressed_ = false;
bool focus_on_input_widget_ = true;
bool is_client_mode_ = false;
bool is_server_mode_ = false;
bool reload_recent_connections_ = true;
bool show_confirm_delete_connection_ = false;
bool show_edit_connection_alias_window_ = false;
bool show_offline_warning_window_ = false;
bool delete_connection_ = false;
bool is_tab_bar_hovered_ = false;
std::string delete_connection_name_;
std::string edit_connection_alias_remote_id_;
char edit_connection_alias_[128] = "";
std::string offline_warning_text_;
bool re_enter_remote_id_ = false;
double copy_start_time_ = 0;
};
struct ApplicationState : MainWindowState,
@@ -192,4 +95,4 @@ struct ApplicationState : MainWindowState,
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
#endif
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,9 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _GUI_APPLICATION_H_
#define _GUI_APPLICATION_H_
@@ -13,6 +19,8 @@
namespace crossdesk {
class UpdateChecker;
// Slint application shell. GuiRuntime remains the owner of transport, media,
// device, clipboard, transfer and settings behavior.
class GuiApplication final : private GuiRuntime {
@@ -42,7 +50,9 @@ private:
void BindStreamCallbacks();
void BindServerCallbacks();
void Tick();
void PollUpdateCheck();
void ShareLocalCursorState();
bool RequestPasswordChange(const std::string &password);
void HandlePasswordChangeResult();
void HandleCredentialRecovery();
void SyncMainWindow();
@@ -61,6 +71,7 @@ private:
void UpdateLocalization();
void ResetSettingsUi();
void SaveSettingsFromUi();
void ApplySettingsFromConfig(bool reconnect);
#if _WIN32 && CROSSDESK_PORTABLE
void CheckPortableWindowsService();
void StartPortableWindowsServiceInstall();
@@ -81,6 +92,7 @@ private:
bool OpenUrl(const std::string &url);
std::unique_ptr<SlintUi> ui_;
std::unique_ptr<UpdateChecker> update_checker_;
CursorStateProvider cursor_state_provider_;
std::unordered_map<std::string, CursorDeliveryState>
cursor_delivery_states_;
@@ -1,323 +0,0 @@
#include "application/gui_application.h"
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "rd_log.h"
namespace crossdesk {
void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
if (main_ctx_) {
ImGui::SetCurrentContext(main_ctx_);
ImGui_ImplSDL3_ProcessEvent(&event);
} else {
LOG_ERROR("Main context is null");
return;
}
if (stream_window_inited_) {
if (stream_ctx_) {
ImGui::SetCurrentContext(stream_ctx_);
ImGui_ImplSDL3_ProcessEvent(&event);
} else {
LOG_ERROR("Stream context is null");
return;
}
}
if (server_window_inited_) {
if (server_ctx_) {
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDL3_ProcessEvent(&event);
} else {
LOG_ERROR("Server context is null");
return;
}
}
if (APP_EXIT_EVENT != 0 && event.type == APP_EXIT_EVENT) {
LOG_INFO("Quit program from system tray");
if (stream_window_) {
SDL_SetWindowMouseGrab(stream_window_, false);
}
#if defined(__linux__) && !defined(__APPLE__)
if (tray_) {
tray_->RemoveTrayIcon();
}
#endif
exit_ = true;
return;
}
if (clipboard_.event_type() != 0 && event.type == clipboard_.event_type()) {
clipboard_.ApplyPendingRemoteText();
return;
}
switch (event.type) {
case SDL_EVENT_QUIT:
if (stream_window_inited_) {
LOG_INFO("Destroy stream window");
SDL_SetWindowMouseGrab(stream_window_, false);
DestroyStreamWindow();
DestroyStreamWindowContext();
{
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &[host_name, props] : remote_sessions_) {
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
int video_width = 0;
int video_height = 0;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
frame_snapshot = props->front_frame_;
video_width = props->video_width_;
video_height = props->video_height_;
}
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
video_height > 0) {
thumbnail_->SaveToThumbnail(
(char *)frame_snapshot->data(), video_width, video_height,
host_name, props->remote_host_name_,
props->remember_password_ ? props->remote_password_ : "");
}
if (props->peer_) {
std::string client_id = (host_name == client_id_)
? "C-" + std::string(client_id_)
: client_id_;
LOG_INFO("[{}] Leave connection [{}]", client_id, host_name);
LeaveConnection(props->peer_, host_name.c_str());
LOG_INFO("Destroy peer [{}]", client_id);
DestroyPeer(&props->peer_);
}
props->streaming_ = false;
props->remember_password_ = false;
props->connection_established_ = false;
props->audio_capture_button_pressed_ = false;
memset(&props->net_traffic_stats_, 0,
sizeof(props->net_traffic_stats_));
SDL_SetWindowFullscreen(main_window_, false);
SDL_FlushEvents(STREAM_REFRESH_EVENT, STREAM_REFRESH_EVENT);
memset(audio_buffer_, 0, 720);
}
}
{
// std::unique_lock lock(remote_sessions_mutex_);
remote_sessions_.clear();
}
rejoin_ = false;
is_client_mode_ = false;
reload_recent_connections_ = true;
fullscreen_button_pressed_ = false;
start_keyboard_capturer_ = false;
just_created_ = false;
recent_connection_image_save_time_ = SDL_GetTicks();
} else {
LOG_INFO("Quit program");
exit_ = true;
}
break;
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
if (stream_window_ &&
event.window.windowID == SDL_GetWindowID(stream_window_)) {
break;
}
if (main_window_ &&
event.window.windowID == SDL_GetWindowID(main_window_)) {
if (!MinimizeMainWindowToTray()) {
SDL_HideWindow(main_window_);
}
break;
}
exit_ = true;
break;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
if (stream_window_created_ &&
event.window.windowID == SDL_GetWindowID(stream_window_)) {
UpdateRenderRect();
}
break;
case SDL_EVENT_WINDOW_FOCUS_GAINED:
if (stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) {
focus_on_stream_window_ = true;
} else if (main_window_ &&
SDL_GetWindowID(main_window_) == event.window.windowID) {
foucs_on_main_window_ = true;
}
break;
case SDL_EVENT_WINDOW_FOCUS_LOST:
if (stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) {
keyboard_.ForceReleasePressedKeys();
focus_on_stream_window_ = false;
} else if (main_window_ &&
SDL_GetWindowID(main_window_) == event.window.windowID) {
foucs_on_main_window_ = false;
}
break;
case SDL_EVENT_DROP_FILE:
transfers_.HandleDropEvent(event);
break;
case SDL_EVENT_CLIPBOARD_UPDATE:
clipboard_.HandleLocalUpdate();
break;
case SDL_EVENT_MOUSE_MOTION:
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
case SDL_EVENT_MOUSE_WHEEL: {
Uint32 mouse_window_id = 0;
if (event.type == SDL_EVENT_MOUSE_MOTION) {
mouse_window_id = event.motion.windowID;
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
mouse_window_id = event.button.windowID;
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
mouse_window_id = event.wheel.windowID;
}
if (focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == mouse_window_id) {
ProcessMouseEvent(event);
}
break;
}
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
if (keyboard_capturer_is_started_ &&
keyboard_capturer_uses_window_events_ &&
focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == event.key.windowID) {
ProcessKeyboardEvent(event);
}
break;
default:
if (event.type == STREAM_REFRESH_EVENT) {
auto *props = static_cast<RemoteSession *>(event.user.data1);
if (!props) {
break;
}
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
int video_width = 0;
int video_height = 0;
bool render_rect_dirty = false;
bool cleanup_pending = false;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
cleanup_pending = props->stream_cleanup_pending_;
if (!cleanup_pending) {
frame_snapshot = props->front_frame_;
video_width = props->video_width_;
video_height = props->video_height_;
}
render_rect_dirty = props->render_rect_dirty_;
}
if (cleanup_pending) {
if (props->stream_texture_) {
SDL_DestroyTexture(props->stream_texture_);
props->stream_texture_ = nullptr;
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->stream_cleanup_pending_ = false;
}
if (render_rect_dirty) {
UpdateRenderRect();
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->render_rect_dirty_ = false;
}
break;
}
if (video_width <= 0 || video_height <= 0) {
break;
}
if (!frame_snapshot || frame_snapshot->empty()) {
break;
}
if (props->stream_texture_) {
if (video_width != props->texture_width_ ||
video_height != props->texture_height_) {
props->texture_width_ = video_width;
props->texture_height_ = video_height;
SDL_DestroyTexture(props->stream_texture_);
// props->stream_texture_ = SDL_CreateTexture(
// stream_renderer_, stream_pixformat_,
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
// props->texture_height_);
SDL_PropertiesID nvProps = SDL_CreateProperties();
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
props->texture_width_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
props->texture_height_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
SDL_PIXELFORMAT_NV12);
SDL_SetNumberProperty(nvProps,
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
SDL_COLORSPACE_BT601_LIMITED);
props->stream_texture_ =
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
SDL_DestroyProperties(nvProps);
}
} else {
props->texture_width_ = video_width;
props->texture_height_ = video_height;
// props->stream_texture_ = SDL_CreateTexture(
// stream_renderer_, stream_pixformat_,
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
// props->texture_height_);
SDL_PropertiesID nvProps = SDL_CreateProperties();
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
props->texture_width_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
props->texture_height_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
SDL_PIXELFORMAT_NV12);
SDL_SetNumberProperty(nvProps,
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
SDL_COLORSPACE_BT601_LIMITED);
props->stream_texture_ =
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
SDL_DestroyProperties(nvProps);
}
SDL_UpdateTexture(props->stream_texture_, NULL, frame_snapshot->data(),
props->texture_width_);
if (render_rect_dirty) {
UpdateRenderRect();
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->render_rect_dirty_ = false;
}
}
break;
}
}
} // namespace crossdesk
@@ -1,424 +0,0 @@
#include "application/gui_application.h"
#include <remote_action.h>
#include <algorithm>
#include <cmath>
#include <string>
#include "device_controller.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent &event) {
const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0;
switch (event.scancode) {
case SDL_SCANCODE_NUMLOCKCLEAR:
return 0x90;
case SDL_SCANCODE_KP_ENTER:
return 0x0D;
case SDL_SCANCODE_KP_0:
if (!numlock_enabled) {
return 0x2D;
}
return 0x60;
case SDL_SCANCODE_KP_1:
if (!numlock_enabled) {
return 0x23;
}
return 0x61;
case SDL_SCANCODE_KP_2:
if (!numlock_enabled) {
return 0x28;
}
return 0x62;
case SDL_SCANCODE_KP_3:
if (!numlock_enabled) {
return 0x22;
}
return 0x63;
case SDL_SCANCODE_KP_4:
if (!numlock_enabled) {
return 0x25;
}
return 0x64;
case SDL_SCANCODE_KP_5:
return 0x65;
case SDL_SCANCODE_KP_6:
if (!numlock_enabled) {
return 0x27;
}
return 0x66;
case SDL_SCANCODE_KP_7:
if (!numlock_enabled) {
return 0x24;
}
return 0x67;
case SDL_SCANCODE_KP_8:
if (!numlock_enabled) {
return 0x26;
}
return 0x68;
case SDL_SCANCODE_KP_9:
if (!numlock_enabled) {
return 0x21;
}
return 0x69;
case SDL_SCANCODE_KP_PERIOD:
case SDL_SCANCODE_KP_COMMA:
if (!numlock_enabled) {
return 0x2E;
}
return 0x6E;
case SDL_SCANCODE_KP_DIVIDE:
return 0x6F;
case SDL_SCANCODE_KP_MULTIPLY:
return 0x6A;
case SDL_SCANCODE_KP_MINUS:
return 0x6D;
case SDL_SCANCODE_KP_PLUS:
return 0x6B;
case SDL_SCANCODE_KP_EQUALS:
return 0xBB;
default:
return -1;
}
}
int TranslateSdlKeyboardEventToVk(const SDL_KeyboardEvent &event) {
const int keypad_key_code = TranslateSdlKeypadScancodeToVk(event);
if (keypad_key_code >= 0) {
return keypad_key_code;
}
const int key = static_cast<int>(event.key);
if (key >= 'a' && key <= 'z') {
return key - 'a' + 0x41;
}
if (key >= 'A' && key <= 'Z') {
return key;
}
if (key >= '0' && key <= '9') {
return key;
}
switch (key) {
case ';':
return 0xBA;
case '\'':
return 0xDE;
case '`':
return 0xC0;
case ',':
return 0xBC;
case '.':
return 0xBE;
case '/':
return 0xBF;
case '\\':
return 0xDC;
case '[':
return 0xDB;
case ']':
return 0xDD;
case '-':
return 0xBD;
case '=':
return 0xBB;
default:
break;
}
switch (event.scancode) {
case SDL_SCANCODE_ESCAPE:
return 0x1B;
case SDL_SCANCODE_RETURN:
return 0x0D;
case SDL_SCANCODE_SPACE:
return 0x20;
case SDL_SCANCODE_BACKSPACE:
return 0x08;
case SDL_SCANCODE_TAB:
return 0x09;
case SDL_SCANCODE_PRINTSCREEN:
return 0x2C;
case SDL_SCANCODE_SCROLLLOCK:
return 0x91;
case SDL_SCANCODE_PAUSE:
return 0x13;
case SDL_SCANCODE_INSERT:
return 0x2D;
case SDL_SCANCODE_DELETE:
return 0x2E;
case SDL_SCANCODE_HOME:
return 0x24;
case SDL_SCANCODE_END:
return 0x23;
case SDL_SCANCODE_PAGEUP:
return 0x21;
case SDL_SCANCODE_PAGEDOWN:
return 0x22;
case SDL_SCANCODE_LEFT:
return 0x25;
case SDL_SCANCODE_RIGHT:
return 0x27;
case SDL_SCANCODE_UP:
return 0x26;
case SDL_SCANCODE_DOWN:
return 0x28;
case SDL_SCANCODE_F1:
return 0x70;
case SDL_SCANCODE_F2:
return 0x71;
case SDL_SCANCODE_F3:
return 0x72;
case SDL_SCANCODE_F4:
return 0x73;
case SDL_SCANCODE_F5:
return 0x74;
case SDL_SCANCODE_F6:
return 0x75;
case SDL_SCANCODE_F7:
return 0x76;
case SDL_SCANCODE_F8:
return 0x77;
case SDL_SCANCODE_F9:
return 0x78;
case SDL_SCANCODE_F10:
return 0x79;
case SDL_SCANCODE_F11:
return 0x7A;
case SDL_SCANCODE_F12:
return 0x7B;
case SDL_SCANCODE_CAPSLOCK:
return 0x14;
case SDL_SCANCODE_LSHIFT:
return 0xA0;
case SDL_SCANCODE_RSHIFT:
return 0xA1;
case SDL_SCANCODE_LCTRL:
return 0xA2;
case SDL_SCANCODE_RCTRL:
return 0xA3;
case SDL_SCANCODE_LALT:
return 0xA4;
case SDL_SCANCODE_RALT:
return 0xA5;
case SDL_SCANCODE_LGUI:
return 0x5B;
case SDL_SCANCODE_RGUI:
return 0x5C;
default:
return -1;
}
}
} // namespace
int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
if (event.type != SDL_EVENT_KEY_DOWN && event.type != SDL_EVENT_KEY_UP) {
return -1;
}
// SDL represents key auto-repeat as additional key-down events. Forward
// them so the fallback path has the same long-press behavior as native hooks.
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
if (key_code < 0) {
return 0;
}
return keyboard_.SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN);
}
int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
controlled_remote_id_ = "";
RemoteAction remote_action{};
if ((event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) &&
event.button.button != SDL_BUTTON_LEFT &&
event.button.button != SDL_BUTTON_RIGHT &&
event.button.button != SDL_BUTTON_MIDDLE) {
return 0;
}
float cursor_x = last_mouse_event.motion.x;
float cursor_y = last_mouse_event.motion.y;
auto normalize_cursor_to_window_space = [&](float *x, float *y) {
if (!x || !y || !stream_window_) {
return;
}
int window_width = 0;
int window_height = 0;
int pixel_width = 0;
int pixel_height = 0;
SDL_GetWindowSize(stream_window_, &window_width, &window_height);
SDL_GetWindowSizeInPixels(stream_window_, &pixel_width, &pixel_height);
if (window_width <= 0 || window_height <= 0 || pixel_width <= 0 ||
pixel_height <= 0) {
return;
}
if ((window_width != pixel_width || window_height != pixel_height) &&
(*x > static_cast<float>(window_width) + 1.0f ||
*y > static_cast<float>(window_height) + 1.0f)) {
const float scale_x =
static_cast<float>(window_width) / static_cast<float>(pixel_width);
const float scale_y =
static_cast<float>(window_height) / static_cast<float>(pixel_height);
*x *= scale_x;
*y *= scale_y;
static bool logged_pixel_to_window_conversion = false;
if (!logged_pixel_to_window_conversion) {
LOG_INFO(
"Mouse coordinate space converted from pixels to window units: "
"window={}x{}, pixels={}x{}, scale=({:.4f},{:.4f})",
window_width, window_height, pixel_width, pixel_height, scale_x,
scale_y);
logged_pixel_to_window_conversion = true;
}
}
};
if (event.type == SDL_EVENT_MOUSE_MOTION) {
cursor_x = event.motion.x;
cursor_y = event.motion.y;
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
cursor_x = event.button.x;
cursor_y = event.button.y;
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
cursor_x = last_mouse_event.motion.x;
cursor_y = last_mouse_event.motion.y;
}
const bool is_pointer_position_event =
(event.type == SDL_EVENT_MOUSE_MOTION ||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP);
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &it : remote_sessions_) {
auto props = it.second;
if (!props->control_mouse_) {
continue;
}
const bool file_transfer_window_hovered =
props->file_transfer_.file_transfer_window_hovered_;
const bool overlay_hovered =
props->control_bar_hovered_ || props->display_selectable_hovered_ ||
props->shortcut_selectable_hovered_ || file_transfer_window_hovered;
const SDL_FRect render_rect = props->stream_render_rect_f_;
if (render_rect.w <= 1.0f || render_rect.h <= 1.0f) {
continue;
}
if (is_pointer_position_event && cursor_x >= render_rect.x &&
cursor_x <= render_rect.x + render_rect.w &&
cursor_y >= render_rect.y &&
cursor_y <= render_rect.y + render_rect.h) {
controlled_remote_id_ = it.first;
last_mouse_event.motion.x = cursor_x;
last_mouse_event.motion.y = cursor_y;
last_mouse_event.button.x = cursor_x;
last_mouse_event.button.y = cursor_y;
remote_action.m.x = (cursor_x - render_rect.x) / render_rect.w;
remote_action.m.y = (cursor_y - render_rect.y) / render_rect.h;
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) {
remote_action.type = ControlType::mouse;
if (SDL_BUTTON_LEFT == event.button.button) {
remote_action.m.flag = MouseFlag::left_down;
} else if (SDL_BUTTON_RIGHT == event.button.button) {
remote_action.m.flag = MouseFlag::right_down;
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
remote_action.m.flag = MouseFlag::middle_down;
}
} else if (SDL_EVENT_MOUSE_BUTTON_UP == event.type) {
remote_action.type = ControlType::mouse;
if (SDL_BUTTON_LEFT == event.button.button) {
remote_action.m.flag = MouseFlag::left_up;
} else if (SDL_BUTTON_RIGHT == event.button.button) {
remote_action.m.flag = MouseFlag::right_up;
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
remote_action.m.flag = MouseFlag::middle_up;
}
} else if (SDL_EVENT_MOUSE_MOTION == event.type) {
remote_action.type = ControlType::mouse;
remote_action.m.flag = MouseFlag::move;
}
if (overlay_hovered) {
break;
}
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
}
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
last_mouse_event.button.x >= render_rect.x &&
last_mouse_event.button.x <= render_rect.x + render_rect.w &&
last_mouse_event.button.y >= render_rect.y &&
last_mouse_event.button.y <= render_rect.y + render_rect.h) {
float scroll_x = event.wheel.x;
float scroll_y = event.wheel.y;
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
scroll_x = -scroll_x;
scroll_y = -scroll_y;
}
remote_action.type = ControlType::mouse;
auto roundUp = [](float value) -> int {
if (value > 0) {
return static_cast<int>(std::ceil(value));
} else if (value < 0) {
return static_cast<int>(std::floor(value));
}
return 0;
};
if (std::abs(scroll_y) >= std::abs(scroll_x)) {
remote_action.m.flag = MouseFlag::wheel_vertical;
remote_action.m.s = roundUp(scroll_y);
} else {
remote_action.m.flag = MouseFlag::wheel_horizontal;
remote_action.m.s = roundUp(scroll_x);
}
remote_action.m.x = (last_mouse_event.button.x - render_rect.x) /
(std::max)(render_rect.w, 1.0f);
remote_action.m.y = (last_mouse_event.button.y - render_rect.y) /
(std::max)(render_rect.h, 1.0f);
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
if (overlay_hovered) {
continue;
}
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
}
}
}
return 0;
}
} // namespace crossdesk
@@ -1,4 +1,11 @@
#pragma once
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SERVER_WINDOW_STATE_H_
#define _SERVER_WINDOW_STATE_H_
#include <algorithm>
#include <iterator>
@@ -24,5 +31,6 @@ inline int ReconcileSelectedController(const std::vector<std::string>& ids,
}
return static_cast<int>(std::distance(ids.begin(), selected));
}
} // namespace crossdesk::server_window_state
} // namespace crossdesk::server_window_state
#endif
@@ -1,4 +1,11 @@
#pragma once
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOW_GEOMETRY_H_
#define _WINDOW_GEOMETRY_H_
#include <algorithm>
@@ -38,3 +45,5 @@ constexpr PhysicalPosition BottomRightPosition(const PhysicalRect& bounds,
}
} // namespace crossdesk::window_geometry
#endif
@@ -1,687 +0,0 @@
#include "application/gui_application.h"
#include <libyuv.h>
#if defined(__linux__) && !defined(__APPLE__)
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#endif
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include "fa_regular_400.h"
#include "fa_solid_900.h"
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "version_checker.h"
#if defined(__APPLE__)
#include "window_util_mac.h"
#endif
namespace crossdesk {
namespace {
const ImWchar *GetMultilingualGlyphRanges() {
static std::vector<ImWchar> glyph_ranges;
if (glyph_ranges.empty()) {
ImGuiIO &io = ImGui::GetIO();
ImFontGlyphRangesBuilder builder;
builder.AddRanges(io.Fonts->GetGlyphRangesDefault());
builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull());
builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic());
ImVector<ImWchar> built_ranges;
builder.BuildRanges(&built_ranges);
glyph_ranges.assign(built_ranges.Data,
built_ranges.Data + built_ranges.Size);
}
return glyph_ranges.empty() ? nullptr : glyph_ranges.data();
}
bool CanReadFontFile(const char *font_path) {
if (!font_path) {
return false;
}
std::ifstream font_file(font_path, std::ios::binary);
return font_file.good();
}
#if _WIN32
HICON LoadTrayIcon() {
HMODULE module = GetModuleHandleW(nullptr);
HICON icon = reinterpret_cast<HICON>(
LoadImageW(module, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
if (icon) {
return icon;
}
return LoadIconW(nullptr, IDI_APPLICATION);
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
inline bool X11GetDisplayAndWindow(SDL_Window *window, Display **display_out,
::Window *x11_window_out) {
if (!window || !display_out || !x11_window_out) {
return false;
}
#if !defined(SDL_PROP_WINDOW_X11_DISPLAY_POINTER) || \
!defined(SDL_PROP_WINDOW_X11_WINDOW_NUMBER)
// SDL build does not expose X11 window properties.
return false;
#else
SDL_PropertiesID props = SDL_GetWindowProperties(window);
Display *display = (Display *)SDL_GetPointerProperty(
props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
const Sint64 x11_window_num =
SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
const ::Window x11_window = (::Window)x11_window_num;
if (!display || !x11_window) {
return false;
}
*display_out = display;
*x11_window_out = x11_window;
return true;
#endif
}
inline void X11SendNetWmState(Display *display, ::Window x11_window,
long action, Atom state1, Atom state2 = 0) {
if (!display || !x11_window) {
return;
}
const Atom wm_state = XInternAtom(display, "_NET_WM_STATE", False);
XEvent event;
memset(&event, 0, sizeof(event));
event.xclient.type = ClientMessage;
event.xclient.serial = 0;
event.xclient.send_event = True;
event.xclient.message_type = wm_state;
event.xclient.window = x11_window;
event.xclient.format = 32;
event.xclient.data.l[0] = action;
event.xclient.data.l[1] = (long)state1;
event.xclient.data.l[2] = (long)state2;
event.xclient.data.l[3] = 1; // normal source indication
event.xclient.data.l[4] = 0;
XSendEvent(display, DefaultRootWindow(display), False,
SubstructureRedirectMask | SubstructureNotifyMask, &event);
}
inline void X11SetWindowTypeUtility(Display *display, ::Window x11_window) {
if (!display || !x11_window) {
return;
}
const Atom wm_window_type =
XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
const Atom wm_window_type_utility =
XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
XChangeProperty(display, x11_window, wm_window_type, XA_ATOM, 32,
PropModeReplace, (unsigned char *)&wm_window_type_utility, 1);
}
inline void X11SetWindowAlwaysOnTop(SDL_Window *window) {
Display *display = nullptr;
::Window x11_window = 0;
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
return;
}
const Atom state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False);
const Atom state_stays_on_top =
XInternAtom(display, "_NET_WM_STATE_STAYS_ON_TOP", False);
// Request _NET_WM_STATE_ADD for ABOVE + STAYS_ON_TOP.
X11SendNetWmState(display, x11_window, 1, state_above, state_stays_on_top);
XFlush(display);
}
inline void X11SetWindowSkipTaskbar(SDL_Window *window) {
Display *display = nullptr;
::Window x11_window = 0;
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
return;
}
const Atom skip_taskbar =
XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", False);
const Atom skip_pager =
XInternAtom(display, "_NET_WM_STATE_SKIP_PAGER", False);
// Request _NET_WM_STATE_ADD for SKIP_TASKBAR + SKIP_PAGER.
X11SendNetWmState(display, x11_window, 1, skip_taskbar, skip_pager);
// Hint the WM that this is an auxiliary/utility window.
X11SetWindowTypeUtility(display, x11_window);
XFlush(display);
}
#endif
} // namespace
SDL_HitTestResult GuiApplication::HitTestCallback(SDL_Window *window,
const SDL_Point *area,
void *data) {
GuiApplication *application = (GuiApplication *)data;
if (!application) {
return SDL_HITTEST_NORMAL;
}
if (application->fullscreen_button_pressed_) {
return SDL_HITTEST_NORMAL;
}
// Server window: OS-level dragging for the title bar, but keep the left-side
// collapse/expand button clickable.
if (application->server_window_ && window == application->server_window_) {
const float title_h = application->server_window_title_bar_height_;
const float button_w = title_h;
if (area->y >= 0 && area->y < title_h) {
if (area->x >= 0 && area->x < button_w) {
return SDL_HITTEST_NORMAL;
}
return SDL_HITTEST_DRAGGABLE;
}
return SDL_HITTEST_NORMAL;
}
int window_width, window_height;
SDL_GetWindowSize(window, &window_width, &window_height);
// check if curosor is in tab bar
if (application->stream_window_inited_ && application->stream_window_created_ &&
!application->fullscreen_button_pressed_ && application->stream_ctx_) {
ImGuiContext *prev_ctx = ImGui::GetCurrentContext();
ImGui::SetCurrentContext(application->stream_ctx_);
ImGuiWindow *tab_bar_window = ImGui::FindWindowByName("TabBar");
if (tab_bar_window && tab_bar_window->Active) {
ImGuiIO &io = ImGui::GetIO();
float scale_x = io.DisplayFramebufferScale.x;
float scale_y = io.DisplayFramebufferScale.y;
float tab_bar_x = tab_bar_window->Pos.x * scale_x;
float tab_bar_y = tab_bar_window->Pos.y * scale_y;
float tab_bar_width = tab_bar_window->Size.x * scale_x;
float tab_bar_height = tab_bar_window->Size.y * scale_y;
ImGui::SetCurrentContext(prev_ctx);
if (area->x >= tab_bar_x && area->x <= tab_bar_x + tab_bar_width &&
area->y >= tab_bar_y && area->y <= tab_bar_y + tab_bar_height) {
return SDL_HITTEST_NORMAL;
}
} else {
ImGui::SetCurrentContext(prev_ctx);
}
}
float mouse_grab_padding = application->title_bar_button_width_ * 0.16f;
if (area->y < application->title_bar_button_width_ &&
area->y > mouse_grab_padding &&
area->x < window_width - application->title_bar_button_width_ * 3.0f &&
area->x > mouse_grab_padding) {
return SDL_HITTEST_DRAGGABLE;
}
// if (!application->streaming_) {
// return SDL_HITTEST_NORMAL;
// }
if (area->y < mouse_grab_padding) {
if (area->x < mouse_grab_padding) {
return SDL_HITTEST_RESIZE_TOPLEFT;
} else if (area->x > window_width - mouse_grab_padding) {
return SDL_HITTEST_RESIZE_TOPRIGHT;
} else {
return SDL_HITTEST_RESIZE_TOP;
}
} else if (area->y > window_height - mouse_grab_padding) {
if (area->x < mouse_grab_padding) {
return SDL_HITTEST_RESIZE_BOTTOMLEFT;
} else if (area->x > window_width - mouse_grab_padding) {
return SDL_HITTEST_RESIZE_BOTTOMRIGHT;
} else {
return SDL_HITTEST_RESIZE_BOTTOM;
}
} else if (area->x < mouse_grab_padding) {
return SDL_HITTEST_RESIZE_LEFT;
} else if (area->x > window_width - mouse_grab_padding) {
return SDL_HITTEST_RESIZE_RIGHT;
}
return SDL_HITTEST_NORMAL;
}
int GuiApplication::CreateMainWindow() {
main_ctx_ = ImGui::CreateContext();
if (!main_ctx_) {
LOG_ERROR("Main context is null");
return -1;
}
ImGui::SetCurrentContext(main_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Main Window", (int)main_window_width_,
(int)main_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_HIDDEN | SDL_WINDOW_TRANSPARENT,
&main_window_, &main_renderer_)) {
LOG_ERROR("Error creating MainWindow and MainRenderer: {}", SDL_GetError());
return -1;
}
float dpi_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
if (std::abs(dpi_scale_ - dpi_scale) > 0.01f) {
dpi_scale_ = dpi_scale;
main_window_width_ = (int)(main_window_width_default_ * dpi_scale_);
main_window_height_ = (int)(main_window_height_default_ * dpi_scale_);
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
server_window_width_ = (int)(server_window_width_default_ * dpi_scale_);
server_window_height_ = (int)(server_window_height_default_ * dpi_scale_);
server_window_normal_width_ =
(int)(server_window_width_default_ * dpi_scale_);
server_window_normal_height_ =
(int)(server_window_height_default_ * dpi_scale_);
window_rounding_ = window_rounding_default_ * dpi_scale_;
SDL_SetWindowSize(main_window_, (int)main_window_width_,
(int)main_window_height_);
}
SDL_SetWindowResizable(main_window_, false);
// for window region action
SDL_SetWindowHitTest(main_window_, HitTestCallback, this);
SDL_SetRenderDrawBlendMode(main_renderer_, SDL_BLENDMODE_BLEND);
SetupFontAndStyle(&main_windows_system_chinese_font_);
ImGuiStyle &style = ImGui::GetStyle();
style.ScaleAllSizes(dpi_scale_);
style.FontScaleDpi = dpi_scale_;
#if _WIN32
SDL_PropertiesID props = SDL_GetWindowProperties(main_window_);
HWND main_hwnd = (HWND)SDL_GetPointerProperty(
props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
HICON tray_icon = LoadTrayIcon();
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
localization_language_index_);
#elif defined(__APPLE__)
tray_ = std::make_unique<MacTray>(main_window_, "CrossDesk",
localization_language_index_);
#elif defined(__linux__) && !defined(__APPLE__)
tray_ = std::make_unique<LinuxTray>(
main_window_, "CrossDesk", localization_language_index_, APP_EXIT_EVENT);
#endif
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
ImGui_ImplSDLRenderer3_Init(main_renderer_);
return 0;
}
int GuiApplication::DestroyMainWindow() {
if (main_ctx_) {
ImGui::SetCurrentContext(main_ctx_);
}
if (main_renderer_) {
SDL_DestroyRenderer(main_renderer_);
}
if (main_window_) {
SDL_DestroyWindow(main_window_);
}
return 0;
}
int GuiApplication::CreateStreamWindow() {
if (stream_window_created_) {
return 0;
}
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
stream_ctx_ = ImGui::CreateContext();
if (!stream_ctx_) {
LOG_ERROR("Stream context is null");
return -1;
}
ImGui::SetCurrentContext(stream_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Stream Window", (int)stream_window_width_,
(int)stream_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
&stream_window_, &stream_renderer_)) {
LOG_ERROR("Error creating stream_window_ and stream_renderer_: {}",
SDL_GetError());
return -1;
}
stream_pixformat_ = SDL_PIXELFORMAT_NV12;
SDL_SetWindowResizable(stream_window_, true);
// for window region action
SDL_SetWindowHitTest(stream_window_, HitTestCallback, this);
SDL_SetRenderDrawBlendMode(stream_renderer_, SDL_BLENDMODE_BLEND);
SetupFontAndStyle(&stream_windows_system_chinese_font_);
ImGuiStyle &style = ImGui::GetStyle();
style.ScaleAllSizes(dpi_scale_);
style.FontScaleDpi = dpi_scale_;
ImGui_ImplSDL3_InitForSDLRenderer(stream_window_, stream_renderer_);
ImGui_ImplSDLRenderer3_Init(stream_renderer_);
// change props->stream_render_rect_
SDL_Event event;
event.type = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
event.window.windowID = SDL_GetWindowID(stream_window_);
SDL_PushEvent(&event);
stream_window_created_ = true;
just_created_ = true;
stream_window_inited_ = true;
LOG_INFO("Stream window inited");
return 0;
}
int GuiApplication::DestroyStreamWindow() {
stream_window_width_ = (float)stream_window_width_default_;
stream_window_height_ = (float)stream_window_height_default_;
if (stream_ctx_) {
ImGui::SetCurrentContext(stream_ctx_);
}
if (stream_renderer_) {
SDL_DestroyRenderer(stream_renderer_);
stream_renderer_ = nullptr;
}
if (stream_window_) {
SDL_DestroyWindow(stream_window_);
stream_window_ = nullptr;
}
stream_window_created_ = false;
focus_on_stream_window_ = false;
stream_window_grabbed_ = false;
control_mouse_ = false;
return 0;
}
int GuiApplication::CreateServerWindow() {
if (server_window_created_) {
return 0;
}
server_ctx_ = ImGui::CreateContext();
if (!server_ctx_) {
LOG_ERROR("Server context is null");
return -1;
}
ImGui::SetCurrentContext(server_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Server Window", (int)server_window_width_,
(int)server_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
&server_window_, &server_renderer_)) {
LOG_ERROR("Error creating server_window_ and server_renderer_: {}",
SDL_GetError());
return -1;
}
#if _WIN32
// Hide server window from the taskbar by making it a tool window.
{
SDL_PropertiesID server_props = SDL_GetWindowProperties(server_window_);
HWND server_hwnd = (HWND)SDL_GetPointerProperty(
server_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
if (server_hwnd) {
LONG_PTR ex_style = GetWindowLongPtr(server_hwnd, GWL_EXSTYLE);
ex_style |= WS_EX_TOOLWINDOW;
ex_style &= ~WS_EX_APPWINDOW;
SetWindowLongPtr(server_hwnd, GWL_EXSTYLE, ex_style);
// Keep the server window above normal windows.
SetWindowPos(server_hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOACTIVATE);
}
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
// Best-effort keep above other windows on X11.
X11SetWindowAlwaysOnTop(server_window_);
// Best-effort hide from taskbar on X11.
X11SetWindowSkipTaskbar(server_window_);
#endif
#if defined(__APPLE__)
// Best-effort keep above other windows on macOS.
MacSetWindowAlwaysOnTop(server_window_, true);
// Best-effort exclude from Window menu / window cycling.
MacSetWindowExcludedFromWindowMenu(server_window_, true);
#endif
// Set window position to bottom-right corner
SDL_Rect display_bounds;
if (SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(server_window_),
&display_bounds)) {
int window_x =
display_bounds.x + display_bounds.w - (int)server_window_width_;
int window_y =
display_bounds.y + display_bounds.h - (int)server_window_height_;
SDL_SetWindowPosition(server_window_, window_x, window_y);
}
SDL_SetWindowResizable(server_window_, false);
SDL_SetRenderDrawBlendMode(server_renderer_, SDL_BLENDMODE_BLEND);
// for window region action
SDL_SetWindowHitTest(server_window_, HitTestCallback, this);
SetupFontAndStyle(&server_windows_system_chinese_font_);
ImGuiStyle &style = ImGui::GetStyle();
style.ScaleAllSizes(dpi_scale_);
style.FontScaleDpi = dpi_scale_;
ImGui_ImplSDL3_InitForSDLRenderer(server_window_, server_renderer_);
ImGui_ImplSDLRenderer3_Init(server_renderer_);
server_window_created_ = true;
server_window_inited_ = true;
LOG_INFO("Server window inited");
return 0;
}
int GuiApplication::DestroyServerWindow() {
if (server_ctx_) {
ImGui::SetCurrentContext(server_ctx_);
}
if (server_renderer_) {
SDL_DestroyRenderer(server_renderer_);
server_renderer_ = nullptr;
}
if (server_window_) {
SDL_DestroyWindow(server_window_);
server_window_ = nullptr;
}
server_window_created_ = false;
server_window_inited_ = false;
return 0;
}
int GuiApplication::SetupFontAndStyle(ImFont **system_chinese_font_out) {
float font_size = 32.0f;
// Setup Dear ImGui style
ImGuiIO &io = ImGui::GetIO();
io.IniFilename = NULL; // disable imgui.ini
// Build one merged atlas: UI font + icon font + multilingual fallback fonts.
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
config.MergeMode = false;
if (system_chinese_font_out) {
*system_chinese_font_out = nullptr;
}
ImFont *ui_font = nullptr;
const ImWchar *multilingual_ranges = GetMultilingualGlyphRanges();
#if defined(_WIN32)
const char *base_font_paths[] = {
"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/msyhbd.ttc",
"C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf",
"C:/Windows/Fonts/simsun.ttc", nullptr};
#elif defined(__APPLE__)
const char *base_font_paths[] = {
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/System/Library/Fonts/SFNS.ttf", nullptr};
#else
const char *base_font_paths[] = {
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf",
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
nullptr};
#endif
for (int i = 0; base_font_paths[i] != nullptr && ui_font == nullptr; ++i) {
if (!CanReadFontFile(base_font_paths[i])) {
continue;
}
ui_font = io.Fonts->AddFontFromFileTTF(base_font_paths[i], font_size,
&config, multilingual_ranges);
if (ui_font != nullptr) {
LOG_INFO("Loaded base UI font: {}", base_font_paths[i]);
}
}
if (!ui_font) {
ui_font = io.Fonts->AddFontDefault(&config);
}
if (!ui_font) {
LOG_WARN("Failed to initialize base UI font");
ImGui::StyleColorsLight();
return 0;
}
ImFontConfig icon_config = config;
icon_config.MergeMode = true;
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len,
font_size, &icon_config, icon_ranges);
io.FontDefault = ui_font;
if (system_chinese_font_out) {
*system_chinese_font_out = ui_font;
}
ImGui::StyleColorsLight();
return 0;
}
int GuiApplication::DestroyMainWindowContext() {
if (!main_ctx_) {
return 0;
}
ImGui::SetCurrentContext(main_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(main_ctx_);
main_ctx_ = nullptr;
return 0;
}
int GuiApplication::DestroyStreamWindowContext() {
if (!stream_ctx_) {
stream_window_inited_ = false;
return 0;
}
stream_window_inited_ = false;
ImGui::SetCurrentContext(stream_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(stream_ctx_);
stream_ctx_ = nullptr;
return 0;
}
int GuiApplication::DestroyServerWindowContext() {
if (!server_ctx_) {
server_window_inited_ = false;
return 0;
}
server_window_inited_ = false;
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(server_ctx_);
server_ctx_ = nullptr;
return 0;
}
} // namespace crossdesk
@@ -1,236 +0,0 @@
#include "application/gui_application.h"
#include <cmath>
#include "rd_log.h"
namespace crossdesk {
int GuiApplication::DrawMainWindow() {
if (!main_ctx_) {
LOG_ERROR("Main context is null");
return -1;
}
ImGui::SetCurrentContext(main_ctx_);
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGuiIO &io = ImGui::GetIO();
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y),
ImGuiCond_Always);
ImGui::Begin("MainRender", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoDocking);
ImGui::PopStyleColor();
ImGui::PopStyleVar();
TitleBar(true);
MainWindow();
UpdateNotificationWindow();
#if _WIN32 && CROSSDESK_PORTABLE
PortableServiceInstallWindow();
#endif
#ifdef __APPLE__
if (show_request_permission_window_) {
RequestPermissionWindow();
}
#endif
ImGui::End();
// Rendering
(void)io;
ImGui::Render();
SDL_SetRenderScale(main_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(main_renderer_, 0, 0, 0, 0);
SDL_RenderClear(main_renderer_);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), main_renderer_);
SDL_RenderPresent(main_renderer_);
return 0;
}
int GuiApplication::DrawStreamWindow() {
if (!stream_ctx_) {
LOG_ERROR("Stream context is null");
return -1;
}
ImGui::SetCurrentContext(stream_ctx_);
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
StreamWindow();
ImGuiIO &io = ImGui::GetIO();
float stream_title_window_height =
fullscreen_button_pressed_ ? 0 : title_bar_height_;
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
// Set minimum window size to 0 to allow exact height control
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, stream_title_window_height),
ImGuiCond_Always);
ImGui::Begin("StreamTitleWindow", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoDocking);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
if (!fullscreen_button_pressed_) {
TitleBar(false);
}
ImGui::End();
// Rendering
(void)io;
ImGui::Render();
SDL_SetRenderScale(stream_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(stream_renderer_, 0, 0, 0, 255);
SDL_RenderClear(stream_renderer_);
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &it : remote_sessions_) {
auto props = it.second;
if (props->tab_selected_) {
SDL_FRect render_rect_f = {
props->stream_render_rect_f_.x, props->stream_render_rect_f_.y,
props->stream_render_rect_f_.w, props->stream_render_rect_f_.h};
SDL_RenderTexture(stream_renderer_, props->stream_texture_, NULL,
&render_rect_f);
}
}
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), stream_renderer_);
SDL_RenderPresent(stream_renderer_);
return 0;
}
int GuiApplication::DrawServerWindow() {
if (!server_ctx_) {
LOG_ERROR("Server context is null");
return -1;
}
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGuiIO &io = ImGui::GetIO();
server_window_width_ = io.DisplaySize.x;
server_window_height_ = io.DisplaySize.y;
ServerWindow();
ImGui::Render();
SDL_SetRenderScale(server_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(server_renderer_, 0, 0, 0, 0);
SDL_RenderClear(server_renderer_);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), server_renderer_);
SDL_RenderPresent(server_renderer_);
return 0;
}
bool GuiApplication::MinimizeMainWindowToTray() {
if (!main_window_) {
return false;
}
#if defined(_WIN32) || defined(__APPLE__)
if (!tray_) {
return false;
}
tray_->MinimizeToTray();
return true;
#elif defined(__linux__) && !defined(__APPLE__)
if (!tray_) {
return false;
}
return tray_->MinimizeToTray();
#else
return false;
#endif
}
void GuiApplication::UpdateRenderRect() {
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &[_, props] : remote_sessions_) {
if (!props->reset_control_bar_pos_) {
props->mouse_diff_control_bar_pos_x_ = 0;
props->mouse_diff_control_bar_pos_y_ = 0;
}
if (!just_created_) {
props->reset_control_bar_pos_ = true;
}
int stream_window_width, stream_window_height;
SDL_GetWindowSize(stream_window_, &stream_window_width,
&stream_window_height);
stream_window_width_ = (float)stream_window_width;
stream_window_height_ = (float)stream_window_height;
float video_ratio =
(float)props->video_width_ / (float)props->video_height_;
float video_ratio_reverse =
(float)props->video_height_ / (float)props->video_width_;
float render_area_width = props->render_window_width_;
float render_area_height = props->render_window_height_;
props->stream_render_rect_last_ = props->stream_render_rect_;
SDL_FRect rect_f{props->render_window_x_, props->render_window_y_,
render_area_width, render_area_height};
if (render_area_width < render_area_height * video_ratio) {
rect_f.x = props->render_window_x_;
rect_f.y = std::abs(render_area_height -
render_area_width * video_ratio_reverse) /
2.0f +
props->render_window_y_;
rect_f.w = render_area_width;
rect_f.h = render_area_width * video_ratio_reverse;
} else if (render_area_width > render_area_height * video_ratio) {
rect_f.x =
std::abs(render_area_width - render_area_height * video_ratio) /
2.0f +
props->render_window_x_;
rect_f.y = props->render_window_y_;
rect_f.w = render_area_height * video_ratio;
rect_f.h = render_area_height;
} else {
rect_f.x = props->render_window_x_;
rect_f.y = props->render_window_y_;
rect_f.w = render_area_width;
rect_f.h = render_area_height;
}
props->stream_render_rect_f_ = rect_f;
props->stream_render_rect_ = {static_cast<int>(std::lround(rect_f.x)),
static_cast<int>(std::lround(rect_f.y)),
static_cast<int>(std::lround(rect_f.w)),
static_cast<int>(std::lround(rect_f.h))};
}
}
} // namespace crossdesk
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,85 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-06-14
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LAYOUT_STYLE_H_
#define _LAYOUT_STYLE_H_
#define MENU_WINDOW_WIDTH_CN 300 * dpi_scale_
#define MENU_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define LOCAL_WINDOW_WIDTH_CN 300 * dpi_scale_
#define LOCAL_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define REMOTE_WINDOW_WIDTH_CN 300 * dpi_scale_
#define REMOTE_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define MENU_WINDOW_WIDTH_EN 190 * dpi_scale_
#define MENU_WINDOW_HEIGHT_EN 245 * dpi_scale_
#define IPUT_WINDOW_WIDTH 160 * dpi_scale_
#define INPUT_WINDOW_PADDING_CN 66 * dpi_scale_
#define INPUT_WINDOW_PADDING_EN 96 * dpi_scale_
#define SETTINGS_WINDOW_WIDTH_CN 202 * dpi_scale_
#define SETTINGS_WINDOW_WIDTH_EN 248 * dpi_scale_
#if USE_CUDA
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 405 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 405 * dpi_scale_
#else
#define SETTINGS_WINDOW_HEIGHT_CN 375 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 375 * dpi_scale_
#endif
#else
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 375 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 375 * dpi_scale_
#else
#define SETTINGS_WINDOW_HEIGHT_CN 345 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 345 * dpi_scale_
#endif
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 195 * dpi_scale_
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_AUTOSTART_PADDING_CN 171 * dpi_scale_
#define ENABLE_AUTOSTART_PADDING_EN 218 * dpi_scale_
#define ENABLE_DAEMON_PADDING_CN 171 * dpi_scale_
#define ENABLE_DAEMON_PADDING_EN 218 * dpi_scale_
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171 * dpi_scale_
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218 * dpi_scale_
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90 * dpi_scale_
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137 * dpi_scale_
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90 * dpi_scale_
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137 * dpi_scale_
#define SETTINGS_SELECT_WINDOW_WIDTH 73 * dpi_scale_
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130 * dpi_scale_
#define SETTINGS_OK_BUTTON_PADDING_CN 65 * dpi_scale_
#define SETTINGS_OK_BUTTON_PADDING_EN 83 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91 * dpi_scale_
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162 * dpi_scale_
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146 * dpi_scale_
#define UPDATE_NOTIFICATION_RESERVED_HEIGHT 120 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_WIDTH_CN 130 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_HEIGHT_CN 125 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_WIDTH_EN 260 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210 * dpi_scale_
#endif
@@ -1,89 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-06-14
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LAYOUT_STYLE_H_
#define _LAYOUT_STYLE_H_
#define TITLE_BAR_HEIGHT 0.0625f
#define TITLE_BAR_BUTTON_WIDTH 0.0625f
#define TITLE_BAR_BUTTON_HEIGHT 0.0625f
#define STATUS_BAR_HEIGHT 0.05f
#define MENU_WINDOW_WIDTH_CN 300
#define MENU_WINDOW_HEIGHT_CN 280
#define LOCAL_WINDOW_WIDTH_CN 300
#define LOCAL_WINDOW_HEIGHT_CN 280
#define REMOTE_WINDOW_WIDTH_CN 300
#define REMOTE_WINDOW_HEIGHT_CN 280
#define MENU_WINDOW_WIDTH_EN 190
#define MENU_WINDOW_HEIGHT_EN 245
#define IPUT_WINDOW_WIDTH 160
#define INPUT_WINDOW_PADDING_CN 66
#define INPUT_WINDOW_PADDING_EN 96
#define SETTINGS_WINDOW_WIDTH_CN 202
#define SETTINGS_WINDOW_WIDTH_EN 248
#if USE_CUDA
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 405
#define SETTINGS_WINDOW_HEIGHT_EN 405
#else
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#endif
#else
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#else
#define SETTINGS_WINDOW_HEIGHT_CN 345
#define SETTINGS_WINDOW_HEIGHT_EN 345
#endif
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 195
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218
#define ENABLE_AUTOSTART_PADDING_CN 171
#define ENABLE_AUTOSTART_PADDING_EN 218
#define ENABLE_DAEMON_PADDING_CN 171
#define ENABLE_DAEMON_PADDING_EN 218
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137
#define SETTINGS_SELECT_WINDOW_WIDTH 73
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130
#define SETTINGS_OK_BUTTON_PADDING_CN 65
#define SETTINGS_OK_BUTTON_PADDING_EN 83
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146
#define UPDATE_NOTIFICATION_RESERVED_HEIGHT 120
#define REQUEST_PERMISSION_WINDOW_WIDTH_CN 130
#define REQUEST_PERMISSION_WINDOW_HEIGHT_CN 125
#define REQUEST_PERMISSION_WINDOW_WIDTH_EN 260
#define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210
#endif
@@ -20,11 +20,78 @@ struct TranslationRow {
// Single source of truth for all UI strings.
#define CROSSDESK_LOCALIZATION_ALL(X) \
X(console_cli_help, u8"Linux 无头运行选项:\n --headless 自动选择已有桌面并启用控制台\n --headless-display DISPLAY 高级:指定已有显示会话\n --headless-size WIDTHxHEIGHT 新建虚拟屏幕;默认1920x1080,偶数尺寸320..8192\n --headless-session EXECUTABLE 启动桌面环境(startxfce4;none 仅启动裸屏幕)\n --no-headless 禁用自动回退\n正常启动会自动选择已有桌面,无需填写显示编号。控制台输入 settings 管理设置。\n", "Linux headless options:\n --headless Select an existing desktop and enable the console\n --headless-display DISPLAY Advanced: override desktop selection\n --headless-size WIDTHxHEIGHT New virtual display; default 1920x1080, even 320..8192\n --headless-session EXECUTABLE Start a desktop (startxfce4; none for a bare display)\n --no-headless Disable automatic fallback\nNormal startup selects your existing desktop automatically. Enter settings in the console to configure it.\n", u8"Параметры запуска без монитора:\n --headless Выбрать рабочий стол и включить консоль\n --headless-display DISPLAY Дополнительно: указать сеанс дисплея\n --headless-size WIDTHxHEIGHT Новый виртуальный экран; 1920x1080 по умолчанию, чётные размеры 320..8192\n --headless-session EXECUTABLE Запустить рабочий стол (startxfce4; none — только экран)\n --no-headless Отключить автоматический резервный запуск\nРабочий стол выбирается автоматически. Команда settings открывает настройки консоли.\n") \
X(console_title, u8"CrossDesk 无头控制台", "CrossDesk headless console", u8"Консоль CrossDesk") \
X(console_log_file, u8"日志文件", "Log file", u8"Файл журнала") \
X(console_help, u8"命令: status 查看ID/密码/状态;settings 设置;password 修改密码;random 随机密码;help 帮助;quit 退出", "Commands: status, settings, password, random, help, quit", u8"Команды: status, settings, password, random, help, quit") \
X(console_status, u8"状态", "Status", u8"Состояние") \
X(console_online, u8"在线", "Online", u8"В сети") \
X(console_offline, u8"未连接服务器", "Disconnected from server", u8"Нет подключения к серверу") \
X(console_id, u8"设备 ID", "Device ID", u8"ID устройства") \
X(console_password, u8"连接密码", "Connection password", u8"Пароль подключения") \
X(console_waiting, u8"等待分配", "Waiting for assignment", u8"Ожидание назначения") \
X(console_password_hidden, u8"(仅在交互终端显示)", "(shown only in an interactive terminal)", u8"(виден только в интерактивном терминале)") \
X(console_cancelled, u8"已取消修改。", "Change cancelled.", u8"Изменение отменено.") \
X(console_password_invalid, u8"密码必须是6位英文字母或数字,未提交修改。", "Password must be 6 ASCII letters or digits; no change submitted.", u8"Пароль должен содержать 6 латинских букв или цифр. Изменение не отправлено.") \
X(console_password_offline, u8"尚未连接服务器,暂时无法修改密码。", "Connect to the server before changing the password.", u8"Для смены пароля подключитесь к серверу.") \
X(console_password_prompt, u8"输入新密码(6位英文字母或数字;输入 cancel 取消):", "New password (6 ASCII letters or digits; cancel to abort):", u8"Новый пароль (6 латинских букв или цифр; cancel — отмена):") \
X(console_quitting, u8"正在退出 CrossDesk……", "Exiting CrossDesk...", u8"Выход из CrossDesk...") \
X(console_unknown_command, u8"未知命令,输入 help 查看帮助。", "Unknown command. Enter help for commands.", u8"Неизвестная команда. Введите help для справки.") \
X(console_line_too_long, u8"命令过长,已忽略。", "Command too long; ignored.", u8"Команда слишком длинная и проигнорирована.") \
X(console_random_failed, u8"无法生成随机密码,请重试。", "Unable to generate a random password; try again.", u8"Не удалось создать случайный пароль. Повторите попытку.") \
X(console_password_pending, u8"密码修改正在处理中,请等待服务器确认。", "Password change pending; wait for server confirmation.", u8"Смена пароля выполняется. Дождитесь подтверждения сервера.") \
X(console_password_stage_failed, u8"无法保存密码修改记录,未提交修改。请检查日志。", "Unable to save password recovery record; no change submitted. Check the log.", u8"Не удалось сохранить запись восстановления пароля. Изменение не отправлено. Проверьте журнал.") \
X(console_password_send_failed, u8"发送密码修改请求失败,请等待连接恢复后重试。", "Failed to submit password change; retry after reconnection.", u8"Не удалось отправить запрос смены пароля. Повторите после подключения.") \
X(console_password_submitted, u8"已提交密码修改,正在等待服务器确认……", "Password change submitted; waiting for server confirmation...", u8"Запрос смены пароля отправлен. Ожидание подтверждения...") \
X(console_password_uncertain, u8"密码修改结果尚未确认,正在重新连接服务器核实。", "Password change not yet confirmed; reconnecting to verify.", u8"Смена пароля не подтверждена. Повторное подключение для проверки.") \
X(console_password_rejected, u8"服务器未接受密码修改,原密码保持不变。详情见日志。", "Server rejected the password change; the original password remains. See the log.", u8"Сервер отклонил смену пароля. Старый пароль сохранён. См. журнал.") \
X(console_password_confirmed, u8"服务器已确认密码修改,正在重新连接。", "Server confirmed the password change; reconnecting.", u8"Сервер подтвердил смену пароля. Повторное подключение.") \
X(console_password_recovered, u8"重新连接确认:新密码已生效。", "Reconnection confirmed: the new password is active.", u8"Повторное подключение подтвердило новый пароль.") \
X(console_password_retained, u8"重新连接确认:继续使用原密码。", "Reconnection confirmed: the original password remains active.", u8"Повторное подключение подтвердило прежний пароль.") \
X(console_exited, u8"CrossDesk 已退出。", "CrossDesk exited.", u8"CrossDesk завершён.") \
X(console_stopped, u8"CrossDesk 已停止,退出码", "CrossDesk stopped; exit code", u8"CrossDesk остановлен; код выхода") \
X(console_settings_title, u8"设置(修改后立即保存)", "Settings (each change is saved immediately)", u8"Настройки (изменения сохраняются сразу)") \
X(console_settings_prompt, u8"输入编号或选项名称修改;back 返回。也可使用 settings set 名称 值。", "Enter an item number or key; back to return. Shortcut: settings set KEY VALUE.", u8"Введите номер или имя параметра; back — назад. Команда: settings set KEY VALUE.") \
X(console_settings_value, u8"输入新值(cancel 取消)", "Enter a new value (cancel to abort)", u8"Введите новое значение (cancel — отмена)") \
X(console_settings_unavailable, u8"设置尚未初始化,请稍后重试。", "Settings are not ready; try again shortly.", u8"Настройки ещё не готовы. Повторите позже.") \
X(console_settings_invalid, u8"无效的选项或值,未保存。请按菜单列出的格式输入。", "Invalid setting or value; not saved. Use the format shown in the menu.", u8"Неверный параметр или значение. Ничего не сохранено. Используйте указанный формат.") \
X(console_settings_save_failed, u8"保存设置失败,原设置保持不变。请检查日志。", "Failed to save settings; the previous value is retained. Check the log.", u8"Не удалось сохранить настройки. Прежнее значение сохранено. Проверьте журнал.") \
X(console_settings_busy, u8"有远程会话或密码修改正在进行,暂时不能更改连接设置。", "Connection settings cannot change during a remote session or password change.", u8"Нельзя менять параметры подключения во время удалённого сеанса или смены пароля.") \
X(console_settings_saved, u8"已保存并生效。", "Saved and applied.", u8"Сохранено и применено.") \
X(console_settings_unchanged, u8"设置未改变。", "Setting unchanged.", u8"Настройка не изменилась.") \
X(console_settings_reconnect, u8"已保存,正在重新连接服务器。", "Saved; reconnecting to the server.", u8"Сохранено. Повторное подключение к серверу.") \
X(console_settings_next_connection, u8"已保存,将用于后续连接。", "Saved; applies to subsequent connections.", u8"Сохранено. Будет применено к следующим подключениям.") \
X(console_settings_next_file, u8"已保存,将用于后续接收的文件。", "Saved; applies to subsequently received files.", u8"Сохранено. Будет применено к следующим получаемым файлам.") \
X(console_settings_autostart, u8"已保存,在下次图形桌面登录时生效。", "Saved; applies at the next graphical desktop login.", u8"Сохранено. Будет применено при следующем входе в графический сеанс.") \
X(console_settings_daemon, u8"已保存,普通模式重启后生效;无头控制台仍保持前台运行。", "Saved for the next normal-mode start; the headless console stays in the foreground.", u8"Сохранено для следующего запуска в обычном режиме. Консоль остаётся на переднем плане.") \
X(console_settings_hardware_unavailable, u8"当前构建不支持硬件编解码,未保存。", "Hardware codecs are unavailable in this build; not saved.", u8"Аппаратные кодеки недоступны в этой сборке. Не сохранено.") \
X(console_settings_server_required, u8"请先设置有效的服务器地址和端口,再启用自托管。", "Set a valid server host and port before enabling self-hosting.", u8"Укажите адрес и порт сервера перед включением собственного сервера.") \
X(console_settings_path_invalid, u8"请选择已存在且可写的绝对目录路径;default 使用默认目录。", "Use an existing writable absolute directory, or default for the default location.", u8"Укажите существующий доступный для записи абсолютный путь к каталогу или default.") \
X(console_settings_readonly, u8"该选项由系统或控制端管理,不可在此修改。", "This option is managed by the system or controller and is read-only here.", u8"Этот параметр управляется системой или клиентом и здесь недоступен для изменения.") \
X(console_setting_language, u8"语言", "Language", u8"Язык") \
X(console_setting_codec, u8"视频编码", "Video codec", u8"Видеокодек") \
X(console_setting_hardware, u8"硬件编解码", "Hardware codecs", u8"Аппаратные кодеки") \
X(console_setting_turn, u8"中继模式", "TURN relay mode", u8"Режим ретрансляции TURN") \
X(console_setting_self_hosted, u8"自托管", "Self-hosted server", u8"Собственный сервер") \
X(console_setting_host, u8"服务器地址", "Server host", u8"Адрес сервера") \
X(console_setting_port, u8"信令端口", "Signaling port", u8"Порт сигнализации") \
X(console_setting_privacy, u8"连接时自动开启隐私屏", "Privacy screen on connection", u8"Приватный экран при подключении") \
X(console_setting_files, u8"文件保存目录", "Received file directory", u8"Каталог получаемых файлов") \
X(console_setting_autostart, u8"登录桌面时自启", "Start at desktop login", u8"Запуск при входе в систему") \
X(console_setting_daemon, u8"守护进程(无头控制台中不生效)", "Daemon (inactive in headless console)", u8"Фоновый режим (не действует в консоли)") \
X(privacy_screen, u8"隐私屏", "Privacy screen", u8"Приватный экран") \
X(privacy_enable, u8"开启隐私屏", "Enable privacy screen", u8"Включить приватный экран") \
X(privacy_disable, u8"关闭隐私屏", "Disable privacy screen", u8"Выключить приватный экран") \
X(privacy_on, u8"隐私屏:已开启", "Privacy screen: on", u8"Приватный экран: включён") \
X(privacy_off, u8"隐私屏:已关闭", "Privacy screen: off", u8"Приватный экран: выключен") \
X(privacy_unsupported, u8"隐私屏:不可用", "Privacy screen: unavailable", u8"Приватный экран: недоступен") \
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
u8"Скопировано в буфер обмена") \
X(password, u8"密码", "Password", u8"Пароль") \
X(refresh_password, u8"刷新随机密码", "Refresh random password", \
u8"Обновить случайный пароль") \
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
u8"Удаленный рабочий стол") \
@@ -120,6 +187,13 @@ struct TranslationRow {
u8"стола") \
X(settings, u8"设置", "Settings", u8"Настройки") \
X(language, u8"语言:", "Language:", u8"Язык:") \
X(screen_capture_method, u8"采集方式:", "Capture Method:", \
u8"Способ захвата:") \
X(screen_capture_method_auto, u8"自动", "Auto", u8"Авто") \
X(video_settings, u8"画面设置", "Video settings", u8"Настройки видео") \
X(video_settings_pending, u8"正在应用…", "Applying…", u8"Применение…") \
X(video_settings_failed, u8"调整失败,请重试。", "Could not apply settings. Try again.", u8"Не удалось применить настройки. Повторите попытку.") \
X(video_settings_unavailable, u8"连接建立后可调整,需被控端支持。", "Available when connected to a supported remote device.", u8"Доступно при подключении к поддерживаемому устройству.") \
X(video_quality, u8"画面质量:", "Video Quality:", u8"Качество видео:") \
X(video_frame_rate, u8"画面采集帧率:", \
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
@@ -140,7 +214,7 @@ struct TranslationRow {
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
X(enable_turn, u8"启用中继服务:", \
"Enable TURN Service:", u8"Включить TURN-сервис:") \
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
X(force_relay, u8"强制中继连接:", "Force Relay:", u8"Только через реле:") \
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
u8"Конфигурация self-hosted") \
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
@@ -149,8 +223,6 @@ struct TranslationRow {
"Server Address:", u8"Адрес сервера:") \
X(self_hosted_server_port, u8"信令服务端口:", \
"Signal Service Port:", u8"Порт сигнального сервиса:") \
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
"Relay Service Port:", u8"Порт реле-сервиса:") \
X(ok, u8"确认", "OK", u8"ОК") \
X(cancel, u8"取消", "Cancel", u8"Отмена") \
X(new_password, u8"请输入六位密码:", \
@@ -186,6 +258,15 @@ struct TranslationRow {
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \
X(check_for_updates, u8"检查更新", "Check for updates", \
u8"Проверить обновления") \
X(checking_for_updates, u8"正在检查更新…", "Checking for updates…", \
u8"Проверка обновлений…") \
X(up_to_date, u8"已是最新版本", "You're up to date", \
u8"Установлена последняя версия") \
X(update_check_failed, u8"检查失败,请稍后重试", \
"Check failed. Please try again later.", \
u8"Не удалось проверить обновления. Повторите позже.") \
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
@@ -204,6 +285,8 @@ struct TranslationRow {
"Confirm to delete this connection", u8"Удалить это подключение?") \
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
X(privacy_on_connect, u8"被连接时开启隐私屏:", "Privacy screen on connect:", \
u8"Приватность при подключении:") \
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
u8"Вступит в силу после перезапуска") \
X(select_file, u8"选择文件发送", "Select File to Send", \
@@ -222,9 +305,14 @@ struct TranslationRow {
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
X(video_latency, u8"视频延时", "Video delay", u8"Задержка") \
X(connection_latency, u8"连接延时", "Connection RTT", u8"Задержка соединения") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
X(transport_encryption, u8"加密传输", "Encryption", u8"Шифрование") \
X(transport_encryption_enabled, u8"已开启", "Enabled", u8"Включено") \
X(transport_encryption_disabled, u8"未开启", "Disabled", u8"Выключено") \
X(online, u8"在线", "Online", u8"Онлайн") \
X(offline, u8"离线", "Offline", u8"Офлайн") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
@@ -239,6 +327,9 @@ struct TranslationRow {
u8"Для работы приложения требуются следующие разрешения:") \
X(show_main_window, u8"显示主界面", "Show Main Window", \
u8"Показать главное окно") \
X(privacy_screen_unlock_hint, u8"按下快捷键,解除隐私屏", \
"To turn off the privacy screen, press", \
u8"Чтобы отключить приватный экран, нажмите") \
X(exit_program, u8"退出", "Exit", u8"Выход")
inline constexpr TranslationRow kTranslationRows[] = {
@@ -1,5 +1,9 @@
#include "features/clipboard/clipboard_controller.h"
#include <SDL3/SDL_clipboard.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_events.h>
#include <cstring>
#include <shared_mutex>
#include <utility>
@@ -1,7 +1,11 @@
#ifndef CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
#define CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-25
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include <SDL3/SDL.h>
#ifndef _CLIPBOARD_CONTROLLER_H_
#define _CLIPBOARD_CONTROLLER_H_
#include <atomic>
#include <cstddef>
@@ -42,4 +46,4 @@ private:
} // namespace crossdesk
#endif // CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
#endif // _CLIPBOARD_CONTROLLER_H_
@@ -1,14 +1,27 @@
#include "features/devices/session_device_manager.h"
#include <remote_action.h>
#include <chrono>
#include <cstdlib>
#include <SDL3/SDL_audio.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_init.h>
#include <display_stream_id.h>
#include <remote_action.h>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <utility>
#include "device_controller_factory.h"
#include "screen_capturer_factory.h"
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "speaker_capturer_factory.h"
#ifdef _WIN32
#include "platform/windows/screen_capturer/screen_capturer_win.h"
#include "platform/windows/input/mouse/mouse_controller.h"
#endif
namespace crossdesk {
namespace {
@@ -19,7 +32,23 @@ constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1);
} // namespace
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner) : owner_(owner) {}
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner)
: owner_(owner),
speaker_capture_(
[] {
return std::unique_ptr<SpeakerCapturer>(
SpeakerCapturerFactory().Create());
},
[this](unsigned char *data, size_t size, const char *) {
if (!owner_.peer_) return;
MiniRtcAudioFrame frame{};
frame.data = reinterpret_cast<const char *>(data);
frame.size = size;
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
SendAudioFrame(owner_.peer_, &frame, owner_.audio_label_.c_str());
}) {}
SessionDeviceManager::~SessionDeviceManager() { speaker_capture_.Shutdown(); }
bool SessionDeviceManager::ShouldSendCapturedFrame(
std::chrono::steady_clock::time_point now, int fps) {
@@ -38,10 +67,56 @@ bool SessionDeviceManager::ShouldSendCapturedFrame(
return true;
}
void SessionDeviceManager::RecordCaptureCadence(
std::chrono::steady_clock::time_point now, int fps,
bool from_secure_desktop) {
std::lock_guard<std::mutex> lock(capture_metrics_mutex_);
if (capture_metrics_started_ == std::chrono::steady_clock::time_point{})
capture_metrics_started_ = now;
const auto elapsed_us = std::chrono::duration_cast<std::chrono::microseconds>(
now - capture_metrics_started_)
.count();
if (elapsed_us >= 2000000) {
if (capture_forwarded_) {
const char* source = capture_secure_callbacks_ == 0 ? "normal_desktop"
: capture_secure_callbacks_ == capture_callbacks_
? "secure_desktop"
: "mixed";
LOG_DEBUG(
"Capture delivery: source={} normal_callbacks={} secure_callbacks={} "
"target_fps={} input_fps={:.2f} "
"forwarded_fps={:.2f} callbacks={} rate_dropped={} "
"forwarded={} send_errors={} max_callback_gap_us={} "
"send_avg_us={} send_max_us={} window_ms={}",
source, capture_callbacks_ - capture_secure_callbacks_,
capture_secure_callbacks_, fps,
capture_callbacks_ * 1000000.0 / elapsed_us,
capture_forwarded_ * 1000000.0 / elapsed_us, capture_callbacks_,
capture_rate_drops_, capture_forwarded_, capture_send_errors_,
capture_max_gap_us_, capture_send_us_ / capture_forwarded_,
capture_max_send_us_, elapsed_us / 1000);
}
capture_metrics_started_ = now;
capture_callbacks_ = capture_rate_drops_ = capture_forwarded_ = 0;
capture_secure_callbacks_ = 0;
capture_send_errors_ = 0;
capture_max_gap_us_ = capture_send_us_ = capture_max_send_us_ = 0;
}
if (last_capture_callback_ != std::chrono::steady_clock::time_point{}) {
capture_max_gap_us_ =
std::max<int64_t>(capture_max_gap_us_,
std::chrono::duration_cast<std::chrono::microseconds>(
now - last_capture_callback_)
.count());
}
last_capture_callback_ = std::max(last_capture_callback_, now);
++capture_callbacks_;
if (from_secure_desktop) ++capture_secure_callbacks_;
}
void SessionDeviceManager::Initialize() {
InitializeAudioOutput();
screen_capturer_factory_ = new ScreenCapturerFactory();
speaker_capturer_factory_ = new SpeakerCapturerFactory();
device_controller_factory_ = new DeviceControllerFactory();
keyboard_capturer_ =
static_cast<KeyboardCapturer *>(device_controller_factory_->Create(
@@ -59,24 +134,44 @@ int SessionDeviceManager::InitializeScreenCapturer() {
screen_capturer_ =
static_cast<ScreenCapturer *>(screen_capturer_factory_->Create());
}
screen_capturer_->SetPrivacyController(&owner_.privacy_);
#ifdef _WIN32
if (auto windows = dynamic_cast<ScreenCapturerWin*>(screen_capturer_)) {
windows->SetCaptureMethod(owner_.config_center_->GetScreenCaptureMethod());
}
#endif
last_frame_time_ = {};
next_frame_deadline_ = {};
const int fps = owner_.config_center_->GetVideoFrameRate() ==
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
? 30
: 60;
capture_metrics_started_ = last_capture_callback_ = {};
capture_callbacks_ = capture_rate_drops_ = capture_forwarded_ = 0;
capture_secure_callbacks_ = 0;
capture_send_errors_ = 0;
capture_max_gap_us_ = capture_send_us_ = capture_max_send_us_ = 0;
// Keep the shared source ready for either live rate. MiniRTC admits 30/60
// frames independently per viewer, so switching never restarts native
// capture.
const int fps = 60;
LOG_INFO("Init screen capturer with {} fps", fps);
display_info_list_.clear();
SetDisplayInfoList({});
registered_display_stream_count_ = 0;
last_video_frame_stream_id_.clear();
invalid_video_stream_id_logged_ = false;
const int init_ret = screen_capturer_->Init(
fps, [this, fps](unsigned char *data, int size, int width, int height,
const char *display_name) {
const char *display_name,
const MiniRtcNativeVideoFrame *native_frame) {
const auto now_time = std::chrono::steady_clock::now();
bool from_secure_desktop = false;
#ifdef _WIN32
from_secure_desktop =
ScreenCapturerWin::CurrentFrameIsFromSecureDesktop();
#endif
RecordCaptureCadence(now_time, fps, from_secure_desktop);
if (!ShouldSendCapturedFrame(now_time, fps)) {
std::lock_guard<std::mutex> lock(capture_metrics_mutex_);
++capture_rate_drops_;
return;
}
@@ -130,21 +225,37 @@ int SessionDeviceManager::InitializeScreenCapturer() {
last_video_frame_stream_id_ != stream_id;
if (resumed_after_gap || stream_changed) {
if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) {
LOG_INFO("Request video key frame before sending captured frame, "
LOG_DEBUG("Request video key frame before sending captured frame, "
"stream='{}', gap_ms={}, stream_changed={}",
stream_id, duration_ms, stream_changed);
}
}
XVideoFrame frame{};
MiniRtcVideoFrame frame{};
frame.data = reinterpret_cast<const char *>(data);
frame.size = size;
frame.width = width;
frame.height = height;
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
frame.native_frame = native_frame;
const auto send_started = std::chrono::steady_clock::now();
uint64_t send_errors = 0;
for (const std::string &remote_id : connected_remote_ids) {
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
remote_id.data(), remote_id.size());
if (SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
remote_id.data(), remote_id.size()) != 0)
++send_errors;
}
const auto send_us =
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - send_started)
.count();
{
std::lock_guard<std::mutex> lock(capture_metrics_mutex_);
++capture_forwarded_;
capture_send_errors_ += send_errors;
capture_send_us_ += send_us;
capture_max_send_us_ =
std::max<int64_t>(capture_max_send_us_, send_us);
}
last_video_frame_stream_id_ = stream_id;
last_frame_time_ = now_time;
@@ -152,11 +263,11 @@ int SessionDeviceManager::InitializeScreenCapturer() {
if (init_ret == 0) {
LOG_INFO("Init screen capturer success");
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
auto latest_display_info = screen_capturer_->GetDisplayInfoList();
registered_display_stream_count_ = latest_display_info.size();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
SetDisplayInfoList(std::move(latest_display_info));
}
registered_display_stream_count_ = display_info_list_.size();
return 0;
}
@@ -167,6 +278,15 @@ int SessionDeviceManager::InitializeScreenCapturer() {
return -1;
}
bool SessionDeviceManager::ShouldCaptureCursor() const {
std::shared_lock lock(owner_.connection_status_mutex_);
return std::any_of(owner_.connection_status_.begin(),
owner_.connection_status_.end(), [](const auto& entry) {
return entry.second == ConnectionStatus::Connected &&
entry.first.find("web") != std::string::npos;
});
}
int SessionDeviceManager::StartScreenCapturer() {
#ifdef __APPLE__
if (!owner_.EnsureMacScreenRecordingPermission()) {
@@ -182,8 +302,11 @@ int SessionDeviceManager::StartScreenCapturer() {
}
}
LOG_INFO("Start screen capturer, show cursor: {}", owner_.show_cursor_);
const int ret = screen_capturer_->Start(owner_.show_cursor_);
// Clear before sampling, so a change arriving after the sample remains pending.
cursor_capture_dirty_.store(false, std::memory_order_relaxed);
const bool show_cursor = ShouldCaptureCursor();
LOG_INFO("Start screen capturer, show cursor: {}", show_cursor);
const int ret = screen_capturer_->Start(show_cursor);
if (ret != 0) {
LOG_ERROR("Start screen capturer failed: {}", ret);
}
@@ -198,42 +321,46 @@ int SessionDeviceManager::StopScreenCapturer() {
return 0;
}
int SessionDeviceManager::StartSpeakerCapturer() {
if (!speaker_capturer_) {
speaker_capturer_ =
static_cast<SpeakerCapturer *>(speaker_capturer_factory_->Create());
const int init_ret = speaker_capturer_->Init(
[this](unsigned char *data, size_t size, const char *audio_name) {
SendAudioFrame(owner_.peer_, reinterpret_cast<const char *>(data),
size, owner_.audio_label_.c_str());
});
#ifdef _WIN32
void SessionDeviceManager::ReleaseRemoteMouseButtons() {
if (auto mouse = dynamic_cast<PlatformMouseController*>(mouse_controller_))
mouse->ReleasePressedButtons();
}
#endif
if (init_ret != 0) {
speaker_capturer_->Destroy();
delete speaker_capturer_;
speaker_capturer_ = nullptr;
}
}
if (!speaker_capturer_) {
return -1;
}
const int ret = speaker_capturer_->Start();
if (ret != 0) {
LOG_ERROR("Start speaker capturer failed: {}", ret);
return ret;
}
owner_.start_speaker_capturer_ = true;
return 0;
void SessionDeviceManager::StartSpeakerCapturer() {
speaker_capture_.SetEnabled(true);
}
int SessionDeviceManager::StopSpeakerCapturer() {
if (speaker_capturer_) {
speaker_capturer_->Stop();
owner_.start_speaker_capturer_ = false;
void SessionDeviceManager::StopSpeakerCapturer() {
speaker_capture_.SetEnabled(false);
}
bool SessionDeviceManager::SyncDisplayInfo() {
if (!screen_capturer_) return false;
auto displays = screen_capturer_->GetDisplayInfoList();
if (displays.empty()) return false;
// Called every UI tick; replace the shared list only when geometry actually
// changed so readers on other threads rarely contend for the lock.
bool changed = false;
{
std::lock_guard<std::mutex> lock(display_info_mutex_);
changed = !std::equal(
displays.begin(), displays.end(), display_info_list_.begin(),
display_info_list_.end(), [](const auto &a, const auto &b) {
return a.handle == b.handle && a.name == b.name &&
a.is_primary == b.is_primary && a.left == b.left &&
a.top == b.top && a.width == b.width && a.height == b.height;
});
if (changed) display_info_list_ = displays;
}
return 0;
if (changed && mouse_controller_) mouse_controller_->UpdateDisplayInfoList(displays);
return true;
}
void SessionDeviceManager::SetDisplayInfoList(std::vector<DisplayInfo> displays) {
std::lock_guard<std::mutex> lock(display_info_mutex_);
display_info_list_ = std::move(displays);
}
int SessionDeviceManager::StartMouseController() {
@@ -260,14 +387,9 @@ int SessionDeviceManager::StartMouseController() {
}
}
if (screen_capturer_) {
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
}
}
#endif
SyncDisplayInfo();
mouse_controller_ =
static_cast<MouseController *>(device_controller_factory_->Create(
DeviceControllerFactory::Device::Mouse));
@@ -276,7 +398,7 @@ int SessionDeviceManager::StartMouseController() {
return -1;
}
const int init_ret = mouse_controller_->Init(display_info_list_);
const int init_ret = mouse_controller_->Init(display_info_list());
if (init_ret != 0) {
LOG_INFO("Destroy mouse controller");
mouse_controller_->Destroy();
@@ -296,6 +418,7 @@ int SessionDeviceManager::StopMouseController() {
}
int SessionDeviceManager::StartKeyboardCapturer() {
ClearCapturedKeyboardInput();
owner_.keyboard_capturer_uses_window_events_ = false;
#ifdef __APPLE__
@@ -331,6 +454,7 @@ int SessionDeviceManager::StartKeyboardCapturer() {
},
this);
if (hook_ret != 0) {
keyboard_capturer_->Unhook();
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN(
"Start keyboard capturer failed, falling back to Slint keyboard events");
@@ -341,6 +465,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
}
int SessionDeviceManager::StopKeyboardCapturer() {
owner_.keyboard_capturer_is_started_ = false;
if (keyboard_capturer_) keyboard_capturer_->Unhook();
ClearCapturedKeyboardInput();
if (owner_.keyboard_capturer_uses_window_events_) {
owner_.keyboard_capturer_uses_window_events_ = false;
LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
@@ -348,8 +475,6 @@ int SessionDeviceManager::StopKeyboardCapturer() {
}
if (keyboard_capturer_) {
keyboard_capturer_->Unhook();
ClearCapturedKeyboardInput();
LOG_INFO("Stop keyboard capturer");
}
return 0;
@@ -419,6 +544,15 @@ void SessionDeviceManager::PushAudio(const char *data, size_t size) {
}
void SessionDeviceManager::UpdateInteractions() {
#ifdef _WIN32
if (owner_.screen_capturer_is_started_ &&
cursor_capture_dirty_.load(std::memory_order_relaxed)) {
if (auto* windows = dynamic_cast<ScreenCapturerWin*>(screen_capturer_)) {
cursor_capture_dirty_.store(false, std::memory_order_relaxed);
windows->SetCursorCapture(ShouldCaptureCursor());
}
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
const bool is_wayland_session = IsWaylandSession();
const bool stop_wayland_mouse_before_screen =
@@ -443,16 +577,6 @@ void SessionDeviceManager::UpdateInteractions() {
owner_.screen_capturer_is_started_ = false;
}
if (owner_.start_speaker_capturer_ && !owner_.speaker_capturer_is_started_) {
if (StartSpeakerCapturer() == 0) {
owner_.speaker_capturer_is_started_ = true;
}
} else if (!owner_.start_speaker_capturer_ &&
owner_.speaker_capturer_is_started_) {
StopSpeakerCapturer();
owner_.speaker_capturer_is_started_ = false;
}
if (owner_.start_mouse_controller_ && !owner_.mouse_controller_is_started_) {
if (StartMouseController() == 0) {
owner_.mouse_controller_is_started_ = true;
@@ -463,22 +587,33 @@ void SessionDeviceManager::UpdateInteractions() {
owner_.mouse_controller_is_started_ = false;
}
#if defined(__linux__) || defined(__APPLE__)
if (owner_.screen_capturer_is_started_ && screen_capturer_ &&
mouse_controller_) {
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
mouse_controller_->UpdateDisplayInfoList(display_info_list_);
if (owner_.screen_capturer_is_started_ && mouse_controller_ && SyncDisplayInfo()) {
#ifdef _WIN32
const int active_display = screen_capturer_->GetCurrentMonitorIndex();
const auto displays = display_info_list();
if (active_display >= 0 &&
active_display < static_cast<int>(displays.size())) {
owner_.selected_display_ = active_display;
if (displays[active_display].width <= 0 ||
displays[active_display].height <= 0)
ReleaseRemoteMouseButtons();
}
}
#endif
}
if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_) {
if (!owner_.keyboard_capturer_is_started_ && StartKeyboardCapturer() == 0) {
owner_.keyboard_capturer_is_started_ = true;
}
if (owner_.keyboard_capturer_is_started_) {
if (!owner_.keyboard_capturer_uses_window_events_ &&
(!keyboard_capturer_ || !keyboard_capturer_->IsHookActive())) {
StopKeyboardCapturer();
owner_.keyboard_.ForceReleasePressedKeys();
owner_.keyboard_capturer_uses_window_events_ = true;
owner_.keyboard_capturer_is_started_ = true;
LOG_WARN("Native keyboard capture stopped; using Slint keyboard events");
}
DrainCapturedKeyboardInput();
owner_.keyboard_.SendHeartbeat(false);
}
@@ -510,6 +645,7 @@ void SessionDeviceManager::DrainCapturedKeyboardInput() {
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
inputs.swap(captured_keyboard_inputs_);
}
if (owner_.privacy_.Snapshot().input_blocked) return;
for (const CapturedKeyboardInput &input : inputs) {
owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
input.scan_code, input.extended);
@@ -536,21 +672,45 @@ void SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
}
int SessionDeviceManager::SwitchDisplay(int display_id) {
return screen_capturer_ ? screen_capturer_->SwitchTo(display_id) : -1;
if (!screen_capturer_) return -1;
const int ret = screen_capturer_->SwitchTo(display_id);
if (ret == 0) SyncDisplayInfo();
return ret;
}
void SessionDeviceManager::ResetToInitialDisplay() {
if (screen_capturer_) {
screen_capturer_->ResetToInitialMonitor();
if (!screen_capturer_) {
return;
}
const int ret = screen_capturer_->ResetToInitialMonitor();
if (ret != 0) {
LOG_WARN("Reset to initial display failed, ret={}", ret);
return;
}
// Capture backends start at display 0. Reset the input mapping as well so
// the next connection cannot show that display while controlling another.
owner_.selected_display_ = 0;
SyncDisplayInfo();
}
const std::vector<DisplayInfo> &
SessionDeviceManager::display_info_list() const {
std::vector<DisplayInfo> SessionDeviceManager::display_info_list() const {
std::lock_guard<std::mutex> lock(display_info_mutex_);
return display_info_list_;
}
std::vector<HostDisplay> SessionDeviceManager::host_display_list() const {
std::lock_guard<std::mutex> lock(display_info_mutex_);
std::vector<HostDisplay> displays;
displays.reserve(display_info_list_.size());
for (const auto &display : display_info_list_) {
displays.push_back({display.name, display.left, display.top, display.right,
display.bottom});
}
return displays;
}
void SessionDeviceManager::DestroyDevices() {
speaker_capture_.Shutdown();
if (mouse_controller_) {
mouse_controller_->Destroy();
delete mouse_controller_;
@@ -561,11 +721,6 @@ void SessionDeviceManager::DestroyDevices() {
delete screen_capturer_;
screen_capturer_ = nullptr;
}
if (speaker_capturer_) {
speaker_capturer_->Destroy();
delete speaker_capturer_;
speaker_capturer_ = nullptr;
}
if (keyboard_capturer_) {
delete keyboard_capturer_;
keyboard_capturer_ = nullptr;
@@ -575,8 +730,6 @@ void SessionDeviceManager::DestroyDevices() {
void SessionDeviceManager::DestroyFactories() {
delete screen_capturer_factory_;
screen_capturer_factory_ = nullptr;
delete speaker_capturer_factory_;
speaker_capturer_factory_ = nullptr;
delete device_controller_factory_;
device_controller_factory_ = nullptr;
}
@@ -1,8 +1,14 @@
#ifndef CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
#define CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-09
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include <SDL3/SDL.h>
#ifndef _SESSION_DEVICE_MANAGER_H_
#define _SESSION_DEVICE_MANAGER_H_
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
@@ -14,14 +20,17 @@
#include <remote_action.h>
#include "device_controller.h"
#include "device_controller_factory.h"
#include "display_info.h"
#include "screen_capturer_factory.h"
#include "speaker_capturer_factory.h"
#include "speaker_capture_controller.h"
struct SDL_AudioStream;
namespace crossdesk {
class GuiRuntime;
class DeviceControllerFactory;
class ScreenCapturerFactory;
class ScreenCapturer;
// Owns media capture/playback and remote input devices for the active GUI
// session. GuiRuntime supplies application intent; this class handles device
@@ -29,17 +38,28 @@ class GuiRuntime;
class SessionDeviceManager {
public:
explicit SessionDeviceManager(GuiRuntime &owner);
~SessionDeviceManager();
void Initialize();
void UpdateInteractions();
// Call after changing the controller map. UpdateInteractions consumes this
// notification on the UI thread before reading the map under its mutex.
void OnControllerConnectionsChanged() {
cursor_capture_dirty_.store(true, std::memory_order_relaxed);
}
void DestroyDevices();
void DestroyFactories();
int InitializeScreenCapturer();
int StartScreenCapturer();
int StopScreenCapturer();
int StartSpeakerCapturer();
int StopSpeakerCapturer();
#ifdef _WIN32
void ReleaseRemoteMouseButtons();
#endif
// Requests are asynchronous; Stop drains/revokes audio delivery before
// returning so the connection peer can be destroyed safely.
void StartSpeakerCapturer();
void StopSpeakerCapturer();
int StartMouseController();
int StopMouseController();
int StartKeyboardCapturer();
@@ -55,7 +75,12 @@ public:
int SwitchDisplay(int display_id);
void ResetToInitialDisplay();
const std::vector<DisplayInfo> &display_info_list() const;
// Snapshot of the capture displays. The UI thread refreshes the list every
// tick while transport threads build host information and map mouse input
// from it, so callers get a copy taken under the lock.
std::vector<DisplayInfo> display_info_list() const;
// The same snapshot in the wire's host information form.
std::vector<HostDisplay> host_display_list() const;
private:
struct CapturedKeyboardInput {
@@ -71,26 +96,41 @@ private:
void ClearCapturedKeyboardInput();
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
int fps);
bool ShouldCaptureCursor() const;
bool SyncDisplayInfo();
void SetDisplayInfoList(std::vector<DisplayInfo> displays);
void RecordCaptureCadence(std::chrono::steady_clock::time_point now, int fps,
bool from_secure_desktop);
GuiRuntime &owner_;
SDL_AudioStream *output_stream_ = nullptr;
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
ScreenCapturer *screen_capturer_ = nullptr;
SpeakerCapturerFactory *speaker_capturer_factory_ = nullptr;
SpeakerCapturer *speaker_capturer_ = nullptr;
std::atomic<bool> cursor_capture_dirty_{true};
SpeakerCaptureController speaker_capture_;
DeviceControllerFactory *device_controller_factory_ = nullptr;
MouseController *mouse_controller_ = nullptr;
KeyboardCapturer *keyboard_capturer_ = nullptr;
// Guards display_info_list_ (UI thread writes, transport threads read).
mutable std::mutex display_info_mutex_;
std::vector<DisplayInfo> display_info_list_;
size_t registered_display_stream_count_ = 0;
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
std::mutex captured_keyboard_inputs_mutex_;
std::chrono::steady_clock::time_point last_frame_time_{};
std::chrono::steady_clock::time_point next_frame_deadline_{};
std::mutex capture_metrics_mutex_;
std::chrono::steady_clock::time_point capture_metrics_started_{};
std::chrono::steady_clock::time_point last_capture_callback_{};
uint64_t capture_callbacks_ = 0, capture_rate_drops_ = 0;
uint64_t capture_secure_callbacks_ = 0;
uint64_t capture_forwarded_ = 0, capture_send_errors_ = 0;
int64_t capture_max_gap_us_ = 0;
int64_t capture_send_us_ = 0, capture_max_send_us_ = 0;
std::string last_video_frame_stream_id_;
bool invalid_video_stream_id_logged_ = false;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
#endif
@@ -20,6 +20,16 @@ namespace crossdesk {
FileTransferManager::FileTransferManager(GuiRuntime &owner) : owner_(owner) {}
FileTransferManager::~FileTransferManager() {
std::vector<std::future<void>> workers;
{
std::lock_guard lock(workers_mutex_);
stopping_ = true;
workers.swap(workers_);
}
for (auto& worker : workers) worker.wait();
}
FileTransferManager::FileTransferState &FileTransferManager::global_state() {
return global_state_;
}
@@ -92,8 +102,16 @@ void FileTransferManager::Start(
std::shared_ptr<RemoteSession> props,
const std::filesystem::path &file_path, const std::string &file_label,
const std::string &remote_id) {
if (props && props->closing_) return;
const bool is_global = !props;
PeerPtr *peer = is_global ? owner_.peer_ : props->peer_;
PeerPtr* peer = nullptr;
std::shared_ptr<PeerEventHandler> peer_events;
{
std::shared_lock lock(owner_.remote_sessions_mutex_);
if (props && props->closing_) return;
peer = is_global ? owner_.peer_ : props->peer_;
peer_events = is_global ? owner_.peer_events_ : props->peer_events_;
}
if (!peer) {
LOG_ERROR("StartFileTransfer: invalid peer");
return;
@@ -108,7 +126,14 @@ void FileTransferManager::Start(
}
const auto props_weak = std::weak_ptr<RemoteSession>(props);
std::thread([this, peer, file_path, file_label, props_weak, remote_id,
std::lock_guard workers_lock(workers_mutex_);
if (stopping_) return;
// Reap finished senders without blocking the UI.
std::erase_if(workers_, [](auto& worker) {
return worker.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
});
workers_.push_back(std::async(std::launch::async,
[this, peer, peer_events, file_path, file_label, props_weak, remote_id,
is_global]() {
auto props_locked = props_weak.lock();
FileTransferState *state = nullptr;
@@ -166,7 +191,11 @@ void FileTransferManager::Start(
const int ret = sender.SendFile(
file_path, file_path.filename().string(),
[peer, file_label, remote_id](const char *buffer, size_t size) {
[peer, peer_events, file_label, remote_id, props_locked](const char *buffer, size_t size) {
// Close drains each in-flight send before detaching the peer.
if (props_locked && props_locked->closing_) return -1;
auto callback = peer_events->EnterCallback();
if (!callback) return -1;
if (remote_id.empty()) {
return SendReliableDataFrame(peer, buffer, size,
file_label.c_str());
@@ -200,12 +229,13 @@ void FileTransferManager::Start(
}
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
file_path.string().c_str(), ret);
ProcessQueue(props_locked);
}).detach();
if (peer_events->IsActive()) ProcessQueue(props_locked);
}));
}
void FileTransferManager::ProcessQueue(
std::shared_ptr<RemoteSession> props) {
if (props && props->closing_) return;
FileTransferState &state = state_for(props);
if (state.file_sending_.load()) {
return;
@@ -268,7 +298,7 @@ void FileTransferManager::HandleAck(const char *data, size_t size) {
uint32_t rate_bps = 0;
if (props) {
const uint32_t bitrate =
props->net_traffic_stats_.data_outbound_stats.bitrate;
props->net_traffic_stats_.DataOutboundBitrate();
if (bitrate > 0 && state->file_sending_.load()) {
rate_bps = static_cast<uint32_t>(bitrate * 0.99f);
const uint32_t current_rate = state->file_send_rate_bps_.load();
@@ -4,12 +4,15 @@
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <future>
#include <mutex>
#include <vector>
#include <memory>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include "runtime/gui_state.h"
#include "runtime/remote_session.h"
namespace crossdesk {
@@ -21,6 +24,7 @@ public:
using RemoteSession = gui_detail::RemoteSession;
explicit FileTransferManager(GuiRuntime &owner);
~FileTransferManager();
FileTransferState &global_state();
FileTransferState &
@@ -40,6 +44,9 @@ private:
void ProcessQueue(std::shared_ptr<RemoteSession> props);
void Unregister(uint32_t file_id, bool per_peer);
std::mutex workers_mutex_;
std::vector<std::future<void>> workers_;
bool stopping_ = false;
GuiRuntime &owner_;
FileTransferState global_state_;
std::unordered_map<uint32_t, std::weak_ptr<RemoteSession>>
@@ -52,6 +52,9 @@ void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out,
if (!scan_code_out || !extended_out) {
return;
}
if (LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out)) {
return;
}
#if _WIN32
const UINT scan_code =
MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
@@ -61,7 +64,6 @@ void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out,
return;
}
#endif
LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out);
}
#if _WIN32
@@ -150,6 +152,7 @@ void KeyboardController::SendHeartbeat(bool force) {
const std::string target_id = owner_.controlled_remote_id_.empty()
? owner_.focused_remote_id_
: owner_.controlled_remote_id_;
std::shared_lock sessions_lock(owner_.remote_sessions_mutex_);
const auto props_it = owner_.remote_sessions_.find(target_id);
if (target_id.empty() || props_it == owner_.remote_sessions_.end() ||
props_it->second->connection_status_.load() !=
@@ -189,6 +192,7 @@ int KeyboardController::SendKeyCommand(int key_code, bool is_down,
const std::string target_id = owner_.controlled_remote_id_.empty()
? owner_.focused_remote_id_
: owner_.controlled_remote_id_;
std::shared_lock sessions_lock(owner_.remote_sessions_mutex_);
const auto props_it = owner_.remote_sessions_.find(target_id);
if (!target_id.empty() && props_it != owner_.remote_sessions_.end() &&
props_it->second->connection_status_.load() ==
@@ -211,6 +215,9 @@ int KeyboardController::SendKeyCommand(int key_code, bool is_down,
bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
#if _WIN32
if (owner_.privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
owner_.privacy_.Fail("Secure desktop or locked session; privacy screen will turn off");
}
if (owner_.local_service_status_received_ &&
IsSecureDesktopInteractionRequired(owner_.local_interactive_stage_)) {
const std::string response = SendCrossDeskSecureDesktopKeyInput(
@@ -244,6 +251,7 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
return true;
}
#endif
// Recheck after the desktop query; key-up cleanup remains allowed while paused.
return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code,
extended);
}
@@ -340,11 +348,50 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
keys_to_release.size(), remote_id, reason ? reason : "unknown");
}
for (const PressedKey& key : keys_to_release) {
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
if (!InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
#ifdef _WIN32
if (owner_.privacy_.Engaged()) {
std::lock_guard lock(remote_states_mutex_);
pending_privacy_releases_[key.key_code] = key;
}
#endif
}
}
}
void KeyboardController::ReleaseAllRemotePressedKeys(const char* reason) {
std::vector<std::string> remotes;
{
std::lock_guard lock(remote_states_mutex_);
for (const auto& [id, state] : remote_states_) remotes.push_back(id);
}
for (const auto& id : remotes) ReleaseRemotePressedKeys(id, reason);
CheckRemoteTimeouts();
}
void KeyboardController::CheckRemoteTimeouts() {
#ifdef _WIN32
bool pending = false;
{
std::lock_guard lock(remote_states_mutex_);
pending = !pending_privacy_releases_.empty();
}
// A key-up rejected on the secure desktop is retained until the ordinary
// desktop returns. Never route cleanup through the security UI helper.
if (pending && IsWindowsPrivacyDesktopAvailable()) {
std::unordered_map<int, PressedKey> releases;
{
std::lock_guard lock(remote_states_mutex_);
releases.swap(pending_privacy_releases_);
}
for (const auto& [code, key] : releases) {
if (!owner_.devices_.SendKeyboardCommand(code, false, key.scan_code, key.extended)) {
std::lock_guard lock(remote_states_mutex_);
pending_privacy_releases_[code] = key;
}
}
}
#endif
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
std::vector<std::string> timed_out_remotes;
{
@@ -30,6 +30,7 @@ public:
const RemoteAction &remote_action);
void ReleaseRemotePressedKeys(const std::string &remote_id,
const char *reason);
void ReleaseAllRemotePressedKeys(const char* reason);
void CheckRemoteTimeouts();
private:
@@ -58,6 +59,9 @@ private:
uint32_t last_heartbeat_tick_ = 0;
std::unordered_map<std::string, RemoteState> remote_states_;
std::mutex remote_states_mutex_;
#ifdef _WIN32
std::unordered_map<int, PressedKey> pending_privacy_releases_;
#endif
};
} // namespace crossdesk
@@ -9,6 +9,7 @@
#include <fstream>
#include <functional>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
#include <thread>
@@ -297,18 +298,11 @@ int SettingsManager::Load() {
owner_.language_button_value_ = localization::detail::ClampLanguageIndex(
static_cast<int>(owner_.config_center_->GetLanguage()));
owner_.video_quality_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoQuality());
owner_.video_frame_rate_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoFrameRate());
owner_.video_adaptation_policy_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoAdaptationPolicy());
owner_.video_encode_format_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoEncodeFormat());
owner_.enable_hardware_video_codec_ =
owner_.config_center_->IsHardwareVideoCodec();
owner_.enable_turn_ = owner_.config_center_->IsEnableTurn();
owner_.enable_srtp_ = owner_.config_center_->IsEnableSrtp();
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart();
owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon();
@@ -325,17 +319,11 @@ int SettingsManager::Load() {
owner_.file_transfer_save_path_last_ = saved_path;
owner_.language_button_value_last_ = owner_.language_button_value_;
owner_.video_quality_button_value_last_ = owner_.video_quality_button_value_;
owner_.video_frame_rate_button_value_last_ =
owner_.video_frame_rate_button_value_;
owner_.video_adaptation_policy_button_value_last_ =
owner_.video_adaptation_policy_button_value_;
owner_.video_encode_format_button_value_last_ =
owner_.video_encode_format_button_value_;
owner_.enable_hardware_video_codec_last_ =
owner_.enable_hardware_video_codec_;
owner_.enable_turn_last_ = owner_.enable_turn_;
owner_.enable_srtp_last_ = owner_.enable_srtp_;
owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_;
owner_.enable_autostart_last_ = owner_.enable_autostart_;
@@ -598,22 +586,6 @@ std::string SettingsManager::RecentConnectionDisplayName(
return connection.remote_id;
}
void SettingsManager::BeginEditRecentConnectionAlias(
const Thumbnail::RecentConnection &connection) {
owner_.edit_connection_alias_remote_id_ = connection.remote_id;
std::memset(owner_.edit_connection_alias_, 0,
sizeof(owner_.edit_connection_alias_));
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
const std::string alias = alias_it != recent_connection_aliases_.end()
? alias_it->second
: RecentConnectionDisplayName(connection);
CopyString(owner_.edit_connection_alias_, alias.c_str());
owner_.focus_on_input_widget_ = true;
owner_.show_edit_connection_alias_window_ = true;
}
void SettingsManager::SetRecentConnectionAlias(const std::string &remote_id,
const std::string &alias) {
if (!remote_id.empty()) {
@@ -25,8 +25,6 @@ public:
int SaveRecentConnectionAliases() const;
std::string RecentConnectionDisplayName(
const Thumbnail::RecentConnection &connection) const;
void
BeginEditRecentConnectionAlias(const Thumbnail::RecentConnection &connection);
void SetRecentConnectionAlias(const std::string &remote_id,
const std::string &alias);
void EraseRecentConnectionAlias(const std::string &remote_id);
@@ -60,8 +58,9 @@ private:
struct CacheV1 {
char client_id_with_password[17];
int language;
int video_quality;
int video_frame_rate;
// Reserved legacy slots: retain the on-disk cache layout.
int legacy_video_quality;
int legacy_video_frame_rate;
int video_encode_format;
bool enable_hardware_video_codec;
bool enable_turn;
@@ -73,8 +72,9 @@ private:
struct CacheV2 {
char client_id_with_password[17];
int language;
int video_quality;
int video_frame_rate;
// Reserved legacy slots: retain the on-disk cache layout.
int legacy_video_quality;
int legacy_video_frame_rate;
int video_encode_format;
bool enable_hardware_video_codec;
bool enable_turn;
+10 -4
View File
@@ -1,5 +1,11 @@
#ifndef CROSSDESK_GUI_RENDER_H_
#define CROSSDESK_GUI_RENDER_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _RENDER_H_
#define _RENDER_H_
#include <memory>
@@ -7,7 +13,7 @@ namespace crossdesk {
class GuiApplication;
// Stable application-facing facade. The SDL application and feature
// Stable application-facing facade. The Slint application and feature
// controllers remain private implementation details.
class Render {
public:
@@ -25,4 +31,4 @@ private:
} // namespace crossdesk
#endif // CROSSDESK_GUI_RENDER_H_
#endif
@@ -1,4 +1,5 @@
#include "rendering/slint_video_presenter.h"
#include "crossdesk_ui.h"
#include <libyuv.h>
#include <slint.h>
@@ -86,6 +87,7 @@ struct SlintVideoPresenter::Impl {
VideoRenderSize gl_image_size;
VideoRenderSize gl_pending_size;
bool gl_pending_dirty = false;
VideoLatencyFrame gl_pending_timing;
#endif
SurfaceState CurrentState() const {
@@ -154,10 +156,8 @@ struct SlintVideoPresenter::Impl {
const auto window_size = (*stream)->window().size();
const float scale_factor =
std::max(1.0f, (*stream)->window().scale_factor());
const int top_inset =
!surface.fullscreen && surface.tab_count > 1
? static_cast<int>(std::lround(30.0f * scale_factor))
: 0;
const int top_inset = static_cast<int>(std::lround(
(*stream)->get_video_area_top() * scale_factor));
const bool rounded_window = (*stream)->get_custom_titlebar() &&
!surface.fullscreen &&
!(*stream)->window().is_maximized();
@@ -193,6 +193,7 @@ struct SlintVideoPresenter::Impl {
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
gl_pending_dirty = false;
gl_pending_timing.MarkSubmitted();
return;
}
@@ -323,7 +324,7 @@ SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
impl_->seeded_frame_sequence[frame.remote_id] != frame.sequence) {
const auto submit_result = impl_->renderer.SubmitCachedNv12(
frame.remote_id, frame.nv12->data(), frame.nv12->size(), frame.width,
frame.height);
frame.height, frame.timing);
if (submit_result == VideoRenderer::SubmitResult::submitted ||
submit_result == VideoRenderer::SubmitResult::dropped) {
impl_->seeded_frame_sequence[frame.remote_id] = frame.sequence;
@@ -335,8 +336,7 @@ SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
if (metal_renderer && metal_renderer->IsActive()) {
const SurfaceState surface = impl_->CurrentState();
const double top_inset =
!surface.fullscreen && surface.tab_count > 1 ? 30.0 : 0.0;
const double top_inset = (*impl_->stream)->get_video_area_top();
const auto outcome = metal_renderer->RenderLatest(
frame.remote_id, top_inset, !surface.fullscreen);
if (outcome.result == VideoRenderer::RenderResult::rendered) {
@@ -442,6 +442,7 @@ SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
impl_->gl_pending_frame.swap(impl_->gl_conversion_frame);
impl_->gl_pending_size = {output_width, output_height};
impl_->gl_pending_dirty = true;
impl_->gl_pending_timing = frame.timing;
}
const VideoRenderSize output_size{output_width, output_height};
if (impl_->gl_image_size != output_size) {
@@ -471,6 +472,7 @@ SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
}
(*impl_->stream)->set_frame(slint::Image(std::move(pixels)));
frame.timing.MarkSubmitted();
(*impl_->stream)->set_has_frame(true);
(*impl_->stream)->set_receiving_text("");
result.width = frame.width;
@@ -1,5 +1,11 @@
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SLINT_VIDEO_PRESENTER_H_
#define _SLINT_VIDEO_PRESENTER_H_
#include <cstddef>
#include <cstdint>
@@ -8,7 +14,15 @@
#include <string>
#include <vector>
#include "crossdesk_ui.h"
#include "rendering/video_latency.h"
namespace slint {
template <typename T> class ComponentHandle;
}
namespace crossdesk::ui {
class StreamWindow;
}
namespace crossdesk {
@@ -22,7 +36,6 @@ public:
struct SurfaceState {
std::string selected_stream;
bool fullscreen = false;
size_t tab_count = 0;
};
struct Frame {
@@ -31,6 +44,7 @@ public:
int width = 0;
int height = 0;
uint64_t sequence = 0;
VideoLatencyFrame timing;
};
struct PresentResult {
@@ -72,4 +86,4 @@ private:
} // namespace crossdesk
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
#endif
@@ -0,0 +1,51 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-22
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _VIDEO_GEOMETRY_H_
#define _VIDEO_GEOMETRY_H_
#include <algorithm>
#include <cmath>
#include <optional>
namespace crossdesk {
struct VideoPoint {
float x;
float y;
};
// Area dimensions and pointer positions must use the same coordinate space.
// Slint supplies both in logical pixels; remote input is resolution
// independent.
inline std::optional<VideoPoint> MapVideoPoint(float x, float y,
float area_width,
float area_height,
int video_width,
int video_height) {
if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(area_width) ||
!std::isfinite(area_height) || area_width <= 0 || area_height <= 0 ||
video_width <= 0 || video_height <= 0) {
return std::nullopt;
}
const double scale =
std::min(static_cast<double>(area_width) / video_width,
static_cast<double>(area_height) / video_height);
const double width = video_width * scale;
const double height = video_height * scale;
const double left = (area_width - width) * 0.5;
const double top = (area_height - height) * 0.5;
if (x < left || x > left + width || y < top || y > top + height) {
return std::nullopt;
}
return VideoPoint{static_cast<float>((x - left) / width),
static_cast<float>((y - top) / height)};
}
} // namespace crossdesk
#endif
@@ -0,0 +1,110 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _VIDEO_LATENCY_H_
#define _VIDEO_LATENCY_H_
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <memory>
#include <mutex>
#include <optional>
namespace crossdesk {
class VideoLatencyStats;
// Travels with the exact queued pixels, including native frames. Its clock is
// translated once at the receive callback; rendering needs no live PeerPtr.
struct VideoLatencyFrame {
using Clock = std::chrono::steady_clock;
std::shared_ptr<VideoLatencyStats> stats;
uint64_t id = 0;
Clock::time_point capture_time{};
void MarkPresented(Clock::time_point now = Clock::now()) const;
// Fallback for renderers which expose submission but no presentation time.
void MarkSubmitted(Clock::time_point now = Clock::now()) const;
};
class VideoLatencyStats
: public std::enable_shared_from_this<VideoLatencyStats> {
public:
using Clock = VideoLatencyFrame::Clock;
struct Snapshot {
double average_ms;
size_t samples;
};
VideoLatencyFrame Frame(uint64_t captured_us, int64_t local_now_us,
Clock::time_point now = Clock::now()) {
if (captured_us == 0 || local_now_us <= 0 ||
captured_us > uint64_t(local_now_us) ||
uint64_t(local_now_us) - captured_us > 5'000'000)
return {};
std::lock_guard lock(mutex_);
return {shared_from_this(), ++issued_id_,
now - std::chrono::microseconds(local_now_us - captured_us)};
}
void Record(const VideoLatencyFrame& frame, Clock::time_point now) {
std::lock_guard lock(mutex_);
// Redraws, cached snapshots and late completions must not count twice.
if (frame.id <= last_id_) return;
last_id_ = frame.id;
const double ms =
std::chrono::duration<double, std::milli>(now - frame.capture_time)
.count();
if (ms < 0 || ms > 5000) return;
samples_.push_back({now, ms});
Prune(now);
}
std::optional<Snapshot> Get(Clock::time_point now = Clock::now()) {
std::lock_guard lock(mutex_);
Prune(now);
if (samples_.empty()) return std::nullopt;
double sum = 0;
for (const auto& sample : samples_) {
sum += sample.ms;
}
return Snapshot{sum / samples_.size(), samples_.size()};
}
void Reset() {
std::lock_guard lock(mutex_);
samples_.clear();
last_id_ = issued_id_; // Invalidate frames still held by a renderer.
}
private:
void Prune(Clock::time_point now) {
while (!samples_.empty() &&
(now - samples_.front().time > std::chrono::seconds(1) ||
samples_.size() > 240))
samples_.pop_front();
}
struct Sample {
Clock::time_point time;
double ms;
};
std::mutex mutex_;
uint64_t issued_id_ = 0;
uint64_t last_id_ = 0;
std::deque<Sample> samples_;
};
inline void VideoLatencyFrame::MarkSubmitted(Clock::time_point now) const {
MarkPresented(now);
}
inline void VideoLatencyFrame::MarkPresented(Clock::time_point now) const {
if (stats) stats->Record(*this, now);
}
} // namespace crossdesk
#endif
@@ -0,0 +1,59 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CONNECTION_LATENCY_H_
#define _CONNECTION_LATENCY_H_
#include <chrono>
#include <cmath>
#include <mutex>
#include <optional>
namespace crossdesk::gui_detail {
// Presentation-only smoothing. The transport's raw RTT remains available for
// recovery, congestion control and diagnostics. A missing one-second report
// does not flicker, but a disconnected/stalled peer cannot leave a stale value.
class ConnectionLatencyStats {
public:
using Clock = std::chrono::steady_clock;
void Update(double rtt_ms, Clock::time_point now = Clock::now()) {
if (!std::isfinite(rtt_ms) || rtt_ms < 0 || rtt_ms > 2000) return;
std::lock_guard lock(mutex_);
if (average_ms_ && now < updated_) return;
if (!Fresh(now)) {
average_ms_ = rtt_ms;
} else {
*average_ms_ += 0.25 * (rtt_ms - *average_ms_);
}
updated_ = now;
}
std::optional<double> Get(Clock::time_point now = Clock::now()) const {
std::lock_guard lock(mutex_);
return Fresh(now) ? average_ms_ : std::nullopt;
}
void Reset() {
std::lock_guard lock(mutex_);
average_ms_.reset();
}
private:
bool Fresh(Clock::time_point now) const {
// The UI may snapshot `now` just before the report thread updates us.
// A newer valid report must not cause a one-frame unavailable flicker.
return average_ms_ && now - updated_ < std::chrono::seconds(3);
}
mutable std::mutex mutex_;
std::optional<double> average_ms_;
Clock::time_point updated_{};
};
} // namespace crossdesk::gui_detail
#endif
@@ -6,6 +6,7 @@
#include <shared_mutex>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include <display_stream_id.h>
@@ -13,42 +14,49 @@
#include "platform.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
#include "runtime/device_presence_request.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
namespace {
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
constexpr auto kPresenceRefreshInterval = std::chrono::seconds(30);
constexpr auto kPresenceRetryInterval = std::chrono::seconds(1);
} // namespace
bool GuiRuntime::HasActiveSession() {
if (stream_window_inited_) return true;
std::shared_lock lock(connection_status_mutex_);
return std::any_of(connection_status_.begin(), connection_status_.end(),
[](const auto& entry) {
return entry.second == ConnectionStatus::Connecting ||
entry.second == ConnectionStatus::Connected;
});
}
void GuiRuntime::HandleConnectionStatusChange() {
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
if (!recent_connection_ids_.empty()) {
nlohmann::json j;
j["type"] = "recent_connections_presence";
j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array();
for (const auto& id : recent_connection_ids_) {
std::string pure_id = id;
size_t pos_y = pure_id.find('Y');
size_t pos_n = pure_id.find('N');
size_t pos = std::string::npos;
if (pos_y != std::string::npos &&
(pos_n == std::string::npos || pos_y < pos_n)) {
pos = pos_y;
} else if (pos_n != std::string::npos) {
pos = pos_n;
}
if (pos != std::string::npos) {
pure_id = pure_id.substr(0, pos);
}
j["devices"].push_back(pure_id);
}
auto s = j.dump();
SendSignalMessage(peer_, s.data(), s.size());
}
if (!signal_connected_ || !peer_) {
return;
}
const auto now = std::chrono::steady_clock::now();
if (now - last_presence_request_attempt_at_ < kPresenceRetryInterval) {
return;
}
// Consume the flag before sending so a concurrent reconnect cannot lose its
// refresh request. Keep it pending while disconnected or after a send error.
const bool requested = need_to_send_recent_connections_.exchange(false);
if (!requested && now < next_presence_refresh_at_) {
return;
}
last_presence_request_attempt_at_ = now;
const auto message =
BuildDevicePresenceRequest(client_id_, recent_connection_ids_, true).dump();
if (SendSignalMessage(peer_, message.data(), message.size()) == 0) {
next_presence_refresh_at_ = now + kPresenceRefreshInterval;
} else {
need_to_send_recent_connections_ = true;
}
need_to_send_recent_connections_ = false;
}
void GuiRuntime::HandlePendingPresenceProbe() {
@@ -123,36 +131,41 @@ void GuiRuntime::HandlePresenceProbeTimeout() {
void GuiRuntime::HandleServerControllerDisconnected(
const std::string& remote_id, const char* reason) {
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
#ifdef _WIN32
devices_.ReleaseRemoteMouseButtons();
#endif
{
std::lock_guard lock(remote_pointer_input_mutex_);
last_remote_pointer_input_time_.erase(remote_id);
}
bool has_connected_controller = false;
bool has_web_controller = false;
std::string remaining_controller_id;
{
std::unique_lock lock(connection_status_mutex_);
connection_status_.erase(remote_id);
devices_.OnControllerConnectionsChanged();
connection_host_names_.erase(remote_id);
privacy_sessions_.Disconnected(remote_id);
for (const auto& [id, status] : connection_status_) {
if (status != ConnectionStatus::Connected) {
continue;
}
has_connected_controller = true;
has_web_controller =
has_web_controller || id.find("web") != std::string::npos;
if (remaining_controller_id.empty()) {
remaining_controller_id = id;
}
}
}
show_cursor_ = has_web_controller;
if (has_connected_controller) {
remote_client_id_ = remaining_controller_id;
return;
}
LOG_INFO("Last remote controller disconnected; recovering privacy, reason={}",
reason ? reason : "unknown");
privacy_.Disconnected();
need_to_create_server_window_.store(false, std::memory_order_release);
need_to_destroy_server_window_.store(true, std::memory_order_release);
is_server_mode_ = false;
@@ -170,14 +183,11 @@ void GuiRuntime::HandleServerControllerDisconnected(
#else
start_screen_capturer_ = false;
#endif
start_speaker_capturer_ = false;
devices_.StopSpeakerCapturer();
start_mouse_controller_ = false;
start_keyboard_capturer_ = false;
remote_client_id_.clear();
if (audio_capture_) {
devices_.StopSpeakerCapturer();
audio_capture_ = false;
}
audio_capture_ = false;
devices_.ResetToInitialDisplay();
}
@@ -199,11 +209,9 @@ int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
pending_presence_remember_password_ = remember_password;
}
nlohmann::json j;
j["type"] = "recent_connections_presence";
j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array({remote_id});
auto s = j.dump();
const auto s = BuildDevicePresenceRequest(
client_id_, recent_connection_ids_, false, remote_id)
.dump();
int ret = SendSignalMessage(peer_, s.data(), s.size());
if (ret != 0) {
@@ -220,6 +228,11 @@ int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
}
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
if (!props || props->closing_.exchange(true)) return;
if (props->peer_events_) props->peer_events_->Deactivate();
props->connection_established_ = false;
props->streaming_ = false;
props->connection_status_.store(ConnectionStatus::Closed);
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
int video_width = 0;
int video_height = 0;
@@ -245,91 +258,170 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
}
}
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
video_height > 0) {
std::vector<unsigned char> buffer_copy(*frame_snapshot);
std::string remote_id = props->remote_id_;
std::string remote_host_name = props->remote_host_name_;
std::string password =
props->remember_password_ ? props->remote_password_ : "";
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
remote_host_name, password,
thumbnail = thumbnail_]() {
thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width,
video_height, remote_id, remote_host_name,
password);
});
{
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
thumbnail_save_threads_.emplace_back(std::move(save_thread));
}
}
if (native_renderer) {
native_renderer->DiscardStream(props->remote_id_);
video_frame_dirty_.store(true, std::memory_order_release);
}
if (props->peer_) {
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
LeaveConnection(props->peer_, props->remote_id_.c_str());
LOG_INFO("Destroy peer [{}]", props->local_id_);
DestroyPeer(&props->peer_);
PeerPtr* peer = nullptr;
{
// Audio/clipboard senders hold a shared map lock while using this pointer.
std::unique_lock lock(remote_sessions_mutex_);
peer = std::exchange(props->peer_, nullptr);
}
const auto queued_at = std::chrono::steady_clock::now();
session_cleanup_tasks_[props->remote_id_] = session_cleanup_queue_.PostTask(
[props, peer, frame_snapshot, video_width, video_height,
thumbnail = thumbnail_, queued_at]() mutable {
if (peer) {
LOG_INFO("[{}] Background leave connection [{}]", props->local_id_,
props->remote_id_);
LeaveConnection(peer, props->remote_id_.c_str());
DestroyPeer(&peer);
}
if (thumbnail && frame_snapshot && !frame_snapshot->empty() &&
video_width > 0 && video_height > 0) {
thumbnail->SaveToThumbnail(
reinterpret_cast<char*>(frame_snapshot->data()), video_width,
video_height, props->remote_id_, props->remote_host_name_,
props->remember_password_ ? props->remote_password_ : "");
}
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - queued_at).count();
LOG_INFO("[{}] Session cleanup completed in {} ms", props->remote_id_,
elapsed);
});
}
void GuiRuntime::CloseServerController(const std::string& remote_id) {
if (!peer_ || remote_id.empty() || controller_cleanup_tasks_.count(remote_id))
return;
controller_cleanup_tasks_[remote_id] = session_cleanup_queue_.PostTask(
[peer = peer_, remote_id] { LeaveConnection(peer, remote_id.c_str()); });
}
void GuiRuntime::CloseConnectionPeer() {
if (!peer_) return;
peer_events_->Deactivate();
std::vector<std::string> controllers;
{
std::shared_lock lock(connection_status_mutex_);
for (const auto& [id, status] : connection_status_) controllers.push_back(id);
}
for (const auto& id : controllers)
HandleServerControllerDisconnected(id, "peer_closed");
devices_.StopMouseController();
devices_.StopScreenCapturer();
devices_.StopSpeakerCapturer();
devices_.StopKeyboardCapturer();
signal_connected_ = false;
device_presence_cache_.SetSignalConnected(false);
signal_status_ = SignalStatus::SignalClosed;
PeerPtr* peer;
{
std::unique_lock lock(remote_sessions_mutex_);
peer = std::exchange(peer_, nullptr);
}
// The same serial queue also owns outstanding controller disconnects, so
// these finish before the shared peer is destroyed.
connection_peer_cleanup_ = session_cleanup_queue_.PostTask(
[peer, handler = peer_events_, id = std::string(client_id_)]() mutable {
LOG_INFO("[{}] Background connection peer cleanup", id);
LeaveConnection(peer, id.c_str());
DestroyPeer(&peer);
});
}
void GuiRuntime::CloseAllRemoteSessions() {
if (peer_) {
LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_);
LeaveConnection(peer_, client_id_);
is_client_mode_ = false;
devices_.StopMouseController();
devices_.StopScreenCapturer();
devices_.StopSpeakerCapturer();
devices_.StopKeyboardCapturer();
LOG_INFO("Destroy peer [{}]", client_id_);
DestroyPeer(&peer_);
}
CloseConnectionPeer();
is_client_mode_ = false;
pending_reconnects_.clear();
std::vector<std::shared_ptr<RemoteSession>> sessions;
{
// std::shared_lock lock(remote_sessions_mutex_);
for (auto& it : remote_sessions_) {
auto props = it.second;
CloseRemoteSession(props);
}
std::shared_lock lock(remote_sessions_mutex_);
for (const auto& [id, props] : remote_sessions_) sessions.push_back(props);
}
for (const auto& props : sessions) CloseRemoteSession(props);
{
// std::unique_lock lock(remote_sessions_mutex_);
std::unique_lock lock(remote_sessions_mutex_);
remote_sessions_.clear();
}
}
void GuiRuntime::WaitForThumbnailSaveTasks() {
std::vector<std::thread> threads_to_join;
{
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
threads_to_join.swap(thumbnail_save_threads_);
void GuiRuntime::HandleSessionCleanup() {
auto completed = [](std::future<void>& task) {
if (!task.valid() || task.wait_for(std::chrono::seconds(0)) !=
std::future_status::ready) return false;
try {
task.get();
} catch (const std::exception& error) {
LOG_ERROR("Connection cleanup failed: {}", error.what());
}
return true;
};
completed(connection_peer_cleanup_);
for (auto it = controller_cleanup_tasks_.begin();
it != controller_cleanup_tasks_.end();) {
if (completed(it->second)) it = controller_cleanup_tasks_.erase(it);
else ++it;
}
if (threads_to_join.empty()) {
return;
std::vector<std::pair<std::string, PendingReconnect>> reconnects;
for (auto it = session_cleanup_tasks_.begin();
it != session_cleanup_tasks_.end();) {
if (it->second.wait_for(std::chrono::seconds(0)) !=
std::future_status::ready) {
++it;
continue;
}
try {
it->second.get();
} catch (const std::exception& error) {
LOG_ERROR("[{}] Session cleanup failed: {}", it->first, error.what());
}
if (auto pending = pending_reconnects_.find(it->first);
pending != pending_reconnects_.end()) {
reconnects.emplace_back(pending->first, std::move(pending->second));
pending_reconnects_.erase(pending);
}
it = session_cleanup_tasks_.erase(it);
reload_recent_connections_ = true;
recent_connection_image_save_time_ = 0;
}
for (const auto& [id, request] : reconnects) {
ConnectTo(id, request.password.c_str(), request.remember_password);
}
}
for (auto& thread : threads_to_join) {
if (thread.joinable()) {
thread.join();
void GuiRuntime::WaitForSessionCleanup() {
pending_reconnects_.clear();
auto drain = [](std::future<void>& task) {
if (!task.valid()) return;
try {
task.get();
} catch (const std::exception& error) {
LOG_ERROR("Connection cleanup failed: {}", error.what());
}
};
drain(connection_peer_cleanup_);
for (auto& [id, task] : controller_cleanup_tasks_) drain(task);
controller_cleanup_tasks_.clear();
for (auto& [id, task] : session_cleanup_tasks_) {
try {
task.get();
} catch (const std::exception& error) {
LOG_ERROR("[{}] Session cleanup failed: {}", id, error.what());
}
}
session_cleanup_tasks_.clear();
}
void GuiRuntime::ResetRemoteSessionResources(
std::shared_ptr<RemoteSession> props) {
props->net_traffic_stats_.Reset();
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->video_latency_->Reset();
props->video_frame_timing_ = {};
props->front_frame_.reset();
props->back_frame_.reset();
props->thumbnail_frame_.reset();
@@ -346,6 +438,7 @@ void GuiRuntime::ResetRemoteSessionResources(
std::lock_guard<std::mutex> lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
props->cursor_presentation_ = {};
}
}
@@ -365,6 +458,11 @@ std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check) {
if (session_cleanup_tasks_.count(remote_id)) {
pending_reconnects_[remote_id] = {password ? password : "", remember_password};
LOG_INFO("[{}] Reconnect queued until previous peer is destroyed", remote_id);
return 0;
}
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
int ret =
RequestSingleDevicePresence(remote_id, password, remember_password);
@@ -382,16 +480,49 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
LOG_INFO("Connect to [{}]", remote_id);
focused_remote_id_ = remote_id;
// std::shared_lock shared_lock(remote_sessions_mutex_);
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
// shared_lock.unlock();
auto stale = FindRemoteSession(remote_id);
bool exists = stale != nullptr;
if (stale) {
// A failed CreatePeer or a completed close can leave a session record
// without a reusable peer. Recreate it instead of leaving the UI in
// Connecting forever while JoinConnection has nothing to call.
if (stale->peer_ == nullptr) {
std::unique_lock lock(remote_sessions_mutex_);
auto it = remote_sessions_.find(remote_id);
if (it != remote_sessions_.end() && it->second == stale) {
remote_sessions_.erase(it);
exists = false;
LOG_INFO("[{}] Recreating stale remote session before connect",
remote_id);
}
}
if (exists && !stale->closing_.load() &&
!stale->connection_established_) {
const auto status = stale->connection_status_.load();
if (status == ConnectionStatus::Disconnected ||
status == ConnectionStatus::Failed ||
status == ConnectionStatus::Closed) {
// The recreated session starts with an empty password; callers that
// pass none expect the one stored by the previous attempt to be reused.
const bool keep_stored_password = !password || !*password;
pending_reconnects_[remote_id] = {
keep_stored_password ? std::string(stale->remote_password_)
: std::string(password),
remember_password};
CloseRemoteSession(stale);
LOG_INFO("[{}] Reconnecting after terminal peer status {}", remote_id,
static_cast<int>(status));
return 0;
}
}
}
if (!exists) {
PeerPtr* peer_to_init = nullptr;
std::string local_id;
{
// std::unique_lock unique_lock(remote_sessions_mutex_);
std::unique_lock unique_lock(remote_sessions_mutex_);
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
auto props = remote_sessions_[remote_id];
@@ -399,15 +530,10 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
props->remote_id_ = remote_id;
memcpy(&props->params_, &params_, sizeof(Params));
props->params_.user_id = props->local_id_.c_str();
props->peer_events_ = std::make_shared<PeerEventHandler>(*this);
props->params_.user_data = props->peer_events_.get();
props->peer_ = CreatePeer(&props->params_);
props->control_window_width_ = title_bar_height_ * 10.0f;
props->control_window_height_ = title_bar_height_ * 1.3f;
props->control_window_min_width_ = title_bar_height_ * 0.65f;
props->control_window_min_height_ = title_bar_height_ * 1.3f;
props->control_window_max_width_ = title_bar_height_ * 10.0f;
props->control_window_max_height_ = title_bar_height_ * 7.0f;
props->connection_status_.store(ConnectionStatus::Connecting);
show_connection_status_window_ = true;
@@ -423,7 +549,7 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
}
AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str(), false);
AddDataStream(props->peer_, props->mouse_label_.c_str(), false);
AddDataStream(props->peer_, props->mouse_label_.c_str(), true);
AddDataStream(props->peer_, props->keyboard_label_.c_str(), true);
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
AddDataStream(props->peer_, props->file_label_.c_str(), true);
@@ -445,8 +571,8 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
}
int ret = -1;
// std::shared_lock read_lock(remote_sessions_mutex_);
auto props = remote_sessions_[remote_id];
auto props = FindRemoteSession(remote_id);
if (!props || props->closing_) return -1;
if (!props->connection_established_) {
props->connection_status_.store(ConnectionStatus::Connecting);
show_connection_status_window_ = true;
@@ -470,7 +596,6 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
}
}
}
// read_lock.unlock();
return 0;
}
@@ -1,3 +1,9 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CURSOR_POSITION_H_
#define _CURSOR_POSITION_H_
@@ -64,4 +70,4 @@ inline bool NormalizeCursorPosition(
} // namespace crossdesk
#endif
#endif
@@ -0,0 +1,77 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-16
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CURSOR_PRESENTATION_H_
#define _CURSOR_PRESENTATION_H_
#include <remote_action.h>
#include <cstdint>
#include <optional>
namespace crossdesk {
// All times use one monotonic millisecond clock. The owning session's cursor
// mutex protects this policy from the transport and UI threads.
class CursorPresentation {
public:
static constexpr uint64_t kStateTimeoutMs = 1500; // Three 500ms heartbeats.
static constexpr uint64_t kInputActivityMs = 1500;
void Observe(const CursorState& state, uint64_t now) {
if (state.render_mode == CursorRenderMode::unknown) {
// Repeated unavailable samples must not extend the recovery deadline.
if (!sample_unavailable_) stale_at_ = now + kStateTimeoutMs;
sample_unavailable_ = true;
return;
}
sample_unavailable_ = false;
stale_at_ = now + kStateTimeoutMs;
const bool separate =
state.render_mode == CursorRenderMode::separate ||
(state.render_mode == CursorRenderMode::legacy && state.visible);
// Old senders cannot distinguish embedded from hidden. Only explicit
// metadata from a new sender can establish the no-device compatibility
// case.
no_device_ = state.render_mode == CursorRenderMode::hidden &&
state.hidden_reason == CursorHiddenReason::no_pointing_device;
held_shape_ = separate ? state.shape : RemoteCursorShape::none;
if (held_shape_ != RemoteCursorShape::none) {
last_visible_shape_ = held_shape_;
}
if (!no_device_) fallback_engaged_ = false;
}
void NoteInput(uint64_t now) { last_input_ = now; }
RemoteCursorShape Resolve(uint64_t now) {
if (!stale_at_) return RemoteCursorShape::default_cursor;
const bool recent_input =
last_input_ && now - *last_input_ < kInputActivityMs;
const bool can_recover =
now >= *stale_at_ || (!sample_unavailable_ && no_device_);
if (can_recover && recent_input) {
fallback_engaged_ = true;
}
// Once needed, retain the locator while idle until a good non-compatibility
// sample arrives. Otherwise it would blink off whenever input pauses.
return fallback_engaged_ ? last_visible_shape_ : held_shape_;
}
private:
bool sample_unavailable_ = false;
bool no_device_ = false;
bool fallback_engaged_ = false;
std::optional<uint64_t> stale_at_;
std::optional<uint64_t> last_input_;
RemoteCursorShape held_shape_ = RemoteCursorShape::default_cursor;
RemoteCursorShape last_visible_shape_ = RemoteCursorShape::default_cursor;
};
} // namespace crossdesk
#endif
@@ -6,39 +6,17 @@
#include <windows.h>
#include "platform/windows/input/windows_cursor_state.h"
#include "platform/windows/screen_capturer/captured_cursor_state.h"
#include "platform/windows/screen_capturer/dxgi_cursor_state.h"
#include "platform/windows/screen_capturer/secure_desktop_cursor_state.h"
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
bool IsSystemCursor(HCURSOR cursor, LPCWSTR resource) {
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
}
RemoteCursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
if (IsSystemCursor(cursor, IDC_HELP)) return RemoteCursorShape::help;
if (IsSystemCursor(cursor, IDC_HAND)) return RemoteCursorShape::pointer;
if (IsSystemCursor(cursor, IDC_APPSTARTING))
return RemoteCursorShape::progress;
if (IsSystemCursor(cursor, IDC_WAIT)) return RemoteCursorShape::wait;
if (IsSystemCursor(cursor, IDC_CROSS)) return RemoteCursorShape::crosshair;
if (IsSystemCursor(cursor, IDC_IBEAM)) return RemoteCursorShape::text;
if (IsSystemCursor(cursor, IDC_NO))
return RemoteCursorShape::not_allowed;
if (IsSystemCursor(cursor, IDC_SIZEALL)) return RemoteCursorShape::move;
if (IsSystemCursor(cursor, IDC_SIZEWE)) return RemoteCursorShape::ew_resize;
if (IsSystemCursor(cursor, IDC_SIZENS)) return RemoteCursorShape::ns_resize;
if (IsSystemCursor(cursor, IDC_SIZENESW))
return RemoteCursorShape::nesw_resize;
if (IsSystemCursor(cursor, IDC_SIZENWSE))
return RemoteCursorShape::nwse_resize;
if (IsSystemCursor(cursor, IDC_UPARROW)) return RemoteCursorShape::n_resize;
return RemoteCursorShape::default_cursor;
}
} // namespace
struct CursorStateProvider::Impl {};
struct CursorStateProvider::Impl {
WindowsCursorState cursor;
};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
@@ -47,16 +25,39 @@ bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state) return false;
SecureDesktopCursorSnapshot secure_cursor{};
if (SharedSecureDesktopCursorState().Get(&secure_cursor)) {
if (!secure_cursor.valid) {
state->hidden_reason = CursorHiddenReason::secure_desktop_pending;
return false;
}
state->seq = 0;
NormalizeCursorPosition(secure_cursor.x, secure_cursor.y, displays,
preferred_display, state);
state->visible = secure_cursor.visible != 0;
state->render_mode = static_cast<CursorRenderMode>(secure_cursor.render_mode);
state->hidden_reason =
static_cast<CursorHiddenReason>(secure_cursor.hidden_reason);
state->shape = state->visible
? static_cast<RemoteCursorShape>(secure_cursor.shape)
: RemoteCursorShape::none;
return true;
}
CURSORINFO info{};
info.cbSize = sizeof(info);
if (!GetCursorInfo(&info)) return false;
state->seq = 0;
state->visible = (info.flags & CURSOR_SHOWING) != 0;
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
: RemoteCursorShape::none;
NormalizeCursorPosition(info.ptScreenPos.x, info.ptScreenPos.y, displays,
preferred_display, state);
void* cursor_monitor = state->position_valid
? displays[state->display_id].handle
: nullptr;
const bool embedded = SharedCapturedCursorState().IsEmbedded(cursor_monitor) ||
((info.flags & CURSOR_SHOWING) != 0 &&
!SharedDxgiCursorState().ShouldDrawCursor(true, cursor_monitor));
impl_->cursor.Sample(info, embedded, state);
return true;
}
@@ -1,3 +1,9 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CURSOR_STATE_PROVIDER_H_
#define _CURSOR_STATE_PROVIDER_H_
@@ -30,4 +36,4 @@ class CursorStateProvider {
} // namespace crossdesk
#endif
#endif
@@ -1,12 +1,13 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-28
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#ifndef _DEVICE_PRESENCE_CACHE_H_
#define _DEVICE_PRESENCE_CACHE_H_
#include <chrono>
#include <mutex>
#include <string>
#include <unordered_map>
@@ -15,15 +16,29 @@ namespace crossdesk {
class DevicePresenceCache {
public:
void SetOnline(const std::string &device_id, bool online) {
using Clock = std::chrono::steady_clock;
static constexpr auto kMaxAge = std::chrono::seconds(60);
void SetSignalConnected(bool connected) {
std::lock_guard<std::mutex> lock(mutex_);
cache_[device_id] = online;
signal_connected_ = connected;
cache_.clear();
}
bool IsOnline(const std::string &device_id) const {
void SetOnline(const std::string &device_id, bool online,
Clock::time_point now = Clock::now()) {
std::lock_guard<std::mutex> lock(mutex_);
if (signal_connected_) {
cache_[device_id] = {online, now};
}
}
bool IsOnline(const std::string &device_id,
Clock::time_point now = Clock::now()) const {
std::lock_guard<std::mutex> lock(mutex_);
const auto it = cache_.find(device_id);
return it != cache_.end() && it->second;
return signal_connected_ && it != cache_.end() && it->second.online &&
now - it->second.updated_at < kMaxAge;
}
void Clear() {
@@ -32,10 +47,15 @@ public:
}
private:
std::unordered_map<std::string, bool> cache_;
struct Entry {
bool online;
Clock::time_point updated_at;
};
bool signal_connected_ = false;
std::unordered_map<std::string, Entry> cache_;
mutable std::mutex mutex_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#endif
@@ -0,0 +1,43 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DEVICE_PRESENCE_REQUEST_H_
#define _DEVICE_PRESENCE_REQUEST_H_
#include <algorithm>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace crossdesk {
inline nlohmann::json BuildDevicePresenceRequest(
const std::string& client_id, const std::vector<std::string>& recent_ids,
bool subscribe, const std::string& probe_id = {}) {
std::vector<std::string> devices;
for (const auto& id : recent_ids) {
// Recent connection keys contain Y/N plus display settings after the ID.
const std::string pure_id = id.substr(0, id.find_first_of("YN"));
if (!pure_id.empty() &&
std::find(devices.begin(), devices.end(), pure_id) == devices.end()) {
devices.push_back(pure_id);
}
}
if (!probe_id.empty() &&
std::find(devices.begin(), devices.end(), probe_id) == devices.end()) {
devices.push_back(probe_id);
}
// Include recent devices even in a one-shot probe: older servers ignore
// subscribe=false and replace their subscriptions with this device list.
return {{"type", "recent_connections_presence"},
{"user_id", client_id},
{"devices", devices},
{"subscribe", subscribe}};
}
} // namespace crossdesk
#endif
+52 -93
View File
@@ -1,5 +1,7 @@
#include "runtime/gui_runtime.h"
#include <SDL3/SDL_timer.h>
#include <algorithm>
#include <chrono>
#include <cstring>
@@ -14,26 +16,33 @@
#include "localization.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
#ifdef __linux__
#include "platform/linux/headless/headless_console.h"
#endif
namespace crossdesk {
GuiRuntime::GuiRuntime()
: clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this),
keyboard_(*this), peer_events_(*this) {}
keyboard_(*this) {}
GuiRuntime::~GuiRuntime() = default;
GuiRuntime::~GuiRuntime() { WaitForSessionCleanup(); }
int GuiRuntime::CreateConnectionPeer() {
// Tick retries after the old peer's leave notification and socket shutdown.
if (connection_peer_cleanup_.valid()) return 0;
{
std::unique_lock lock(remote_sessions_mutex_);
peer_events_ = std::make_shared<PeerEventHandler>(*this);
}
params_.use_cfg_file = false;
std::string signal_server_ip;
int signal_server_port;
int coturn_server_port;
if (config_center_->IsSelfHosted()) {
signal_server_ip = config_center_->GetSignalServerHost();
signal_server_port = config_center_->GetSignalServerPort();
coturn_server_port = config_center_->GetCoturnServerPort();
std::string current_self_hosted_ip = config_center_->GetSignalServerHost();
const bool use_cached_id = settings_.LoadCachedSelfHostedIdentity();
@@ -58,7 +67,6 @@ int GuiRuntime::CreateConnectionPeer() {
} else {
signal_server_ip = config_center_->GetDefaultServerHost();
signal_server_port = config_center_->GetDefaultSignalServerPort();
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
settings_.ActivateCachedPublicIdentity();
params_.user_id = client_id_with_password_;
}
@@ -89,6 +97,11 @@ int GuiRuntime::CreateConnectionPeer() {
const char *active_identity =
self_hosted ? self_hosted_user_id_ : client_id_with_password_;
#ifdef __linux__
// Publish the committed identity; an unconfirmed recovery credential must
// not be advertised as the current connection password.
HeadlessConsole::Instance().SetIdentity(active_identity);
#endif
const std::string login_identity =
try_pending_identity ? pending_identity : std::string(active_identity);
std::memset(connection_login_identity_, 0,
@@ -109,41 +122,23 @@ int GuiRuntime::CreateConnectionPeer() {
} else {
signal_server_port_self_[0] = '\0';
}
int coturn_port = config_center_->GetCoturnServerPort();
if (coturn_port > 0) {
strncpy(coturn_server_port_self_, std::to_string(coturn_port).c_str(),
sizeof(coturn_server_port_self_) - 1);
coturn_server_port_self_[sizeof(coturn_server_port_self_) - 1] = '\0';
} else {
coturn_server_port_self_[0] = '\0';
}
// peer config
strncpy((char *)params_.signal_server_ip, signal_server_ip.c_str(),
sizeof(params_.signal_server_ip) - 1);
params_.signal_server_ip[sizeof(params_.signal_server_ip) - 1] = '\0';
params_.signal_server_port = signal_server_port;
strncpy((char *)params_.stun_server_ip, signal_server_ip.c_str(),
sizeof(params_.stun_server_ip) - 1);
params_.stun_server_ip[sizeof(params_.stun_server_ip) - 1] = '\0';
params_.stun_server_port = coturn_server_port;
strncpy((char *)params_.turn_server_ip, signal_server_ip.c_str(),
sizeof(params_.turn_server_ip) - 1);
params_.turn_server_ip[sizeof(params_.turn_server_ip) - 1] = '\0';
params_.turn_server_port = coturn_server_port;
// TURN credentials are issued by the signaling server after login. Keep the
// initial values empty so a reusable static password is never embedded in
// the client binary.
params_.turn_server_username[0] = '\0';
params_.turn_server_password[0] = '\0';
strncpy(params_.log_path, dll_log_path_.c_str(),
sizeof(params_.log_path) - 1);
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
// The Slint desktop renderer currently consumes packed CPU frames. Native
// renderers can opt into platform-native frame output independently.
#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__)
// Windows and Linux retain pooled CPU NV12 frames or CUDA device frames;
// macOS retains VideoToolbox CVPixelBuffers for direct Metal sampling. All
// platforms fall back to a packed CPU copy when native upload is unavailable.
params_.native_video_output = true;
#else
params_.native_video_output = false;
#endif
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
? true
@@ -151,28 +146,12 @@ int GuiRuntime::CreateConnectionPeer() {
params_.turn_mode = static_cast<TurnMode>(config_center_->GetTurnMode());
params_.enable_srtp = config_center_->IsEnableSrtp();
params_.video_content_type = VideoContentType::ScreenContent;
params_.video_quality =
static_cast<VideoQuality>(config_center_->GetVideoQuality());
params_.video_frame_rate =
config_center_->GetVideoFrameRate() ==
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
? 30
: 60;
switch (config_center_->GetVideoAdaptationPolicy()) {
case ConfigCenter::VIDEO_ADAPTATION_POLICY::FRAME_RATE_PRIORITY:
params_.video_degradation_preference =
VideoDegradationPreference::MaintainFrameRate;
break;
case ConfigCenter::VIDEO_ADAPTATION_POLICY::BALANCED:
params_.video_degradation_preference =
VideoDegradationPreference::Balanced;
break;
case ConfigCenter::VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY:
default:
params_.video_degradation_preference =
VideoDegradationPreference::MaintainResolution;
break;
}
// Bootstrap outgoing media with fixed defaults. Live overrides come only
// from the authorized controller and apply to its connection on this host.
params_.video_quality = VideoQuality::QualityHigh;
params_.video_frame_rate = 60;
params_.video_degradation_preference =
VideoDegradationPreference::MaintainResolution;
params_.on_receive_video_buffer = nullptr;
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
@@ -184,14 +163,19 @@ int GuiRuntime::CreateConnectionPeer() {
params_.on_connection_status = PeerEventHandler::OnConnectionStatus;
params_.on_net_status_report = PeerEventHandler::OnNetStatusReport;
params_.user_data = &peer_events_;
params_.user_data = peer_events_.get();
// The previous peer may have left a terminal status behind. Reset it before
// Init() starts emitting callbacks for the newly selected server.
signal_connected_ = false;
device_presence_cache_.SetSignalConnected(false);
need_to_send_recent_connections_ = true;
signal_status_ = SignalStatus::SignalConnecting;
peer_ = CreatePeer(&params_);
{
std::unique_lock lock(remote_sessions_mutex_);
peer_ = CreatePeer(&params_);
}
if (peer_) {
LOG_INFO("Create peer instance [{}] successful", client_id_);
Init(peer_);
@@ -209,7 +193,7 @@ int GuiRuntime::CreateConnectionPeer() {
AddAudioStream(peer_, audio_label_.c_str());
AddDataStream(peer_, data_label_.c_str(), false);
AddDataStream(peer_, mouse_label_.c_str(), false);
AddDataStream(peer_, mouse_label_.c_str(), true);
AddDataStream(peer_, keyboard_label_.c_str(), true);
AddDataStream(peer_, control_data_label_.c_str(), true);
AddDataStream(peer_, file_label_.c_str(), true);
@@ -221,21 +205,8 @@ int GuiRuntime::CreateConnectionPeer() {
}
}
void GuiRuntime::UpdateLabels() {
if (!label_inited_ ||
localization_language_index_last_ != localization_language_index_) {
connect_button_label_ =
connect_button_pressed_
? localization::disconnect[localization_language_index_]
: localization::connect[localization_language_index_];
label_inited_ = true;
localization_language_index_last_ = localization_language_index_;
}
}
void GuiRuntime::HandleRecentConnections() {
if (reload_recent_connections_ && thumbnail_) {
if (session_cleanup_tasks_.empty() && reload_recent_connections_ && thumbnail_) {
uint32_t now_time = SDL_GetTicks();
if (now_time - recent_connection_image_save_time_ >= 50) {
int ret = thumbnail_->LoadThumbnail(recent_connections_,
@@ -264,11 +235,22 @@ void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
if (1) {
std::shared_lock lock(runtime->remote_sessions_mutex_);
uint64_t captured_timestamp = 0;
for (const auto &it : runtime->remote_sessions_) {
auto props = it.second;
if (props->connection_status_.load() == ConnectionStatus::Connected) {
if (props->peer_) {
SendAudioFrame(props->peer_, (const char *)stream, len,
if (captured_timestamp == 0) {
const int64_t timestamp_us = GetSystemTimeMicros(props->peer_);
if (timestamp_us > 0) {
captured_timestamp = static_cast<uint64_t>(timestamp_us);
}
}
MiniRtcAudioFrame frame{};
frame.data = reinterpret_cast<const char *>(stream);
frame.size = static_cast<size_t>(len);
frame.captured_timestamp = captured_timestamp;
SendAudioFrame(props->peer_, &frame,
runtime->audio_label_.c_str());
}
}
@@ -285,29 +267,6 @@ void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
void GuiRuntime::SdlCaptureAudioOut([[maybe_unused]] void *userdata,
[[maybe_unused]] Uint8 *stream,
[[maybe_unused]] int len) {
// GuiApplication *runtime = (GuiApplication *)userdata;
// for (auto it : runtime->remote_sessions_) {
// auto props = it.second;
// if (props->connection_status_ == SignalStatus::SignalConnected) {
// SendAudioFrame(props->peer_, (const char *)stream, len);
// }
// }
// if (!runtime->audio_buffer_fresh_) {
// return;
// }
// SDL_memset(stream, 0, len);
// if (runtime->audio_len_ == 0) {
// return;
// } else {
// }
// len = (len > runtime->audio_len_ ? runtime->audio_len_ : len);
// SDL_MixAudioFormat(stream, runtime->audio_buffer_, AUDIO_S16LSB, len,
// SDL_MIX_MAXVOLUME);
// runtime->audio_buffer_fresh_ = false;
}
} // namespace crossdesk
+35 -9
View File
@@ -1,7 +1,15 @@
#ifndef CROSSDESK_GUI_RUNTIME_H_
#define CROSSDESK_GUI_RUNTIME_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _GUI_RUNTIME_H_
#define _GUI_RUNTIME_H_
#include <atomic>
#include <cstdint>
#include <deque>
#include <memory>
#include <string>
@@ -10,15 +18,17 @@
#include "features/file_transfer/file_transfer_manager.h"
#include "features/input/keyboard_controller.h"
#include "features/settings/settings_manager.h"
#include "privacy_controller.h"
#include "runtime/gui_state.h"
#include "runtime/peer_event_handler.h"
#include "runtime/privacy_session_policy.h"
namespace crossdesk {
class VideoRenderer;
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
// state, but no window lifecycle, ImGui view, or transport callback methods.
// state, but no window lifecycle, UI view, or transport callback methods.
class GuiRuntime : protected gui_detail::GuiState {
protected:
using FileTransferState = gui_detail::FileTransferState;
@@ -35,8 +45,8 @@ class GuiRuntime : protected gui_detail::GuiState {
GuiRuntime();
~GuiRuntime();
static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
static void SdlCaptureAudioOut(void* userdata, Uint8* stream, int len);
static void SdlCaptureAudioIn(void* userdata, uint8_t* stream, int len);
static void SdlCaptureAudioOut(void* userdata, uint8_t* stream, int len);
int CreateConnectionPeer();
int ConnectTo(const std::string& remote_id, const char* password,
@@ -44,19 +54,29 @@ class GuiRuntime : protected gui_detail::GuiState {
int RequestSingleDevicePresence(const std::string& remote_id,
const char* password, bool remember_password);
void UpdateLabels();
void HandleRecentConnections();
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandlePresenceProbeTimeout();
bool HasActiveSession();
void HandleServerControllerDisconnected(const std::string& remote_id,
const char* reason);
void HandleWindowsServiceIntegration();
void HandlePrivacy();
bool IsAuthorizedController(const std::string& remote_id);
void SetControllerPrivacySupport(const std::string& remote_id, bool supported);
// Caller holds connection_status_mutex_ exclusively. Turns privacy off when
// any connected controller lacks support, or on for the first automatic one.
void ApplyPrivacyAdmission(bool automatic_enable);
void QueuePrivacyCommand(const std::string& remote_id, const PrivacyCommand& command);
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions();
void CloseConnectionPeer();
void CloseServerController(const std::string& remote_id);
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
void WaitForThumbnailSaveTasks();
void HandleSessionCleanup();
void WaitForSessionCleanup();
std::shared_ptr<RemoteSession> FindRemoteSession(
const std::string& remote_id);
@@ -78,12 +98,18 @@ class GuiRuntime : protected gui_detail::GuiState {
bool EnsureMacAccessibilityPermission();
#endif
PrivacyController privacy_;
PrivacySessionPolicy privacy_sessions_;
std::mutex privacy_commands_mutex_;
std::deque<std::pair<std::string, PrivacyCommand>> privacy_commands_;
uint32_t last_privacy_revision_ = 0;
uint64_t last_privacy_status_tick_ = 0;
ClipboardController clipboard_;
SessionDeviceManager devices_;
FileTransferManager transfers_;
SettingsManager settings_;
KeyboardController keyboard_;
PeerEventHandler peer_events_;
std::shared_ptr<PeerEventHandler> peer_events_;
std::atomic<bool> video_frame_dirty_{false};
std::unique_ptr<VideoRenderer> video_renderer_;
@@ -98,4 +124,4 @@ class GuiRuntime : protected gui_detail::GuiState {
} // namespace crossdesk
#endif // CROSSDESK_GUI_RUNTIME_H_
#endif
+6 -8
View File
@@ -1,13 +1,11 @@
/*
* Aggregate state visible to GuiRuntime and GuiApplication.
*
* Definitions live beside their owners: SDL/window state in application and
* connection/session state in runtime. This header is intentionally only the
* composition point.
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_GUI_STATE_H_
#define CROSSDESK_GUI_STATE_H_
#ifndef _GUI_STATE_H_
#define _GUI_STATE_H_
#include "application/application_state.h"
#include "runtime/runtime_state.h"
@@ -18,4 +16,4 @@ struct GuiState : ApplicationState, RuntimeState {};
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_STATE_H_
#endif
@@ -0,0 +1,122 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _NET_TRAFFIC_STATS_CACHE_H_
#define _NET_TRAFFIC_STATS_CACHE_H_
#include <atomic>
#include "minirtc.h"
#include "runtime/connection_latency.h"
namespace crossdesk::gui_detail {
// Fields are independent observations and may come from different reports.
// Relaxed accesses keep each field safe without publishing any other state.
class NetTrafficStatsCache {
static_assert(std::atomic<uint32_t>::is_always_lock_free &&
std::atomic<float>::is_always_lock_free &&
std::atomic<double>::is_always_lock_free &&
std::atomic<bool>::is_always_lock_free,
"Network statistics require lock-free scalar atomics");
public:
void Store(const MiniRtcNetTrafficStats& stats) {
video_inbound_stats_.Store(stats.video_inbound_stats);
video_outbound_stats_.Store(stats.video_outbound_stats);
audio_inbound_stats_.Store(stats.audio_inbound_stats);
audio_outbound_stats_.Store(stats.audio_outbound_stats);
data_inbound_stats_.Store(stats.data_inbound_stats);
data_outbound_stats_.Store(stats.data_outbound_stats);
total_inbound_stats_.Store(stats.total_inbound_stats);
total_outbound_stats_.Store(stats.total_outbound_stats);
srtp_active_.store(stats.srtp_active, std::memory_order_relaxed);
rtt_ms_.store(stats.rtt_ms, std::memory_order_relaxed);
connection_latency_.Update(stats.rtt_ms);
}
MiniRtcNetTrafficStats Load() const {
MiniRtcNetTrafficStats stats{};
stats.video_inbound_stats = video_inbound_stats_.Load();
stats.video_outbound_stats = video_outbound_stats_.Load();
stats.audio_inbound_stats = audio_inbound_stats_.Load();
stats.audio_outbound_stats = audio_outbound_stats_.Load();
stats.data_inbound_stats = data_inbound_stats_.Load();
stats.data_outbound_stats = data_outbound_stats_.Load();
stats.total_inbound_stats = total_inbound_stats_.Load();
stats.total_outbound_stats = total_outbound_stats_.Load();
stats.srtp_active = srtp_active_.load(std::memory_order_relaxed);
stats.rtt_ms = rtt_ms_.load(std::memory_order_relaxed);
return stats;
}
uint32_t DataOutboundBitrate() const {
return data_outbound_stats_.bitrate.load(std::memory_order_relaxed);
}
std::optional<double> SmoothedRttMs(
ConnectionLatencyStats::Clock::time_point now =
ConnectionLatencyStats::Clock::now()) const {
return connection_latency_.Get(now);
}
void ResetLatency() { connection_latency_.Reset(); }
void Reset() {
Store(MiniRtcNetTrafficStats{});
ResetLatency();
}
private:
struct InboundStats {
void Store(const MiniRtcInboundStats& stats) {
bitrate.store(stats.bitrate, std::memory_order_relaxed);
rtp_packet_count.store(stats.rtp_packet_count, std::memory_order_relaxed);
loss_rate.store(stats.loss_rate, std::memory_order_relaxed);
}
MiniRtcInboundStats Load() const {
return {bitrate.load(std::memory_order_relaxed),
rtp_packet_count.load(std::memory_order_relaxed),
loss_rate.load(std::memory_order_relaxed)};
}
std::atomic<uint32_t> bitrate{0};
std::atomic<uint32_t> rtp_packet_count{0};
std::atomic<float> loss_rate{0};
};
struct OutboundStats {
void Store(const MiniRtcOutboundStats& stats) {
bitrate.store(stats.bitrate, std::memory_order_relaxed);
rtp_packet_count.store(stats.rtp_packet_count, std::memory_order_relaxed);
}
MiniRtcOutboundStats Load() const {
return {bitrate.load(std::memory_order_relaxed),
rtp_packet_count.load(std::memory_order_relaxed)};
}
std::atomic<uint32_t> bitrate{0};
std::atomic<uint32_t> rtp_packet_count{0};
};
InboundStats video_inbound_stats_;
OutboundStats video_outbound_stats_;
InboundStats audio_inbound_stats_;
OutboundStats audio_outbound_stats_;
InboundStats data_inbound_stats_;
OutboundStats data_outbound_stats_;
InboundStats total_inbound_stats_;
OutboundStats total_outbound_stats_;
std::atomic<bool> srtp_active_{false};
std::atomic<double> rtt_ms_{-1};
ConnectionLatencyStats connection_latency_;
};
} // namespace crossdesk::gui_detail
#endif
@@ -1,6 +1,9 @@
#include <SDL3/SDL_timer.h>
#include "runtime/peer_event_handler.h"
#include <remote_action.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <chrono>
@@ -20,7 +23,6 @@
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "runtime/remote_action_codec.h"
#if _WIN32
#include "interactive_state.h"
@@ -79,6 +81,9 @@ void PeerEventHandler::OnReceiveDataBuffer(
const char *data, size_t size, const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
@@ -144,15 +149,98 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
std::string remote_id(user_id, user_id_size);
if (remote_action.type == ControlType::video_settings_status) {
if (auto props = runtime->FindRemoteSession(remote_id)) {
if (source_id != props->control_data_label_) return;
std::lock_guard lock(props->video_settings_mutex_);
if (!props->video_settings_supported_) return;
if (remote_action.vs.accepted &&
static_cast<int32_t>(remote_action.vs.request_id -
props->applied_video_settings_.request_id) >= 0) {
props->applied_video_settings_ = remote_action.vs;
}
if (remote_action.vs.request_id != props->video_settings_.request_id) return;
props->video_settings_pending_ = false;
props->video_settings_failed_ = !remote_action.vs.accepted;
if (remote_action.vs.accepted) {
props->video_settings_ = remote_action.vs;
props->applied_video_settings_ = remote_action.vs;
} else {
const auto request_id = props->video_settings_.request_id;
props->video_settings_ = props->applied_video_settings_;
props->video_settings_.request_id = request_id;
}
}
return;
}
if (remote_action.type == ControlType::video_settings) {
if (source_id != runtime->control_data_label_ ||
!runtime->IsAuthorizedController(remote_id))
return;
const auto& settings = remote_action.vs;
remote_action.vs.accepted =
UpdateVideoSettings(
runtime->peer_, remote_id.data(), remote_id.size(),
static_cast<VideoQuality>(settings.quality), settings.frame_rate,
static_cast<VideoDegradationPreference>(settings.preference)) == 0;
remote_action.type = ControlType::video_settings_status;
const auto message = remote_action.to_json();
SendReliableDataFrameToPeer(runtime->peer_, message.data(), message.size(),
runtime->control_data_label_.c_str(),
remote_id.data(), remote_id.size());
return;
}
if (remote_action.type == ControlType::privacy_status) {
if (auto props = runtime->FindRemoteSession(remote_id)) {
if (source_id != props->control_data_label_) return;
std::lock_guard lock(props->privacy_status_mutex_);
if (!props->privacy_status_received_ ||
static_cast<int32_t>(remote_action.ps.revision - props->privacy_status_.revision) >= 0) {
props->privacy_status_ = remote_action.ps;
props->privacy_status_received_ = true;
props->privacy_status_tick_ = SDL_GetTicks();
if (static_cast<int32_t>(remote_action.ps.revision - props->privacy_request_revision_) > 0 &&
remote_action.ps.state != PrivacyState::starting &&
remote_action.ps.state != PrivacyState::stopping)
props->privacy_command_pending_ = false;
}
}
return;
}
if (remote_action.type == ControlType::privacy_command) {
if (source_id == runtime->control_data_label_)
runtime->QueuePrivacyCommand(remote_id, remote_action.pc);
return;
}
const bool operates_host = remote_action.type == ControlType::mouse ||
remote_action.type == ControlType::keyboard || remote_action.type == ControlType::keyboard_state ||
remote_action.type == ControlType::service_command || remote_action.type == ControlType::display_id ||
remote_action.type == ControlType::audio_capture;
if (operates_host) {
if (!runtime->IsAuthorizedController(remote_id)) return;
#ifdef _WIN32
// KeyboardController checks the desktop immediately before each native
// key injection, including state reconciliation and cleanup. Avoid a
// second synchronous session query for every received keyboard packet.
const bool keyboard_input = remote_action.type == ControlType::keyboard ||
remote_action.type == ControlType::keyboard_state;
if (!keyboard_input && runtime->privacy_.Engaged() &&
!IsWindowsPrivacyDesktopAvailable()) {
runtime->privacy_.Fail("Secure desktop or lock screen; privacy screen will turn off");
}
#endif
}
if (remote_action.type == ControlType::service_status) {
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
runtime->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss);
if (auto props = runtime->FindRemoteSession(remote_id)) {
runtime->ApplyRemoteServiceStatus(*props, remote_action.ss);
}
return;
}
if (remote_action.type == ControlType::service_command) {
if (runtime->privacy_.Engaged()) {
runtime->privacy_.Fail("Security shortcut requested; privacy screen will turn off");
}
#if _WIN32
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
runtime->pending_windows_service_sas_.store(true,
@@ -189,7 +277,11 @@ void PeerEventHandler::OnReceiveDataBuffer(
const bool changed =
!props->remote_cursor_state_received_ ||
props->remote_cursor_state_.visible != remote_action.cs.visible ||
props->remote_cursor_state_.shape != remote_action.cs.shape;
props->remote_cursor_state_.shape != remote_action.cs.shape ||
props->remote_cursor_state_.render_mode !=
remote_action.cs.render_mode ||
props->remote_cursor_state_.hidden_reason !=
remote_action.cs.hidden_reason;
CursorState merged = remote_action.cs;
if (!remote_action.cs.position_update &&
props->remote_cursor_state_received_) {
@@ -200,10 +292,14 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
props->remote_cursor_state_ = merged;
props->remote_cursor_state_received_ = true;
props->cursor_presentation_.Observe(merged, SDL_GetTicks());
if (changed) {
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}",
LOG_DEBUG("Received cursor state: seq={}, visible={}, shape={}, "
"mode={}, reason={}",
remote_action.cs.seq, remote_action.cs.visible,
static_cast<int>(remote_action.cs.shape));
static_cast<int>(remote_action.cs.shape),
static_cast<int>(remote_action.cs.render_mode),
static_cast<int>(remote_action.cs.hidden_reason));
}
}
return;
@@ -222,6 +318,25 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
if (is_client_mode) {
// Negotiate live settings only on the reliable control stream. Old
// hosts omit this capability and retain their existing video behavior.
if (props && source_id == props->control_data_label_) {
std::lock_guard lock(props->video_settings_mutex_);
const bool first_advertisement = !props->video_settings_supported_;
props->video_settings_supported_ =
remote_action.i.supports_video_settings;
if (first_advertisement && props->video_settings_supported_) {
RemoteAction settings{};
settings.type = ControlType::video_settings;
++props->video_settings_.request_id;
props->video_settings_pending_ = true;
props->video_settings_request_tick_ = SDL_GetTicks();
settings.vs = props->video_settings_;
const auto message = settings.to_json();
SendReliableDataFrame(props->peer_, message.data(), message.size(),
props->control_data_label_.c_str());
}
}
// client mode
if (props && props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name,
@@ -235,7 +350,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
remote_action.i.right[i], remote_action.i.bottom[i]));
}
}
remote_action_codec::Free(remote_action);
FreeRemoteAction(remote_action);
} else {
// server mode
std::string host_name(remote_action.i.host_name,
@@ -245,7 +360,11 @@ void PeerEventHandler::OnReceiveDataBuffer(
runtime->connection_host_names_[remote_id] = host_name;
}
LOG_INFO("Remote hostname: [{}]", host_name);
remote_action_codec::Free(remote_action);
if (source_id == runtime->control_data_label_) {
runtime->SetControllerPrivacySupport(
remote_id, remote_action.i.supports_privacy_screen);
}
FreeRemoteAction(remote_action);
}
} else {
// remote
@@ -296,9 +415,9 @@ void PeerEventHandler::OnReceiveDataBuffer(
runtime->devices_.SendMouseCommand(remote_action,
runtime->selected_display_);
} else if (remote_action.type == ControlType::audio_capture) {
if (remote_action.a && !runtime->start_speaker_capturer_)
if (remote_action.a)
runtime->devices_.StartSpeakerCapturer();
else if (!remote_action.a && runtime->start_speaker_capturer_)
else
runtime->devices_.StopSpeakerCapturer();
} else if (remote_action.type == ControlType::keyboard) {
runtime->keyboard_.ApplyRemoteEvent(remote_id, remote_action);
@@ -1,6 +1,7 @@
#include "runtime/peer_event_handler.h"
#include <remote_action.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <chrono>
@@ -21,7 +22,9 @@
#include "platform/video_renderer.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "runtime/remote_action_codec.h"
#ifdef __linux__
#include "platform/linux/headless/headless_console.h"
#endif
#if _WIN32
#include "interactive_state.h"
@@ -60,6 +63,9 @@ PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime || !message || size == 0) {
return;
@@ -153,6 +159,9 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
size_t user_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
@@ -161,6 +170,8 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
std::string client_id(user_id, user_id_size);
if (client_id == runtime->client_id_) {
runtime->signal_status_ = status;
runtime->device_presence_cache_.SetSignalConnected(
status == SignalStatus::SignalConnected);
if (SignalStatus::SignalConnecting == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalConnected == status) {
@@ -198,12 +209,8 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
}
std::string remote_id(client_id.begin() + 2, client_id.end());
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
auto props = runtime->remote_sessions_.find(remote_id)->second;
auto props = runtime->FindRemoteSession(remote_id);
if (!props) return;
props->signal_status_ = status;
if (SignalStatus::SignalConnecting == status) {
props->signal_connected_ = false;
@@ -230,6 +237,9 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
const size_t user_id_size,
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) return;
@@ -255,57 +265,29 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
props->cursor_presentation_ = {};
}
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.display_info_list().size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
RemoteAction remote_action = MakeHostInformation(
GetHostName(), runtime->devices_.host_display_list(),
/*supports_privacy_screen=*/true);
remote_action.i.supports_video_settings = true;
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
remote_action.i.display_list[i]);
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
const int ret =
SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
if (ret != 0) {
LOG_WARN("[{}] host information send failed, ret={}", remote_id,
ret);
}
FreeRemoteAction(remote_action);
}
if (!runtime->need_to_create_stream_window_ &&
!runtime->remote_sessions_.empty()) {
if (!runtime->need_to_create_stream_window_) {
runtime->need_to_create_stream_window_ = true;
}
props->connection_established_ = true;
@@ -318,12 +300,20 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
"connection_closed");
props->connection_established_ = false;
props->net_traffic_stats_.Reset();
props->enable_mouse_control_ = false;
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard lock(props->video_settings_mutex_);
props->video_settings_supported_ = false;
props->video_settings_pending_ = false;
props->video_settings_failed_ = false;
}
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
props->cursor_presentation_ = {};
}
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
@@ -347,6 +337,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->video_latency_->Reset();
props->video_frame_timing_ = {};
props->front_frame_.reset();
props->back_frame_.reset();
if (native_snapshot &&
@@ -374,21 +366,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
case ConnectionStatus::IncorrectPassword: {
runtime->password_validating_ = false;
runtime->password_validating_time_++;
if (runtime->connect_button_pressed_) {
runtime->connect_button_pressed_ = false;
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
}
break;
}
case ConnectionStatus::NoSuchTransmissionId:
case ConnectionStatus::RemoteUnavailable: {
if (runtime->connect_button_pressed_) {
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
}
break;
}
default:
@@ -399,7 +376,15 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->show_connection_status_window_ = true;
{
std::unique_lock lock(runtime->connection_status_mutex_);
if (status == ConnectionStatus::Connected) {
// Local privacy requires explicit desktop capability. Host info may
// arrive before Connected; consume that saved capability here.
const bool automatic_enable = runtime->privacy_sessions_.Connected(
remote_id, runtime->config_center_->IsEnablePrivacyScreen());
runtime->ApplyPrivacyAdmission(automatic_enable);
}
runtime->connection_status_[remote_id] = status;
runtime->devices_.OnControllerConnectionsChanged();
}
switch (status) {
@@ -408,52 +393,23 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->last_windows_service_status_tick_ = 0;
#endif
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.display_info_list().size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
RemoteAction remote_action = MakeHostInformation(
GetHostName(), runtime->devices_.host_display_list(),
/*supports_privacy_screen=*/true);
remote_action.i.supports_video_settings = true;
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
remote_action.i.display_list[i]);
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret =
const int ret =
SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
if (ret != 0) {
LOG_WARN("[{}] host information send failed, ret={}", remote_id,
ret);
}
FreeRemoteAction(remote_action);
}
runtime->need_to_destroy_server_window_.store(
@@ -462,19 +418,9 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::memory_order_release);
runtime->is_server_mode_ = true;
runtime->start_screen_capturer_ = true;
runtime->start_speaker_capturer_ = true;
runtime->devices_.StartSpeakerCapturer();
runtime->remote_client_id_ = remote_id;
runtime->start_mouse_controller_ = true;
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(),
[](const auto& kv) {
return kv.first.find("web") != std::string::npos;
})) {
runtime->show_cursor_ = true;
}
}
break;
}
@@ -493,9 +439,12 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
void PeerEventHandler::OnNetStatusReport(
const char* client_id, size_t client_id_size, TraversalMode mode,
const XNetTrafficStats* net_traffic_stats, const char* user_id,
const MiniRtcNetTrafficStats* net_traffic_stats, const char* user_id,
const size_t user_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
@@ -553,16 +502,16 @@ void PeerEventHandler::OnNetStatusReport(
LOG_INFO("Use client id [{}] and save id into cache file", id);
runtime->settings_.Save();
}
#ifdef __linux__
HeadlessConsole::Instance().SetIdentity(id + "@" + password);
#endif
}
std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
auto props = runtime->remote_sessions_.find(remote_id)->second;
auto props = runtime->FindRemoteSession(remote_id);
if (!props) return;
if (props->traversal_mode_ != mode) {
props->net_traffic_stats_.ResetLatency();
props->traversal_mode_ = mode;
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
}
@@ -573,7 +522,7 @@ void PeerEventHandler::OnNetStatusReport(
// only display client side net status if connected to itself
if (!(runtime->peer_reserved_ && !strstr(client_id, "C-"))) {
props->net_traffic_stats_ = *net_traffic_stats;
props->net_traffic_stats_.Store(*net_traffic_stats);
}
}
} // namespace crossdesk
@@ -1,10 +1,17 @@
#ifndef CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PEER_EVENT_HANDLER_H_
#define _PEER_EVENT_HANDLER_H_
#include <cstddef>
#include <string>
#include "minirtc.h"
#include "runtime/session_lifecycle.h"
namespace crossdesk {
@@ -15,7 +22,11 @@ class PeerEventHandler {
public:
explicit PeerEventHandler(GuiRuntime &owner);
static void OnReceiveVideoBuffer(const XVideoFrame *video_frame,
bool IsActive() const { return callbacks_.IsActive(); }
auto EnterCallback() { return callbacks_.Enter(); }
void Deactivate() { callbacks_.Close(); }
static void OnReceiveVideoBuffer(const MiniRtcVideoFrame *video_frame,
const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size,
void *user_data);
@@ -35,7 +46,7 @@ public:
size_t user_id_size, void *user_data);
static void OnNetStatusReport(const char *client_id, size_t client_id_size,
TraversalMode mode,
const XNetTrafficStats *net_traffic_stats,
const MiniRtcNetTrafficStats *net_traffic_stats,
const char *user_id, size_t user_id_size,
void *user_data);
@@ -43,8 +54,9 @@ private:
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
GuiRuntime &owner_;
gui_detail::SessionCallbackGate callbacks_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#endif
@@ -14,33 +14,75 @@ namespace {
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
const MiniRtcNativeVideoFrame* GetNativeVideoFrame(const MiniRtcVideoFrame* frame) {
if (!frame || !frame->native_frame) {
return nullptr;
}
const auto* native = frame->native_frame;
return native->struct_size >=
static_cast<uint32_t>(sizeof(MiniRtcNativeVideoFrame)) &&
native->owner && native->copy_to_nv12
? native
: nullptr;
}
} // namespace
void PeerEventHandler::OnReceiveVideoBuffer(
const XVideoFrame* video_frame, const char* user_id, size_t user_id_size,
const MiniRtcVideoFrame* video_frame, const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
if (!runtime || !video_frame) {
return;
}
std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
GuiRuntime::RemoteSession* props =
runtime->remote_sessions_.find(remote_id)->second.get();
auto props = runtime->FindRemoteSession(remote_id);
if (!props) return;
if (props->connection_established_) {
const auto timing_now = VideoLatencyFrame::Clock::now();
const auto timing = props->video_latency_->Frame(
video_frame->captured_timestamp, GetSystemTimeMicros(props->peer_),
timing_now);
bool background_snapshot_only = false;
auto* native_renderer = runtime->video_renderer_.get();
if (native_renderer && native_renderer->IsActive()) {
const auto submit_result = native_renderer->SubmitNv12(
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
video_frame->size, video_frame->width, video_frame->height);
VideoRenderer::SubmitResult submit_result =
VideoRenderer::SubmitResult::failed;
if (const auto* native = GetNativeVideoFrame(video_frame)) {
submit_result =
native_renderer->SubmitNativeFrame(remote_id, *native, timing);
// A CPU NV12 native frame is valid MiniRTC input, but the macOS Metal
// fast path only accepts CVPixelBuffer. Materialize it when that path
// rejects a software decoder's descriptor. This also supports peers
// using CPU decoding when their platform hardware decoder is unavailable.
if (submit_result == VideoRenderer::SubmitResult::failed &&
native->type == MiniRtcNativeVideoFrameCpuNv12) {
const size_t frame_size =
static_cast<size_t>(native->width) * native->height * 3U / 2U;
// One decode callback thread per session; keep the buffer across
// frames instead of a full-frame allocation per rejected frame.
auto& fallback = props->native_cpu_fallback_;
if (fallback.size() != frame_size) fallback.resize(frame_size);
if (native->copy_to_nv12(native->owner, fallback.data(),
fallback.size()) == 0) {
submit_result = native_renderer->SubmitNv12(
remote_id, fallback.data(), fallback.size(),
static_cast<int>(native->width),
static_cast<int>(native->height), timing);
}
}
} else
{
submit_result = native_renderer->SubmitNv12(
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
video_frame->size, video_frame->width, video_frame->height, timing);
}
if (submit_result == VideoRenderer::SubmitResult::submitted) {
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
const bool size_changed = (props->video_width_ != video_frame->width) ||
@@ -88,17 +130,39 @@ void PeerEventHandler::OnReceiveVideoBuffer(
return;
}
size_t frame_size = video_frame->size;
const MiniRtcNativeVideoFrame* native_frame =
GetNativeVideoFrame(video_frame);
if (native_frame) {
frame_size = static_cast<size_t>(native_frame->width) *
native_frame->height * 3U / 2U;
}
if (frame_size == 0) {
return;
}
// Allocate a third buffer only while the UI still owns the old snapshot.
if (!props->back_frame_ || props->back_frame_.use_count() != 1) {
props->back_frame_ =
std::make_shared<std::vector<unsigned char>>(video_frame->size);
std::make_shared<std::vector<unsigned char>>(frame_size);
}
if (props->back_frame_->size() != video_frame->size) {
props->back_frame_->resize(video_frame->size);
if (props->back_frame_->size() != frame_size) {
props->back_frame_->resize(frame_size);
}
std::memcpy(props->back_frame_->data(), video_frame->data,
video_frame->size);
if (native_frame) {
if (native_frame->copy_to_nv12(
native_frame->owner, props->back_frame_->data(),
props->back_frame_->size()) != 0) {
return;
}
} else
{
if (!video_frame->data || video_frame->size < frame_size) {
return;
}
std::memcpy(props->back_frame_->data(), video_frame->data, frame_size);
}
const bool size_changed = (props->video_width_ != video_frame->width) ||
(props->video_height_ != video_frame->height);
@@ -108,9 +172,10 @@ void PeerEventHandler::OnReceiveVideoBuffer(
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
props->video_size_ = frame_size;
props->front_frame_.swap(props->back_frame_);
props->video_frame_timing_ = timing;
props->thumbnail_frame_ = props->front_frame_;
props->thumbnail_width_ = video_frame->width;
props->thumbnail_height_ = video_frame->height;
@@ -132,6 +197,9 @@ void PeerEventHandler::OnReceiveAudioBuffer(
const char* data, size_t size, const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
@@ -0,0 +1,53 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-12
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_CONTROL_STATE_H_
#define _PRIVACY_CONTROL_STATE_H_
#include <cstdint>
#include "remote_action.h"
namespace crossdesk {
struct PrivacyControlState {
bool active = false;
bool enabled = false;
bool off = false;
bool unsupported = false;
bool can_toggle = false;
};
// Use the same host-confirmed state for presentation and command admission.
// Missing confirmation or a failure makes only this toolbar control unavailable.
inline PrivacyControlState GetPrivacyControlState(
const PrivacyStatus& status, bool received, bool command_pending,
uint64_t status_age_ms, uint64_t command_age_ms) {
PrivacyControlState result;
const bool timeout = command_pending && command_age_ms > 15000;
result.unsupported = timeout ||
(received && (status_age_ms > 5000 || !status.supported ||
status.state == PrivacyState::failed));
if (result.unsupported) return result;
result.active = status.overlay_active || status.remote_paused ||
status.state == PrivacyState::on ||
status.state == PrivacyState::starting ||
status.state == PrivacyState::stopping ||
status.state == PrivacyState::failed;
// A command in flight only gates repeated clicks; it adds no display state.
result.enabled = received &&
status.state == PrivacyState::on && status.overlay_active &&
!status.remote_paused;
result.off = received &&
status.state == PrivacyState::off && !result.active;
result.can_toggle = !command_pending && (result.active || received);
return result;
}
} // namespace crossdesk
#endif
@@ -0,0 +1,133 @@
#include <SDL3/SDL_timer.h>
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include <cstring>
#include <shared_mutex>
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
bool GuiRuntime::IsAuthorizedController(const std::string& remote_id) {
// Connections appear here only after the existing password/signalling/RTC
// admission path. Outgoing/viewer sessions live in remote_sessions_ instead.
std::shared_lock lock(connection_status_mutex_);
const auto found = connection_status_.find(remote_id);
return found != connection_status_.end() &&
found->second == ConnectionStatus::Connected;
}
void GuiRuntime::SetControllerPrivacySupport(const std::string& remote_id,
bool supported) {
std::unique_lock lock(connection_status_mutex_);
const auto found = connection_status_.find(remote_id);
if (found == connection_status_.end()) return;
if (found->second != ConnectionStatus::Connected) {
if (found->second == ConnectionStatus::Connecting ||
found->second == ConnectionStatus::Gathering) {
privacy_sessions_.RememberSupport(remote_id, supported);
LOG_INFO("Privacy: retained early controller capability, peer={}, supported={}",
remote_id, supported);
}
return;
}
const bool automatic_enable =
privacy_sessions_.SetSupported(remote_id, supported);
ApplyPrivacyAdmission(automatic_enable &&
config_center_->IsEnablePrivacyScreen());
}
void GuiRuntime::ApplyPrivacyAdmission(bool automatic_enable) {
if (!privacy_sessions_.CanEnable()) {
if (privacy_.Engaged()) privacy_.Disable();
} else if (automatic_enable) {
privacy_.EnableOnConnection();
}
}
void GuiRuntime::QueuePrivacyCommand(const std::string& remote_id,
const PrivacyCommand& command) {
std::shared_lock session_lock(connection_status_mutex_);
const auto found = connection_status_.find(remote_id);
if (found == connection_status_.end() ||
found->second != ConnectionStatus::Connected ||
!privacy_sessions_.Supports(remote_id)) return;
std::lock_guard lock(privacy_commands_mutex_);
if (privacy_commands_.size() >= 32) return;
LOG_INFO("Privacy: queued controller command, peer={}, flag={}", remote_id,
static_cast<int>(command.flag));
privacy_commands_.emplace_back(remote_id, command);
}
void GuiRuntime::HandlePrivacy() {
std::deque<std::pair<std::string, PrivacyCommand>> commands;
{
std::lock_guard lock(privacy_commands_mutex_);
commands.swap(privacy_commands_);
}
for (const auto& [remote_id, command] : commands) {
// Keep admission and activation under the connection lock so an unknown
// controller cannot join between the capability check and enabling.
std::shared_lock lock(connection_status_mutex_);
const auto found = connection_status_.find(remote_id);
if (found == connection_status_.end() ||
found->second != ConnectionStatus::Connected ||
!privacy_sessions_.Supports(remote_id)) continue;
if (command.flag == PrivacyCommandFlag::enable) {
if (privacy_sessions_.CanEnable())
privacy_.Enable(command.block_local_input);
} else if (command.flag == PrivacyCommandFlag::disable) {
privacy_.Disable();
}
last_privacy_status_tick_ = 0;
}
PrivacyStatus status{};
const uint64_t now = SDL_GetTicks();
std::vector<std::string> controllers;
{
std::shared_lock lock(connection_status_mutex_);
status = privacy_.Snapshot();
for (const auto& [id, state] : connection_status_)
if (state == ConnectionStatus::Connected &&
privacy_sessions_.Supports(id))
controllers.push_back(id);
if (!privacy_sessions_.CanEnable()) {
status.state = PrivacyState::unsupported;
status.supported = false;
status.input_block_supported = false;
std::strcpy(status.reason,
"A connected controller does not support privacy screen");
}
}
if (!peer_ || (last_privacy_status_tick_ != 0 &&
status.revision == last_privacy_revision_ &&
now - last_privacy_status_tick_ < 1000))
return;
RemoteAction action{};
action.type = ControlType::privacy_status;
action.ps = status;
const auto message = action.to_json();
bool sent = true;
for (const auto& id : controllers) {
const int result = SendReliableDataFrameToPeer(
peer_, message.data(), message.size(), control_data_label_.c_str(),
id.data(), id.size());
if (result != 0) {
sent = false;
LOG_WARN("Privacy status send failed, peer={}, ret={}", id, result);
}
}
if (sent) {
last_privacy_revision_ = status.revision;
last_privacy_status_tick_ = now;
}
}
} // namespace crossdesk
@@ -0,0 +1,80 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-15
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_SESSION_POLICY_H_
#define _PRIVACY_SESSION_POLICY_H_
#include <algorithm>
#include <string>
#include <unordered_map>
namespace crossdesk {
// Guarded by GuiRuntime's connection_status_mutex_. Privacy covers the local
// displays, so every connected controller must explicitly support its controls.
class PrivacySessionPolicy {
public:
// Web controllers render through the browser and expose no privacy UI.
static bool IsWebController(const std::string& remote_id) {
return remote_id.find("web") != std::string::npos;
}
bool Connected(const std::string& remote_id, bool automatic_privacy) {
const auto [entry, inserted] = controllers_.try_emplace(remote_id, false);
if (inserted && controllers_.size() == 1 && automatic_privacy &&
!IsWebController(remote_id)) {
automatic_controller_ = remote_id;
}
const auto early = early_support_.find(remote_id);
if (early == early_support_.end()) return false;
const bool supported = early->second;
early_support_.erase(early);
return SetSupported(remote_id, supported);
}
// Only transport-authenticated metadata from a connecting session is cached
// by the caller. It grants no control until Connected admits that session.
void RememberSupport(const std::string& remote_id, bool supported) {
early_support_[remote_id] = supported;
}
void Disconnected(const std::string& remote_id) {
controllers_.erase(remote_id);
early_support_.erase(remote_id);
if (automatic_controller_ == remote_id) automatic_controller_.clear();
}
// Consume automatic activation once, on the first controller's host info.
// Repeated metadata or a later join must not undo an explicit privacy exit.
bool SetSupported(const std::string& remote_id, bool supported) {
const auto entry = controllers_.find(remote_id);
if (entry == controllers_.end()) return false;
entry->second = supported && !IsWebController(remote_id);
if (automatic_controller_ != remote_id) return false;
automatic_controller_.clear();
return CanEnable();
}
bool Supports(const std::string& remote_id) const {
const auto entry = controllers_.find(remote_id);
return entry != controllers_.end() && entry->second;
}
bool CanEnable() const {
return !controllers_.empty() &&
std::all_of(controllers_.begin(), controllers_.end(),
[](const auto& entry) { return entry.second; });
}
private:
std::unordered_map<std::string, bool> controllers_;
std::unordered_map<std::string, bool> early_support_;
std::string automatic_controller_;
};
} // namespace crossdesk
#endif
@@ -1,119 +0,0 @@
#include "runtime/remote_action_codec.h"
#include <remote_action.h>
#include <cstdlib>
#include <cstring>
namespace crossdesk::remote_action_codec {
std::vector<char> Serialize(const RemoteAction &action) {
std::vector<char> buffer;
buffer.push_back(static_cast<char>(action.type));
auto insert_bytes = [&](const void *ptr, size_t len) {
buffer.insert(buffer.end(), static_cast<const char *>(ptr),
static_cast<const char *>(ptr) + len);
};
if (action.type == ControlType::host_infomation) {
insert_bytes(&action.i.host_name_size, sizeof(size_t));
insert_bytes(action.i.host_name, action.i.host_name_size);
size_t num = action.i.display_num;
insert_bytes(&num, sizeof(size_t));
for (size_t i = 0; i < num; ++i) {
const size_t len = std::strlen(action.i.display_list[i]);
insert_bytes(&len, sizeof(size_t));
insert_bytes(action.i.display_list[i], len);
}
insert_bytes(action.i.left, sizeof(int) * num);
insert_bytes(action.i.top, sizeof(int) * num);
insert_bytes(action.i.right, sizeof(int) * num);
insert_bytes(action.i.bottom, sizeof(int) * num);
}
return buffer;
}
bool Deserialize(const char *data, size_t size, RemoteAction &out) {
size_t offset = 0;
auto read = [&](void *dst, size_t len) -> bool {
if (offset + len > size) {
return false;
}
std::memcpy(dst, data + offset, len);
offset += len;
return true;
};
if (size < 1) {
return false;
}
out.type = static_cast<ControlType>(data[offset++]);
if (out.type == ControlType::host_infomation) {
size_t name_len;
if (!read(&name_len, sizeof(size_t)) ||
name_len >= sizeof(out.i.host_name)) {
return false;
}
if (!read(out.i.host_name, name_len)) {
return false;
}
out.i.host_name[name_len] = '\0';
out.i.host_name_size = name_len;
size_t num;
if (!read(&num, sizeof(size_t))) {
return false;
}
out.i.display_num = num;
out.i.display_list =
static_cast<char **>(std::malloc(num * sizeof(char *)));
for (size_t i = 0; i < num; ++i) {
size_t len;
if (!read(&len, sizeof(size_t)) || offset + len > size) {
return false;
}
out.i.display_list[i] = static_cast<char *>(std::malloc(len + 1));
std::memcpy(out.i.display_list[i], data + offset, len);
out.i.display_list[i][len] = '\0';
offset += len;
}
auto alloc_int_array = [&](int *&array) {
array = static_cast<int *>(std::malloc(num * sizeof(int)));
return read(array, num * sizeof(int));
};
return alloc_int_array(out.i.left) && alloc_int_array(out.i.top) &&
alloc_int_array(out.i.right) && alloc_int_array(out.i.bottom);
}
return true;
}
void Free(RemoteAction &action) {
if (action.type != ControlType::host_infomation) {
return;
}
for (size_t i = 0; i < action.i.display_num; ++i) {
std::free(action.i.display_list[i]);
}
std::free(action.i.display_list);
std::free(action.i.left);
std::free(action.i.top);
std::free(action.i.right);
std::free(action.i.bottom);
action.i.display_list = nullptr;
action.i.left = action.i.top = action.i.right = action.i.bottom = nullptr;
action.i.display_num = 0;
}
} // namespace crossdesk::remote_action_codec
@@ -1,17 +0,0 @@
#ifndef CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#define CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#include <cstddef>
#include <vector>
#include <remote_action.h>
namespace crossdesk::remote_action_codec {
std::vector<char> Serialize(const RemoteAction &action);
bool Deserialize(const char *data, size_t size, RemoteAction &out);
void Free(RemoteAction &action);
} // namespace crossdesk::remote_action_codec
#endif // CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
+38 -42
View File
@@ -1,3 +1,9 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _REMOTE_SESSION_H_
#define _REMOTE_SESSION_H_
@@ -16,7 +22,12 @@
#include <remote_action.h>
#include <stream_names.h>
#include "display_info.h"
#include "cursor_presentation.h"
#include "minirtc.h"
#include "rendering/video_latency.h"
#include "runtime/net_traffic_stats_cache.h"
namespace crossdesk { class PeerEventHandler; }
namespace crossdesk::gui_detail {
@@ -30,7 +41,6 @@ struct FileTransferState {
std::chrono::steady_clock::time_point file_send_last_update_time_;
uint64_t file_send_last_bytes_ = 0;
bool file_transfer_window_visible_ = false;
bool file_transfer_window_hovered_ = false;
std::atomic<uint32_t> current_file_id_{0};
struct QueuedFile {
@@ -61,6 +71,8 @@ struct FileTransferState {
struct RemoteSession {
Params params_;
PeerPtr* peer_ = nullptr;
std::shared_ptr<PeerEventHandler> peer_events_;
std::atomic<bool> closing_{false};
std::string audio_label_ = kAudioStream;
std::string data_label_ = kDataStream;
std::string mouse_label_ = kMouseStream;
@@ -82,31 +94,12 @@ struct RemoteSession {
bool audio_capture_button_pressed_ = true;
bool control_mouse_ = true;
bool streaming_ = false;
bool is_control_bar_in_left_ = true;
bool control_bar_hovered_ = false;
bool display_selectable_hovered_ = false;
bool shortcut_selectable_hovered_ = false;
bool control_bar_expand_ = true;
bool reset_control_bar_pos_ = false;
bool control_window_width_is_changing_ = false;
bool control_window_height_is_changing_ = false;
bool p2p_mode_ = true;
bool remember_password_ = false;
char remote_password_[7] = "";
float sub_stream_window_width_ = 1280;
float sub_stream_window_height_ = 720;
float control_window_min_width_ = 20;
float control_window_max_width_ = 300;
float control_window_min_height_ = 38;
float control_window_max_height_ = 180;
float control_window_width_ = 300;
float control_window_height_ = 38;
float control_bar_pos_x_ = 0;
float control_bar_pos_y_ = 30;
float mouse_diff_control_bar_pos_x_ = 0;
float mouse_diff_control_bar_pos_y_ = 0;
double control_bar_button_pressed_time_ = 0;
double net_traffic_stats_button_pressed_time_ = 0;
// Reused across frames by the decode callback thread when a CPU NV12
// native frame has to be materialized for a renderer that rejects it.
std::vector<unsigned char> native_cpu_fallback_;
// Written by the decode callback thread and consumed by the UI thread.
std::mutex video_frame_mutex_;
@@ -120,33 +113,33 @@ struct RemoteSession {
std::chrono::steady_clock::time_point background_snapshot_time_;
bool render_rect_dirty_ = false;
bool stream_cleanup_pending_ = false;
float mouse_pos_x_ = 0;
float mouse_pos_y_ = 0;
float mouse_pos_x_last_ = 0;
float mouse_pos_y_last_ = 0;
int texture_width_ = 1280;
int texture_height_ = 720;
int video_width_ = 0;
int video_height_ = 0;
int video_width_last_ = 0;
int video_height_last_ = 0;
int selected_display_ = 0;
size_t video_size_ = 0;
uint64_t video_frame_sequence_ = 0;
VideoLatencyFrame video_frame_timing_;
std::shared_ptr<VideoLatencyStats> video_latency_ =
std::make_shared<VideoLatencyStats>();
bool tab_selected_ = false;
bool tab_opened_ = true;
std::optional<float> pos_x_before_docked_;
std::optional<float> pos_y_before_docked_;
float render_window_x_ = 0;
float render_window_y_ = 0;
float render_window_width_ = 0;
float render_window_height_ = 0;
std::string fullscreen_button_label_ = "Fullscreen";
std::string net_traffic_stats_button_label_ = "Show Net Traffic Stats";
std::string mouse_control_button_label_ = "Mouse Control";
std::string audio_capture_button_label_ = "Audio Capture";
std::string remote_host_name_;
bool remote_service_status_received_ = false;
std::mutex video_settings_mutex_;
// Requested and confirmed settings for this remote session only.
VideoSettings video_settings_{2, 60, 1};
bool video_settings_supported_ = false;
VideoSettings applied_video_settings_{2, 60, 1, 0, false};
bool video_settings_pending_ = false;
bool video_settings_failed_ = false;
uint64_t video_settings_request_tick_ = 0;
std::mutex privacy_status_mutex_;
PrivacyStatus privacy_status_{};
bool privacy_status_received_ = false;
bool privacy_command_pending_ = false;
uint64_t privacy_command_tick_ = 0;
uint32_t privacy_request_revision_ = 0;
uint64_t privacy_status_tick_ = 0;
bool remote_service_available_ = false;
std::string remote_interactive_stage_;
std::vector<DisplayInfo> display_info_list_;
@@ -156,13 +149,16 @@ struct RemoteSession {
std::mutex remote_cursor_state_mutex_;
CursorState remote_cursor_state_{};
bool remote_cursor_state_received_ = false;
CursorPresentation cursor_presentation_;
// Shared by minirtc callbacks, Slint rendering and SDL audio callbacks.
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
int fps_ = 0;
int frame_count_ = 0;
std::chrono::steady_clock::time_point last_time_;
XNetTrafficStats net_traffic_stats_{};
// UI-thread snapshot refreshed with FPS once per second.
std::optional<VideoLatencyStats::Snapshot> video_latency_snapshot_;
NetTrafficStatsCache net_traffic_stats_;
using QueuedFile = FileTransferState::QueuedFile;
using FileTransferStatus = FileTransferState::FileTransferStatus;
+21 -19
View File
@@ -1,3 +1,9 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _RUNTIME_STATE_H_
#define _RUNTIME_STATE_H_
@@ -16,9 +22,11 @@
#include <stream_names.h>
#include "config_center.h"
#include "common/task_queue_lock_free.h"
#include "path_manager.h"
#include "runtime/device_presence_cache.h"
#include "runtime/remote_session.h"
#include "runtime/session_lifecycle.h"
#include "thumbnail.h"
namespace crossdesk::gui_detail {
@@ -32,7 +40,6 @@ struct InfrastructureState {
std::string dll_log_path_;
std::string cache_path_;
int localization_language_index_ = -1;
int localization_language_index_last_ = -1;
bool modules_inited_ = false;
unsigned char aes128_key_[16]{};
unsigned char aes128_iv_[16]{};
@@ -47,13 +54,15 @@ struct RecentConnectionsState {
int recent_connection_image_height_ = 90;
uint32_t recent_connection_image_save_time_ = 0;
DevicePresenceCache device_presence_cache_;
bool need_to_send_recent_connections_ = true;
std::atomic<bool> need_to_send_recent_connections_{true};
std::chrono::steady_clock::time_point next_presence_refresh_at_{};
std::chrono::steady_clock::time_point last_presence_request_attempt_at_{};
};
struct PeerState {
SignalStatus signal_status_ = SignalStatus::SignalClosed;
std::string signal_status_str_;
bool signal_connected_ = false;
std::atomic<bool> signal_connected_{false};
PeerPtr *peer_ = nullptr;
PeerPtr *peer_reserved_ = nullptr;
std::string video_primary_label_ = "primary_display";
@@ -113,32 +122,22 @@ struct PlatformIntegrationState {
struct UserSettingsState {
char client_id_[10] = "";
char client_id_display_[12] = "";
char client_id_with_password_[17] = "";
char password_saved_[7] = "";
char self_hosted_id_[17] = "";
char self_hosted_user_id_[17] = "";
char connection_login_identity_[17] = "";
int language_button_value_ = 0;
int video_quality_button_value_ = 2;
int video_frame_rate_button_value_ = 1;
int video_adaptation_policy_button_value_ = 1;
int video_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = true;
bool enable_turn_ = true;
bool enable_srtp_ = false;
char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099";
char coturn_server_port_[6] = "3478";
bool enable_self_hosted_ = false;
int language_button_value_last_ = 0;
int video_quality_button_value_last_ = 0;
int video_frame_rate_button_value_last_ = 0;
int video_adaptation_policy_button_value_last_ = 1;
int video_encode_format_button_value_last_ = 0;
bool enable_hardware_video_codec_last_ = false;
bool enable_turn_last_ = true;
bool enable_srtp_last_ = false;
bool enable_self_hosted_last_ = false;
bool enable_autostart_ = false;
bool enable_autostart_last_ = false;
@@ -148,9 +147,6 @@ struct UserSettingsState {
std::string file_transfer_save_path_last_;
char signal_server_ip_self_[256] = "";
char signal_server_port_self_[6] = "";
char coturn_server_port_self_[6] = "";
bool settings_window_pos_reset_ = true;
bool self_hosted_server_config_window_pos_reset_ = true;
std::string selected_current_file_path_;
bool show_file_browser_ = true;
};
@@ -178,8 +174,15 @@ struct ConnectionState {
std::unordered_map<std::string, RemoteSessionPtr>;
RemoteSessionMap remote_sessions_;
std::shared_mutex remote_sessions_mutex_;
std::vector<std::thread> thumbnail_save_threads_;
std::mutex thumbnail_save_threads_mutex_;
TaskQueueLockFree session_cleanup_queue_;
std::unordered_map<std::string, std::future<void>> session_cleanup_tasks_;
std::future<void> connection_peer_cleanup_;
std::unordered_map<std::string, std::future<void>> controller_cleanup_tasks_;
struct PendingReconnect {
std::string password;
bool remember_password = false;
};
std::unordered_map<std::string, PendingReconnect> pending_reconnects_;
std::shared_mutex connection_status_mutex_;
std::unordered_map<std::string, ConnectionStatus> connection_status_;
@@ -191,7 +194,6 @@ struct ConnectionState {
std::unordered_map<std::string, std::chrono::steady_clock::time_point>
last_remote_pointer_input_time_;
std::string selected_server_remote_id_;
std::string selected_server_remote_hostname_;
std::mutex pending_presence_probe_mutex_;
bool pending_presence_probe_ = false;
bool pending_presence_result_ready_ = false;
@@ -0,0 +1,47 @@
#include "runtime/session_lifecycle.h"
namespace crossdesk::gui_detail {
CallbackGuard::CallbackGuard(SessionCallbackGate& callbacks)
: callbacks_(callbacks), accepted_(callbacks.BeginCallback()) {}
CallbackGuard::~CallbackGuard() {
if (accepted_) {
callbacks_.EndCallback();
}
}
bool SessionCallbackGate::IsActive() const {
std::lock_guard lock(mutex_);
return accepting_callbacks_;
}
CallbackGuard SessionCallbackGate::Enter() {
return CallbackGuard(*this);
}
bool SessionCallbackGate::BeginCallback() {
std::lock_guard lock(mutex_);
if (!accepting_callbacks_) {
return false;
}
++running_callbacks_;
return true;
}
void SessionCallbackGate::EndCallback() {
std::lock_guard lock(mutex_);
--running_callbacks_;
if (running_callbacks_ == 0) {
callbacks_finished_.notify_all();
}
}
void SessionCallbackGate::Close() {
std::unique_lock lock(mutex_);
accepting_callbacks_ = false;
// Waiting releases the lock so running callbacks can call EndCallback().
callbacks_finished_.wait(lock, [this] { return running_callbacks_ == 0; });
}
} // namespace crossdesk::gui_detail
@@ -0,0 +1,58 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SESSION_LIFECYCLE_H_
#define _SESSION_LIFECYCLE_H_
#include <condition_variable>
#include <cstddef>
#include <mutex>
namespace crossdesk::gui_detail {
class SessionCallbackGate;
// Decrements the count on scope exit, including early returns and exceptions.
class CallbackGuard {
public:
explicit CallbackGuard(SessionCallbackGate& callbacks);
~CallbackGuard();
CallbackGuard(const CallbackGuard&) = delete;
CallbackGuard& operator=(const CallbackGuard&) = delete;
explicit operator bool() const { return accepted_; }
private:
SessionCallbackGate& callbacks_;
bool accepted_;
};
// Each peer counts callbacks as they enter and leave.
// Closing rejects new callbacks, then waits for running callbacks to finish.
class SessionCallbackGate {
public:
bool IsActive() const;
CallbackGuard Enter();
// Called by the session owner, never from within this session's callbacks.
void Close();
private:
friend class CallbackGuard;
bool BeginCallback();
void EndCallback();
mutable std::mutex mutex_;
std::condition_variable callbacks_finished_;
bool accepting_callbacks_ = true;
std::size_t running_callbacks_ = 0;
};
} // namespace crossdesk::gui_detail
#endif
+5
View File
@@ -41,10 +41,12 @@ export global FontAwesomeIcons {
out property <string> keyboard: "\u{f11c}";
out property <string> minus: "\u{f068}";
out property <string> pen: "\u{f304}";
out property <string> rotate: "\u{f2f1}";
out property <string> signal: "\u{f012}";
out property <string> shield-halved: "\u{f3ed}";
out property <string> square-full: "\u{f45c}";
out property <string> trash-can: "\u{f2ed}";
out property <string> user-secret: "\u{f21b}";
out property <string> volume-high: "\u{f028}";
out property <string> volume-xmark: "\u{f6a9}";
out property <string> xmark: "\u{f00d}";
@@ -53,6 +55,7 @@ export global FontAwesomeIcons {
export component IconButton inherits Rectangle {
in property <string> icon;
in property <string> tooltip;
in property <bool> enabled: true;
in property <bool> danger: false;
in property <length> icon-size: 16px;
in property <color> icon-color: #24272d;
@@ -68,6 +71,7 @@ export component IconButton inherits Rectangle {
: touch.has-hover ? (danger ? #f04438 : root.hover-background)
: root.normal-background;
border-radius: 4px;
opacity: root.enabled ? 1 : 0.4;
Text {
text: root.icon;
@@ -89,6 +93,7 @@ export component IconButton inherits Rectangle {
}
touch := TouchArea {
enabled: root.enabled;
clicked => { root.clicked(); }
}
+1 -1
View File
@@ -1,5 +1,5 @@
// Single AOT entry point. Generating one header for the entire UI avoids
// duplicate imported-component definitions when C++ uses all three windows.
export { MainWindow, RecentConnection, UiStrings } from "main_window.slint";
export { MainWindow, RecentConnection, UpdateCheckStatus, UiStrings } from "main_window.slint";
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
export { ServerWindow, ControllerEntry } from "server_window.slint";
+195 -109
View File
@@ -14,6 +14,8 @@ export struct ReleaseNoteBlock {
section-gap: bool,
}
export enum UpdateCheckStatus { idle, checking, up-to-date, available, failed }
export global UiStrings {
in-out property <string> local-desktop: "Local Desktop";
in-out property <string> remote-desktop: "Remote Desktop";
@@ -24,6 +26,7 @@ export global UiStrings {
in-out property <string> online: "Online";
in-out property <string> offline: "Offline";
in-out property <string> password: "Password";
in-out property <string> refresh-password: "Refresh random password";
in-out property <string> copied: "Local ID copied to clipboard";
in-out property <string> connect: "Connect";
in-out property <string> settings: "Settings";
@@ -37,24 +40,20 @@ export global UiStrings {
in-out property <string> delete-connection: "Delete connection";
in-out property <string> confirm-delete: "Delete this recent connection?";
in-out property <string> language: "Language";
in-out property <string> video-quality: "Video quality";
in-out property <string> frame-rate: "Frame rate";
in-out property <string> adaptation-policy: "Video preference";
in-out property <string> priority-frame-rate: "Frame rate";
in-out property <string> priority-quality: "Quality";
in-out property <string> priority-balanced: "Balanced";
in-out property <string> capture-method: "Capture method";
in-out property <string> capture-method-auto: "Auto";
in-out property <string> codec: "Video codec";
in-out property <string> hardware-codec: "Hardware video codec";
in-out property <string> turn-relay: "TURN relay";
in-out property <string> srtp: "SRTP encryption";
in-out property <string> force-relay: "Force relay";
in-out property <string> self-hosted: "Self-hosted server";
in-out property <string> autostart: "Start with system";
in-out property <string> daemon: "Run in background";
in-out property <string> privacy-on-connect: "Privacy screen on connect";
in-out property <string> file-save-path: "File save path";
in-out property <string> default-desktop: "Desktop";
in-out property <string> server-host: "Server host";
in-out property <string> server-port: "Signal port";
in-out property <string> coturn-port: "TURN port";
in-out property <string> version: "Version";
in-out property <string> copyright: "© 2026 by JUNKUN DI. All right reserved.";
in-out property <string> license: "Licensed under GNU GPL v3.";
@@ -62,6 +61,10 @@ export global UiStrings {
in-out property <string> signal-disconnected: "Signal server disconnected";
in-out property <string> tls-error: "Signal server TLS certificate error";
in-out property <string> update-available: "A new version is available";
in-out property <string> check-for-updates: "Check for updates";
in-out property <string> checking-for-updates: "Checking for updates…";
in-out property <string> up-to-date: "You’re up to date";
in-out property <string> update-check-failed: "Check failed. Please try again later.";
in-out property <string> release-notes: "Release notes";
in-out property <string> later: "Later";
in-out property <string> download: "Download";
@@ -81,9 +84,6 @@ export global UiStrings {
in-out property <string> do-not-remind: "Do not remind again";
in-out property <string> notification: "Notification";
in-out property <string> service-suppressed-message: "You will not be reminded again. You can install the service later in Settings.";
in-out property <string> quality-low: "Low";
in-out property <string> quality-medium: "Medium";
in-out property <string> quality-high: "High";
in-out property <string> codec-h264: "H.264";
in-out property <string> codec-av1: "AV1";
in-out property <string> self-hosted-settings: "Self-hosted server settings";
@@ -184,8 +184,60 @@ component ConnectionDialogButton inherits Rectangle {
button-touch := TouchArea { clicked => { root.clicked(); } }
}
// Use the same compact, plain-text field for connection and password changes.
component PasswordInput inherits Rectangle {
in-out property <string> text;
in property <int> max-length: 0;
private property <string> previous-text;
callback accepted;
width: 70px;
height: 23px;
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: 3px;
forward-focus: editor;
init => { root.previous-text = root.text; }
editor := TextInput {
x: 5px; y: 1px; width: parent.width - 10px; height: parent.height - 2px;
text <=> root.text;
input-type: InputType.text;
single-line: true;
color: #202124;
font-size: ImGuiFontStyle.base;
vertical-alignment: center;
selection-foreground-color: white;
selection-background-color: #2463c7;
edited => {
// Validate the resulting text so typing, paste, and replacement
// all obey the limit while deletion and selection still work.
if (root.max-length > 0 && self.text.character-count > root.max-length) {
self.text = root.previous-text;
} else {
root.previous-text = self.text;
}
}
accepted => { root.accepted(); }
}
Timer {
interval: 1ms;
running: true;
triggered => {
self.running = false;
editor.focus();
// Place the caret after any restored password, including UTF-8.
editor.select-all();
editor.clear-selection();
}
}
}
export component MainWindow inherits Window {
title: "CrossDesk";
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
preferred-width: 640px;
preferred-height: 450px + root.titlebar-height;
min-width: 640px;
@@ -206,10 +258,16 @@ export component MainWindow inherits Window {
in property <string> local-password: "";
in-out property <string> remote-id-input: "";
in property <bool> password-visible: true;
in property <bool> password-change-pending: false;
in property <bool> signal-connected: false;
in property <bool> signal-tls-error: false;
in property <[RecentConnection]> recent-connections;
in property <bool> update-available: false;
in property <UpdateCheckStatus> update-check-status: idle;
private property <string> update-status-text:
root.update-check-status == UpdateCheckStatus.checking ? UiStrings.checking-for-updates :
root.update-check-status == UpdateCheckStatus.up-to-date ? UiStrings.up-to-date :
root.update-check-status == UpdateCheckStatus.failed ? UiStrings.update-check-failed : "";
in property <string> current-version: "";
in property <string> latest-version: "";
in property <string> release-name: "";
@@ -236,20 +294,24 @@ export component MainWindow inherits Window {
in-out property <bool> portable-service-do-not-remind: false;
in-out property <int> language-index: 0;
in-out property <int> video-quality-index: 2;
in-out property <int> frame-rate-index: 1;
in-out property <int> adaptation-policy-index: 1;
in property <bool> capture-method-visible: false;
in-out property <int> capture-method-index: 0;
in-out property <int> codec-index: 0;
in-out property <bool> hardware-codec-enabled: true;
in-out property <bool> turn-enabled: true;
in-out property <bool> srtp-enabled: false;
in-out property <bool> force-relay-enabled: false;
changed turn-enabled => {
if !root.turn-enabled { root.force-relay-enabled = false; }
}
in-out property <bool> self-hosted-enabled: false;
in-out property <bool> autostart-enabled: false;
in-out property <bool> daemon-enabled: false;
// Show the preference on every platform; only adapted hosts can change it.
in property <bool> privacy-setting-available: false;
in-out property <bool> privacy-on-connect-enabled: true;
in-out property <string> file-save-path: "";
in-out property <string> server-host: "";
in-out property <string> server-port: "";
in-out property <string> coturn-port: "";
in property <bool> settings-session-active: false;
// Keep the setting visible on every platform. Unsupported builds expose it
// as disabled so the fixed settings layout never turns into a blank row.
@@ -257,6 +319,7 @@ export component MainWindow inherits Window {
callback copy-local-id;
callback toggle-password-visibility;
callback refresh-password;
callback reset-password(string) -> bool;
callback format-remote-id(string) -> string;
callback connect-requested(string);
@@ -269,6 +332,7 @@ export component MainWindow inherits Window {
callback cancel-self-hosted-settings;
callback browse-save-path;
callback open-download;
callback check-for-updates;
callback connection-cancel;
callback connection-acknowledge;
callback connection-submit-password(string, bool);
@@ -288,7 +352,7 @@ export component MainWindow inherits Window {
private property <length> settings-scroll-y: 0px;
in-out property <bool> self-host-settings-open: false;
in-out property <bool> about-open: false;
in-out property <bool> update-open: root.update-available;
in-out property <bool> update-open: false;
in-out property <bool> reset-password-open: false;
in-out property <bool> reset-password-invalid: false;
in-out property <string> new-password-input: "";
@@ -671,19 +735,31 @@ export component MainWindow inherits Window {
IconButton {
x: 174px;
y: 133px;
width: 29px;
width: 24px;
height: 30px;
icon: root.password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash;
icon-size: 17px;
clicked => { root.toggle-password-visibility(); }
}
IconButton {
x: 214px;
x: 198px;
y: 133px;
width: 29px;
width: 24px;
height: 30px;
icon: FontAwesomeIcons.rotate;
icon-size: 15px;
tooltip: UiStrings.refresh-password;
enabled: !root.password-change-pending;
clicked => { root.refresh-password(); }
}
IconButton {
x: 222px;
y: 133px;
width: 24px;
height: 30px;
icon: FontAwesomeIcons.pen;
icon-size: 15px;
enabled: !root.password-change-pending;
clicked => { root.reset-password-open = true; }
}
}
@@ -1139,25 +1215,11 @@ export component MainWindow inherits Window {
padding: 20px;
spacing: 12px;
DialogPromptText { prompt: UiStrings.new-password; }
Rectangle {
height: 28px;
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: 4px;
LineEdit {
x: 1px;
y: 1.5px;
width: parent.width - 2px;
height: parent.height - 2px;
HorizontalLayout {
alignment: center;
PasswordInput {
text <=> root.new-password-input;
input-type: InputType.password;
}
LineEditFocusUnderlineMask {
x: 5px;
y: parent.height - 3px;
width: parent.width - 10px;
max-length: 6;
}
}
Text { height: root.reset-password-invalid ? 18px : 0px; visible: root.reset-password-invalid; text: UiStrings.invalid-password; color: #d92d20; font-size: ImGuiFontStyle.small; horizontal-alignment: center; }
@@ -1283,30 +1345,10 @@ export component MainWindow inherits Window {
vertical-alignment: center;
}
if !root.connection-validating: Rectangle {
x: 71px; y: 63px; width: 70px; height: 23px;
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: 3px;
connection-password-editor := TextInput {
x: 5px; y: 1px; width: parent.width - 10px; height: parent.height - 2px;
text <=> root.connection-password;
// The legacy ImGui field uses CharsNoBlank without the
// Password flag, so entered text remains visible.
input-type: InputType.text;
color: #202124;
font-size: ImGuiFontStyle.base;
vertical-alignment: center;
selection-foreground-color: white;
selection-background-color: #2463c7;
accepted => { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
}
Timer {
interval: 1ms;
running: true;
triggered => { connection-password-editor.focus(); }
}
if !root.connection-validating: PasswordInput {
x: 71px; y: 63px;
text <=> root.connection-password;
accepted => { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
}
if !root.connection-validating: Rectangle {
@@ -1446,20 +1488,10 @@ export component MainWindow inherits Window {
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
}
Rectangle {
if root.capture-method-visible: Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.adaptation-policy; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 91px : 160px; y: 2px; width: root.compact-language ? 88px : 87px; enabled: !root.settings-session-active; model: [UiStrings.priority-frame-rate, UiStrings.priority-quality, UiStrings.priority-balanced]; current-index <=> root.adaptation-policy-index; }
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.capture-method; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.capture-method-auto, "DXGI", "WGC", "GDI"]; current-index <=> root.capture-method-index; }
}
Rectangle {
height: 30px;
@@ -1478,8 +1510,13 @@ export component MainWindow inherits Window {
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.force-relay; color: root.turn-enabled ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.force-relay-enabled; enabled: root.turn-enabled && !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.privacy-on-connect; color: root.privacy-setting-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; }
}
Rectangle {
height: 30px;
@@ -1518,7 +1555,7 @@ export component MainWindow inherits Window {
x: root.compact-language ? 190px : 140px;
y: 105px;
width: root.compact-language ? 260px : 360px;
height: 168px;
height: 138px;
border-radius: 7px;
Rectangle {
@@ -1558,21 +1595,8 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
}
Text { x: 8px; y: 91px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.coturn-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
Rectangle {
x: root.compact-language ? 100px : 173px;
y: 90px;
width: root.compact-language ? 152px : 179px;
height: 24px;
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: 4px;
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.coturn-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
}
CompactButton { x: root.compact-language ? 93px : 135px; y: 130px; primary: true; text: UiStrings.ok; clicked => { root.save-self-hosted-settings(); root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 134px : 176px; y: 130px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 93px : 135px; y: 100px; primary: true; text: UiStrings.ok; clicked => { root.save-self-hosted-settings(); root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 134px : 176px; y: 100px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
}
}
@@ -1582,10 +1606,10 @@ export component MainWindow inherits Window {
z: 46;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: 170px;
y: 127px;
width: 300px;
height: 196px;
x: (root.width - self.width) / 2;
y: (root.height - self.height) / 2;
width: 320px;
height: 216px;
border-radius: 7px;
Rectangle {
@@ -1599,19 +1623,81 @@ export component MainWindow inherits Window {
Rectangle { y: parent.height - 7px; height: 7px; background: #d3d3d3; }
Text { x: 6px; text: UiStrings.about; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
}
Text { x: 30px; y: 40px; width: 240px; height: 20px; text: UiStrings.version + ": CrossDesk " + root.current-version; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
if root.update-available: Text { x: 30px; y: 60px; width: 240px; height: 20px; text: UiStrings.update-available + ":"; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
if root.update-available: Rectangle {
x: 30px;
y: 60px;
width: 240px;
height: 20px;
version-link := Text { text: root.latest-version; color: version-touch.has-hover ? #174ea6 : #334fd1; font-size: ImGuiFontStyle.body; horizontal-alignment: center; vertical-alignment: center; }
version-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
// Keep the dialog/footer stable while visible text rows determine
// each group's size and the layout redistributes the space between.
about-content := VerticalLayout {
padding: 16px;
padding-top: 35px;
spacing: 12px;
alignment: space-between;
VerticalLayout {
vertical-stretch: 0;
spacing: 4px;
Text {
text: "CrossDesk " + root.current-version;
color: #202124;
font-size: ImGuiFontStyle.base;
font-weight: 600;
min-height: 18px;
overflow: elide;
}
if root.update-available: Rectangle {
height: max(18px, version-link.preferred-height);
accessible-role: button;
accessible-label: UiStrings.release-notes + " " + root.latest-version;
accessible-action-default => { version-touch.clicked(); }
version-link := Text {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
text: UiStrings.update-available + ": " + root.latest-version;
color: version-touch.has-hover ? #174ea6 : #334fd1;
font-size: ImGuiFontStyle.body;
vertical-alignment: center;
horizontal-alignment: left;
wrap: word-wrap;
}
version-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.about-open = false; root.update-open = true; }
}
}
if root.update-status-text != "": Text {
text: root.update-status-text;
color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e : #61656c;
font-size: ImGuiFontStyle.body;
min-height: 18px;
wrap: word-wrap;
}
}
VerticalLayout {
vertical-stretch: 0;
spacing: 3px;
Rectangle { height: 1px; background: #e6e8eb; }
Text { height: 16px; text: UiStrings.copyright; color: #61656c; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
Text { height: 16px; text: UiStrings.license; color: #61656c; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
}
HorizontalLayout {
vertical-stretch: 0;
alignment: end;
spacing: 8px;
CompactButton {
width: max(76px, check-button-metrics.preferred-width + 20px);
text: UiStrings.check-for-updates;
enabled: root.update-check-status != UpdateCheckStatus.checking;
accessible-role: button;
accessible-label: self.text;
accessible-action-default => { if self.enabled { root.check-for-updates(); } }
clicked => { root.check-for-updates(); }
check-button-metrics := Text { visible: false; text: UiStrings.check-for-updates; font-size: ImGuiFontStyle.body; }
}
CompactButton { width: 48px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
}
}
Text { x: 30px; y: 85px; width: 240px; height: 20px; text: UiStrings.copyright; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
Text { x: 30px; y: 105px; width: 240px; height: 20px; text: UiStrings.license; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
CompactButton { x: 133px; y: 145px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
}
}
@@ -7,6 +7,7 @@ export struct ControllerEntry {
export component ServerWindow inherits Window {
title: "CrossDesk";
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
preferred-width: root.language-index == 0 ? 250px : root.language-index == 1 ? 330px : 430px;
preferred-height: 150px;
min-width: root.language-index == 0 ? 250px : root.language-index == 1 ? 330px : 430px;
+421 -110
View File
@@ -23,8 +23,22 @@ export struct NetworkStatsRow {
}
export global StreamStrings {
in-out property <string> video-settings: "Video settings";
in-out property <string> video-settings-unavailable: "The remote device does not support live video settings";
in-out property <string> video-quality: "Video quality";
in-out property <string> frame-rate: "Capture frame rate";
in-out property <string> adaptation-policy: "Video preference";
in-out property <string> quality-low: "Low";
in-out property <string> quality-medium: "Medium";
in-out property <string> quality-high: "High";
in-out property <string> priority-frame-rate: "Frame rate";
in-out property <string> priority-quality: "Quality";
in-out property <string> priority-balanced: "Balanced";
in-out property <string> select-display: "Select Display";
in-out property <string> send-shortcut: "Send Shortcut";
in-out property <string> privacy-enable: "Enable privacy screen";
in-out property <string> privacy-disable: "Turn privacy off / recover";
in-out property <string> control-mouse: "Control Mouse";
in-out property <string> release-mouse: "Release Mouse";
in-out property <string> audio: "Audio Capture";
@@ -42,7 +56,17 @@ export global StreamStrings {
in-out property <string> stats-out: "Out";
in-out property <string> stats-loss-rate: "Loss Rate";
in-out property <string> stats-resolution: "Res";
in-out property <string> stats-video-latency: "Video delay";
in-out property <string> stats-connection-latency: "Connection RTT";
in-out property <string> stats-connection-mode: "Mode";
in-out property <string> stats-encryption: "Encryption";
in-out property <string> stats-encryption-enabled: "Enabled";
in-out property <string> stats-encryption-disabled: "Disabled";
}
struct ControlBarTooltip {
text: string,
anchor-x: length,
}
// The legacy ImGui control bar uses 24px buttons with Font Awesome rendered
@@ -54,19 +78,27 @@ component ControlBarButton inherits Rectangle {
in property <string> tooltip: "";
in property <bool> selected: false;
in property <bool> slashed: false;
in property <bool> action-enabled: true;
// Capability and temporary command availability have different visuals.
in property <bool> muted: false;
in property <length> icon-size: 16px;
out property <bool> has-hover: touch.has-hover;
out property <ControlBarTooltip> tooltip-info: {
text: root.visible && touch.has-hover && !touch.pressed ? root.tooltip : "",
anchor-x: root.x + root.width / 2,
};
callback clicked;
width: 24px;
height: 24px;
background: touch.pressed ? #0f87fa
background: root.muted ? #eef0f3 : touch.pressed && root.action-enabled ? #0f87fa
: touch.has-hover ? #4296fa
: root.selected ? #4296fa : #4296fa66;
border-radius: 3px;
Text {
text: root.icon;
color: #24272d;
color: root.muted ? #8a8f96 : #24272d;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: root.icon-size;
@@ -85,32 +117,61 @@ component ControlBarButton inherits Rectangle {
font-size: 11px;
}
// Match the two offset diagonal strokes used by the ImGui mouse/audio
// disabled state instead of covering the glyph with a full-width slash.
if root.slashed: Rectangle {
x: 11px;
y: 0px;
width: 2px;
height: 24px;
background: black;
transform-rotation: -45deg;
// Draw the two offset disabled-state strokes as paths. Rotated rectangles
// stay vertical in the software renderer used for fallback and UI capture.
if root.slashed: Path {
width: 24px; height: 24px;
viewbox-width: 24; viewbox-height: 24;
commands: "M 3.5 3.5 L 20.5 20.5";
stroke: black;
stroke-width: 2px;
}
if root.slashed: Rectangle {
x: 9.5px;
y: 1.5px;
width: 2px;
height: 24px;
background: touch.has-hover ? #4296fa : #b3d5fd;
transform-rotation: -45deg;
if root.slashed: Path {
width: 24px; height: 24px;
viewbox-width: 24; viewbox-height: 24;
commands: "M 2 5 L 19 22";
stroke: touch.has-hover ? #4296fa : #b3d5fd;
stroke-width: 2px;
}
touch := TouchArea {
clicked => { root.clicked(); }
// Keep disabled controls hit-testable so clicks cannot reach the peer.
clicked => { if root.action-enabled { root.clicked(); } }
}
}
component VideoSettingRow inherits VerticalLayout {
in property <string> label;
in property <[string]> options;
in property <int> selected;
in property <bool> enabled;
callback choose(int);
callback pressed;
spacing: 4px;
Text { text: root.label; color: #5c626c; font-size: 11px; height: 18px; }
HorizontalLayout {
spacing: 4px;
for option[index] in root.options: Rectangle {
height: 27px;
background: !root.enabled ? #f1f1f1 : root.selected == index ? #d7eaff
: option-touch.has-hover ? #eaf3ff : #f5f6f8;
border-radius: 4px;
border-width: 1px;
border-color: root.enabled && root.selected == index ? #4296fa : #dce0e6;
Text { text: option; color: root.enabled ? #30343b : #8b8f95;
font-size: 11px; horizontal-alignment: center; vertical-alignment: center; }
option-touch := TouchArea {
pointer-event(event) => { if event.kind == PointerEventKind.down { root.pressed(); } }
clicked => { if root.enabled { root.choose(index); } }
scroll-event(event) => { accept }
}
}
}
}
export component StreamWindow inherits Window {
title: "CrossDesk";
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
preferred-width: 1280px;
preferred-height: 720px + (root.custom-titlebar ? 32px : 0px);
min-width: 640px;
@@ -122,6 +183,11 @@ export component StreamWindow inherits Window {
in property <bool> custom-titlebar: false;
in property <bool> native-video-enabled: false;
// Export the actual input surface in logical pixels. Native rendering and
// input mapping must follow the same layout, including title and tab bars.
out property <length> video-area-top: content-layer.y + video.y;
out property <length> video-area-width: video.width;
out property <length> video-area-height: video.height;
in property <bool> window-active: true;
in property <bool> window-maximized: false;
in property <[StreamTab]> tabs;
@@ -133,17 +199,31 @@ export component StreamWindow inherits Window {
in property <[string]> displays;
in-out property <int> selected-display: 0;
in property <bool> mouse-control-enabled: true;
in property <bool> privacy-can-toggle: false;
in property <bool> privacy-active: false;
in property <bool> privacy-off: false;
in property <bool> privacy-unsupported: false;
// Privacy feedback stays on its toolbar button and hover tooltip.
in property <string> privacy-status-text: "Privacy screen";
// Numeric values mirror crossdesk::RemoteCursorShape and Slint's MouseCursor.
in property <bool> remote-cursor-active: false;
in property <int> remote-cursor-shape: 0;
in property <bool> audio-enabled: true;
in property <bool> srtp-enabled: false;
in property <bool> video-settings-enabled: false;
in property <string> video-settings-feedback: "";
in-out property <int> video-quality-index: 2;
in-out property <int> frame-rate-index: 1;
in-out property <int> adaptation-policy-index: 1;
callback change-video-settings(int, int, int);
in property <bool> fullscreen-enabled: false;
in property <bool> stats-visible: false;
in property <[NetworkStatsRow]> stats-rows;
in property <string> stats-fps: "0";
in property <string> stats-resolution: "";
in property <string> stats-video-latency: "—";
in property <string> stats-connection-latency: "—";
in property <string> stats-connection-mode: "";
in property <bool> stats-encryption-enabled: false;
in property <bool> file-transfer-visible: false;
in property <[FileTransferEntry]> file-transfers;
@@ -153,6 +233,7 @@ export component StreamWindow inherits Window {
callback switch-display(int);
callback send-shortcut(string);
callback toggle-mouse-control;
callback toggle-privacy;
callback toggle-audio;
callback select-file;
callback close-file-transfer;
@@ -171,10 +252,45 @@ export component StreamWindow inherits Window {
callback keyboard-focus-changed(bool);
private property <bool> control-expanded: true;
private property <length> control-toolbar-height: 38px;
private property <length> control-button-step: 32px;
private property <length> stats-row-height: 18px;
private property <length> stats-header-height: 18px;
private property <length> stats-content-height:
root.stats-header-height + root.stats-rows.length * root.stats-row-height + 65px;
private property <bool> display-menu-open: false;
private property <bool> display-menu-animation-enabled: false;
private property <float> display-menu-progress: 0.0;
private property <bool> shortcut-menu-open: false;
in-out property <bool> video-menu-open: false;
private property <length> video-menu-width: min(320px, video.width - 8px);
private property <length> video-menu-height: root.video-settings-enabled && root.video-settings-feedback == "" ? 182px : 220px;
private property <length> video-menu-x: min(max(4px, control.x + video-button.x), video.width - root.video-menu-width - 4px);
private property <length> video-menu-y: control.y + root.control-toolbar-height + root.video-menu-height <= video.height
? control.y + root.control-toolbar-height - 1px : max(0px, control.y - root.video-menu-height);
changed selected-tab => { root.video-menu-open = false; }
changed control-expanded => { if !root.control-expanded { root.video-menu-open = false; } }
changed window-active => { if !root.window-active { root.video-menu-open = false; } }
private property <bool> control-tooltip-visible: false;
private property <ControlBarTooltip> hovered-control: {
if root.window-active && !root.control-dragging
&& !root.display-menu-open && !root.shortcut-menu-open && !root.video-menu-open {
if collapse-button.has-hover { return collapse-button.tooltip-info; }
if root.control-expanded {
if display-button.has-hover { return display-button.tooltip-info; }
if shortcut-button.has-hover { return shortcut-button.tooltip-info; }
if mouse-button.has-hover { return mouse-button.tooltip-info; }
if privacy-button.has-hover { return privacy-button.tooltip-info; }
if video-button.has-hover { return video-button.tooltip-info; }
if audio-button.has-hover { return audio-button.tooltip-info; }
if file-button.has-hover { return file-button.tooltip-info; }
if stats-button.has-hover { return stats-button.tooltip-info; }
if fullscreen-button.has-hover { return fullscreen-button.tooltip-info; }
if close-button.has-hover { return close-button.tooltip-info; }
}
}
return { text: "", anchor-x: 0px };
}
// A popup can remove itself from the scene in its clicked callback before
// the video input layer observes the same pointer release. Remember that
// the press belonged to local UI so that release cannot reach the peer.
@@ -214,27 +330,44 @@ export component StreamWindow inherits Window {
}
}
changed hovered-control => {
root.control-tooltip-visible = false;
control-tooltip-timer.running = root.hovered-control.text != "";
if root.hovered-control.text != "" { control-tooltip-timer.restart(); }
}
control-tooltip-timer := Timer {
interval: 450ms;
running: false;
triggered => {
self.running = false;
root.control-tooltip-visible = true;
}
}
is-local-control-area(pointer-x, pointer-y) => {
let over-control = pointer-x >= control.x
&& pointer-x <= control.x + control.width
&& pointer-y >= control.y
&& pointer-y <= control.y + control.height;
let display-menu-x = control.x + (root.control-docked-left ? 9px : 42px);
let display-menu-y = control.y + control.height - 1px;
let display-menu-y = control.y + root.control-toolbar-height - 1px;
let display-menu-height = min(180px, root.displays.length * 30px + 8px);
let over-display-menu = root.display-menu-open
&& pointer-x >= display-menu-x
&& pointer-x <= display-menu-x + 180px
&& pointer-y >= display-menu-y
&& pointer-y <= display-menu-y + display-menu-height;
let shortcut-menu-x = control.x + (root.control-docked-left ? 41px : 74px);
let shortcut-menu-x = control.x + shortcut-button.x;
let shortcut-menu-y = control.y + 40px;
let over-shortcut-menu = root.shortcut-menu-open
&& pointer-x >= shortcut-menu-x
&& pointer-x <= shortcut-menu-x + 150px
&& pointer-x <= shortcut-menu-x + 180px
&& pointer-y >= shortcut-menu-y
&& pointer-y <= shortcut-menu-y + 68px;
return over-control || over-display-menu || over-shortcut-menu;
let over-video-menu = root.video-menu-open
&& pointer-x >= root.video-menu-x && pointer-x <= root.video-menu-x + root.video-menu-width
&& pointer-y >= root.video-menu-y && pointer-y <= root.video-menu-y + root.video-menu-height;
return over-control || over-display-menu || over-shortcut-menu || over-video-menu;
}
begin-control-drag(press-x, press-y) => {
@@ -253,6 +386,7 @@ export component StreamWindow inherits Window {
root.display-menu-progress = 0.0;
root.display-menu-open = false;
root.shortcut-menu-open = false;
root.video-menu-open = false;
root.control-dragging = true;
}
move-control-drag(pointer-x, pointer-y) => {
@@ -449,20 +583,8 @@ export component StreamWindow inherits Window {
width: parent.width - 27px;
height: parent.height;
background: transparent;
if root.srtp-enabled: Text {
x: 0px;
width: 12px;
height: parent.height;
text: FontAwesomeIcons.shield-halved;
color: #30343b;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: ImGuiFontStyle.base;
horizontal-alignment: center;
vertical-alignment: center;
}
Text {
x: root.srtp-enabled ? 16px : 0px;
x: 0px;
width: parent.width - self.x;
height: parent.height;
text: tab.title;
@@ -638,8 +760,10 @@ export component StreamWindow inherits Window {
control := Rectangle {
x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width;
y: min(max(0px, root.control-pos-y), parent.height - self.height);
width: root.control-expanded ? min(300px, parent.width) : 20px;
height: root.stats-visible && root.control-expanded ? min(180px, parent.height) : 38px;
width: root.control-expanded ? min(76px + 9 * root.control-button-step, parent.width) : 20px;
height: root.stats-visible && root.control-expanded
? min(root.control-toolbar-height + root.stats-content-height + 8px, parent.height)
: root.control-toolbar-height;
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
@@ -656,7 +780,8 @@ export component StreamWindow inherits Window {
background: ImGuiLineStyle.control-separator;
}
if root.control-expanded: display-button := ControlBarButton {
display-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 9px : 42px; y: 7px;
icon: FontAwesomeIcons.display;
badge: root.selected-display + 1;
@@ -667,13 +792,15 @@ export component StreamWindow inherits Window {
root.display-menu-progress = 0.0;
root.display-menu-open = !root.display-menu-open;
root.shortcut-menu-open = false;
root.video-menu-open = false;
if root.display-menu-open {
display-menu-open-timer.running = true;
}
}
}
if root.control-expanded: shortcut-button := ControlBarButton {
x: root.control-docked-left ? 41px : 74px; y: 7px;
shortcut-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + root.control-button-step; y: 7px;
icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut;
clicked => {
display-menu-open-timer.running = false;
@@ -681,49 +808,67 @@ export component StreamWindow inherits Window {
root.display-menu-progress = 0.0;
root.shortcut-menu-open = !root.shortcut-menu-open;
root.display-menu-open = false;
root.video-menu-open = false;
}
}
if root.control-expanded: mouse-button := ControlBarButton {
x: root.control-docked-left ? 73px : 106px; y: 7px;
mouse-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 2 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled;
tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse;
clicked => { root.toggle-mouse-control(); }
}
if root.control-expanded: audio-button := ControlBarButton {
x: root.control-docked-left ? 105px : 138px; y: 7px;
audio-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 4 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled;
tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio;
clicked => { root.toggle-audio(); }
}
if root.control-expanded: file-button := ControlBarButton {
x: root.control-docked-left ? 137px : 170px; y: 7px;
file-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 5 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file;
clicked => { root.select-file(); }
}
if root.control-expanded: stats-button := ControlBarButton {
x: root.control-docked-left ? 169px : 202px; y: 7px;
video-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 6 * root.control-button-step; y: 7px;
icon: "\u{f1de}"; selected: root.video-menu-open;
tooltip: StreamStrings.video-settings;
clicked => {
root.video-menu-open = !root.video-menu-open;
root.display-menu-open = false;
root.shortcut-menu-open = false;
}
}
stats-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 7 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.signal; selected: root.stats-visible;
tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats;
clicked => { root.toggle-stats(); }
}
if root.control-expanded: fullscreen-button := ControlBarButton {
x: root.control-docked-left ? 201px : 234px; y: 7px;
fullscreen-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 8 * root.control-button-step; y: 7px;
icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen;
clicked => { root.toggle-fullscreen(); }
}
if root.control-expanded: close-button := ControlBarButton {
x: root.control-docked-left ? 233px : 266px; y: 7px;
close-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 9 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
clicked => { root.disconnect(); }
}
if root.control-expanded && root.control-docked-left: Rectangle {
x: 267px; y: 12px; width: 2px; height: 15px;
x: parent.width - 33px; y: 12px; width: 2px; height: 15px;
background: ImGuiLineStyle.control-separator;
}
collapse-button := ControlBarButton {
x: root.control-expanded ? (root.control-docked-left ? 275px : 10px) : 2px;
x: root.control-expanded ? (root.control-docked-left ? parent.width - 25px : 10px) : 2px;
y: 7px;
width: 15px;
icon: root.control-expanded
@@ -733,76 +878,134 @@ export component StreamWindow inherits Window {
clicked => { root.control-expanded = !root.control-expanded; }
}
privacy-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 3 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.user-secret;
slashed: root.privacy-off && !root.privacy-unsupported;
muted: root.privacy-unsupported;
action-enabled: root.privacy-can-toggle;
tooltip: root.privacy-status-text
+ (root.privacy-can-toggle ? "\n" + (root.privacy-active
? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : "");
clicked => { root.toggle-privacy(); }
}
if root.stats-visible && root.control-expanded: stats-table := Rectangle {
x: 14px;
y: 38px;
width: parent.width - 28px;
height: 128px;
x: 8px;
y: root.control-toolbar-height + 2px;
width: parent.width - 16px;
height: root.stats-content-height;
background: transparent;
private property <length> row-height: 16px;
private property <length> label-column-width: 60px;
private property <length> loss-column-width: 66px;
private property <length> row-height: root.stats-row-height;
private property <length> label-column-width: 50px;
private property <length> loss-column-width:
max((self.width - self.label-column-width) / 3,
stats-loss-label.preferred-width + 8px);
private property <length> value-column-width:
(self.width - self.label-column-width - self.loss-column-width) / 2;
private property <length> details-right-width:
right-label-width-probe.preferred-width + 6px
+ max(resolution-width-probe.preferred-width,
connection-latency-width-probe.preferred-width,
encryption-width-probe.preferred-width
+ (root.stats-encryption-enabled ? 14px : 0px));
private property <length> details-right-x:
min(self.width / 2, self.width - self.details-right-width - 6px);
Rectangle {
y: 0px;
height: 1px;
background: ImGuiLineStyle.border;
left-label-width-probe := Text {
visible: false;
text: "FPS\n" + StreamStrings.stats-video-latency
+ "\n" + StreamStrings.stats-connection-mode;
font-size: ImGuiFontStyle.small;
}
right-label-width-probe := Text {
visible: false;
text: StreamStrings.stats-resolution + "\n" + StreamStrings.stats-connection-latency
+ "\n" + StreamStrings.stats-encryption;
font-size: ImGuiFontStyle.small;
}
resolution-width-probe := Text {
visible: false;
text: root.stats-resolution;
font-size: ImGuiFontStyle.body;
font-weight: 600;
}
connection-latency-width-probe := Text {
visible: false;
text: root.stats-connection-latency;
font-size: ImGuiFontStyle.body;
font-weight: 600;
}
encryption-width-probe := Text {
visible: false;
text: root.stats-encryption-enabled
? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled;
font-size: ImGuiFontStyle.body;
font-weight: 600;
}
Rectangle {
x: 0px;
y: 0px;
height: stats-table.row-height;
width: parent.width;
height: root.stats-header-height;
background: #f3f5f8;
border-radius: 4px;
Text {
x: stats-table.label-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: StreamStrings.stats-in;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
horizontal-alignment: right;
vertical-alignment: center;
}
Text {
x: stats-table.label-column-width + stats-table.value-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: StreamStrings.stats-out;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
horizontal-alignment: right;
vertical-alignment: center;
}
Text {
stats-loss-label := Text {
x: stats-table.label-column-width + stats-table.value-column-width * 2 + 4px;
width: stats-table.loss-column-width - 8px;
text: StreamStrings.stats-loss-rate;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: ImGuiLineStyle.border;
}
}
for row[index] in root.stats-rows: Rectangle {
y: (index + 1) * stats-table.row-height;
x: 0px;
y: root.stats-header-height + index * stats-table.row-height;
width: parent.width;
height: stats-table.row-height;
background: index == root.stats-rows.length - 1 ? #edf3fc : transparent;
border-radius: 4px;
Text {
x: 4px;
width: stats-table.label-column-width - 8px;
text: row.label;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #475467;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
vertical-alignment: center;
}
Text {
x: stats-table.label-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: row.inbound;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #253247;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
@@ -810,8 +1013,10 @@ export component StreamWindow inherits Window {
x: stats-table.label-column-width + stats-table.value-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: row.outbound;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #253247;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
@@ -819,51 +1024,133 @@ export component StreamWindow inherits Window {
x: stats-table.label-column-width + stats-table.value-column-width * 2 + 4px;
width: stats-table.loss-column-width - 8px;
text: row.loss-rate;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #475467;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
Rectangle {
if index < root.stats-rows.length - 2: Rectangle {
x: 4px;
y: parent.height - 1px;
width: parent.width - 8px;
height: 1px;
background: ImGuiLineStyle.border;
background: #edf0f4;
}
}
for metric[index] in [
{ label: "FPS:", value: root.stats-fps },
{ label: StreamStrings.stats-resolution + ":", value: root.stats-resolution },
{ label: StreamStrings.stats-connection-mode + ":", value: root.stats-connection-mode },
{ label: "FPS", value: root.stats-fps },
{ label: StreamStrings.stats-resolution, value: root.stats-resolution },
{ label: StreamStrings.stats-video-latency, value: root.stats-video-latency },
{ label: StreamStrings.stats-connection-latency, value: root.stats-connection-latency },
{ label: StreamStrings.stats-connection-mode, value: root.stats-connection-mode },
{ label: StreamStrings.stats-encryption, value: root.stats-encryption-enabled
? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled },
]: Rectangle {
y: (index + 5) * stats-table.row-height;
height: stats-table.row-height;
Text {
x: 4px;
width: stats-table.label-column-width - 8px;
private property <length> encryption-icon-width:
index == 5 && root.stats-encryption-enabled ? 14px : 0px;
x: mod(index, 2) == 1 ? stats-table.details-right-x : 6px;
y: root.stats-header-height + root.stats-rows.length * stats-table.row-height
+ 2px + floor(index / 2) * 21px;
width: mod(index, 2) == 1 ? stats-table.details-right-width
: stats-table.details-right-x - 14px;
height: 21px;
metric-label := Text {
x: 0px;
y: 0px;
width: mod(index, 2) == 1 ? right-label-width-probe.preferred-width
: left-label-width-probe.preferred-width;
height: parent.height;
text: metric.label;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
vertical-alignment: center;
}
Text {
x: stats-table.label-column-width + 4px;
width: stats-table.value-column-width - 8px;
metric-value := Text {
x: metric-label.width + 6px;
y: 0px;
height: parent.height;
width: parent.width - self.x - parent.encryption-icon-width;
text: metric.value;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: index == 5 && root.stats-encryption-enabled ? #18794e : #253247;
font-size: ImGuiFontStyle.body;
font-weight: 600;
horizontal-alignment: left;
vertical-alignment: center;
overflow: elide;
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: ImGuiLineStyle.border;
if index == 5 && root.stats-encryption-enabled: Text {
x: metric-value.x + min(metric-value.preferred-width, metric-value.width) + 4px;
y: metric-value.y + (metric-value.height - self.height) / 2;
width: 10px;
height: 12px;
text: FontAwesomeIcons.shield-halved;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 10px;
color: #238653;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
}
// While the menu is open, outside gestures dismiss it locally.
// Keep the backdrop through pointer-up; otherwise Slint's implicit
// pointer move or release can reach the remote input surface.
if root.video-menu-open: Rectangle {
width: parent.width; height: parent.height; z: 9;
TouchArea {
pointer-event(event) => {
if event.kind == PointerEventKind.down {
root.suppress-next-remote-pointer-release = true;
}
}
clicked => { root.video-menu-open = false; }
scroll-event(event) => { accept }
}
}
if root.video-menu-open: Rectangle {
x: root.video-menu-x; y: root.video-menu-y;
width: root.video-menu-width; height: root.video-menu-height;
background: white; border-width: 1px; border-color: ImGuiLineStyle.border;
border-radius: 5px; z: 11;
TouchArea {
pointer-event(event) => { if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; } }
scroll-event(event) => { accept }
}
VerticalLayout {
padding: 10px; spacing: 8px;
VideoSettingRow {
label: StreamStrings.video-quality;
options: [StreamStrings.quality-low, StreamStrings.quality-medium, StreamStrings.quality-high];
selected: root.video-quality-index; enabled: root.video-settings-enabled;
pressed => { root.suppress-next-remote-pointer-release = true; }
choose(index) => { root.change-video-settings(index, root.frame-rate-index, root.adaptation-policy-index); }
}
VideoSettingRow {
label: StreamStrings.frame-rate; options: ["30 fps", "60 fps"];
selected: root.frame-rate-index; enabled: root.video-settings-enabled;
pressed => { root.suppress-next-remote-pointer-release = true; }
choose(index) => { root.change-video-settings(root.video-quality-index, index, root.adaptation-policy-index); }
}
VideoSettingRow {
label: StreamStrings.adaptation-policy;
options: [StreamStrings.priority-frame-rate, StreamStrings.priority-quality, StreamStrings.priority-balanced];
selected: root.adaptation-policy-index; enabled: root.video-settings-enabled;
pressed => { root.suppress-next-remote-pointer-release = true; }
choose(index) => { root.change-video-settings(root.video-quality-index, root.frame-rate-index, index); }
}
if !root.video-settings-enabled || root.video-settings-feedback != "": Text {
text: root.video-settings-enabled ? root.video-settings-feedback : StreamStrings.video-settings-unavailable; wrap: word-wrap;
color: #8b8f95; font-size: 11px;
}
}
}
if root.display-menu-open: display-menu := Rectangle {
private property <length> expanded-height: min(180px, root.displays.length * 30px + 8px);
private property <int> hovered-index:
@@ -871,9 +1158,9 @@ export component StreamWindow inherits Window {
&& floor((display-touch.mouse-y - 4px) / 30px) < root.displays.length
? floor((display-touch.mouse-y - 4px) / 30px) : -1;
x: control.x + (root.control-docked-left ? 9px : 42px);
// Overlap the control border by one pixel so the menu reads as
// an attached panel growing downward from the display button.
y: control.y + control.height - 1px;
// Anchor to the button row, independent of the expanded stats.
// Overlap its bottom edge by one pixel to keep the menu attached.
y: control.y + root.control-toolbar-height - 1px;
width: 180px;
height: self.expanded-height * root.display-menu-progress;
opacity: root.display-menu-progress;
@@ -942,7 +1229,7 @@ export component StreamWindow inherits Window {
}
if root.shortcut-menu-open: Rectangle {
x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 150px; height: 68px;
x: control.x + shortcut-button.x; y: control.y + 40px; width: 180px; height: 68px;
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11;
VerticalLayout {
padding: 4px; spacing: 1px;
@@ -961,6 +1248,30 @@ export component StreamWindow inherits Window {
}
}
// Measure unwrapped text so short hints stay compact. Render outside
// the clipped control bar, capping longer hints before wrapping.
control-tooltip-width-probe := Text {
visible: false;
text: root.hovered-control.text;
font-size: ImGuiFontStyle.body;
}
if root.hovered-control.text != "" && root.control-tooltip-visible: Rectangle {
width: min(240px, video.width - 8px,
control-tooltip-width-probe.preferred-width + 16px);
x: min(max(4px, control.x + root.hovered-control.anchor-x - self.width / 2),
video.width - self.width - 4px);
height: control-tip.preferred-height + 12px;
// Anchor to the button row so expanded stats stay beneath the tooltip.
y: control.y + root.control-toolbar-height + self.height + 8px <= video.height
? control.y + root.control-toolbar-height + 4px : max(4px, control.y - self.height - 4px);
background: #30343bf5; border-radius: 5px; z: 12;
control-tip := Text {
x: 8px; y: 6px; width: parent.width - 16px;
text: root.hovered-control.text;
color: white; font-size: ImGuiFontStyle.body; wrap: word-wrap;
}
}
// Transfer progress overlay at bottom-left.
if root.file-transfer-visible: Rectangle {
x: 18px; y: parent.height - min(210px, parent.height * 0.35); width: min(430px, parent.width * 0.6); height: min(190px, parent.height * 0.32);
+49 -20
View File
@@ -1,5 +1,11 @@
#ifndef CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
#define CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _UI_LOCALIZATION_H_
#define _UI_LOCALIZATION_H_
#include <slint.h>
@@ -30,6 +36,7 @@ inline int ApplyMainWindowStrings(
strings.set_online(Text(localization::online[language]));
strings.set_offline(Text(localization::offline[language]));
strings.set_password(Text(localization::password[language]));
strings.set_refresh_password(Text(localization::refresh_password[language]));
strings.set_copied(
Text(localization::local_id_copied_to_clipboard[language]));
strings.set_connect(Text(localization::connect[language]));
@@ -46,25 +53,19 @@ inline int ApplyMainWindowStrings(
strings.set_confirm_delete(
Text(localization::confirm_delete_connection[language]));
strings.set_language(Text(localization::language[language]));
strings.set_video_quality(Text(localization::video_quality[language]));
strings.set_frame_rate(Text(localization::video_frame_rate[language]));
strings.set_adaptation_policy(
Text(localization::video_adaptation_policy[language]));
strings.set_priority_frame_rate(
Text(localization::video_priority_frame_rate[language]));
strings.set_priority_quality(
Text(localization::video_priority_quality[language]));
strings.set_priority_balanced(
Text(localization::video_priority_balanced[language]));
strings.set_capture_method(Text(localization::screen_capture_method[language]));
strings.set_capture_method_auto(
Text(localization::screen_capture_method_auto[language]));
strings.set_codec(Text(localization::video_encode_format[language]));
strings.set_hardware_codec(
Text(localization::enable_hardware_video_codec[language]));
strings.set_turn_relay(Text(localization::enable_turn[language]));
strings.set_srtp(Text(localization::enable_srtp[language]));
strings.set_force_relay(Text(localization::force_relay[language]));
strings.set_self_hosted(
Text(localization::self_hosted_server_config[language]));
strings.set_autostart(Text(localization::enable_autostart[language]));
strings.set_daemon(Text(localization::enable_daemon[language]));
strings.set_privacy_on_connect(Text(localization::privacy_on_connect[language]));
strings.set_file_save_path(
Text(localization::file_transfer_save_path[language]));
strings.set_default_desktop(Text(localization::default_desktop[language]));
@@ -72,8 +73,6 @@ inline int ApplyMainWindowStrings(
Text(localization::self_hosted_server_address[language]));
strings.set_server_port(
Text(localization::self_hosted_server_port[language]));
strings.set_coturn_port(
Text(localization::self_hosted_server_coturn_server_port[language]));
strings.set_version(Text(localization::version[language]));
strings.set_signal_connected(Text(localization::signal_connected[language]));
strings.set_signal_disconnected(
@@ -81,6 +80,12 @@ inline int ApplyMainWindowStrings(
strings.set_tls_error(Text(localization::signal_tls_cert_error[language]));
strings.set_update_available(
Text(localization::new_version_available[language]));
strings.set_check_for_updates(Text(localization::check_for_updates[language]));
strings.set_checking_for_updates(
Text(localization::checking_for_updates[language]));
strings.set_up_to_date(Text(localization::up_to_date[language]));
strings.set_update_check_failed(
Text(localization::update_check_failed[language]));
strings.set_release_notes(Text(localization::release_notes[language]));
strings.set_download(Text(localization::update[language]));
strings.set_input_password(Text(localization::input_password[language]));
@@ -112,10 +117,6 @@ inline int ApplyMainWindowStrings(
strings.set_notification(Text(localization::notification[language]));
strings.set_service_suppressed_message(
Text(localization::windows_service_prompt_suppressed_message[language]));
strings.set_quality_low(Text(localization::video_quality_low[language]));
strings.set_quality_medium(
Text(localization::video_quality_medium[language]));
strings.set_quality_high(Text(localization::video_quality_high[language]));
strings.set_codec_h264(Text(localization::h264[language]));
strings.set_codec_av1(Text(localization::av1[language]));
strings.set_self_hosted_settings(
@@ -134,8 +135,27 @@ inline int ApplyStreamWindowStrings(
auto& strings = window->template global<ui::StreamStrings>();
strings.set_select_display(Text(localization::select_display[language]));
strings.set_send_shortcut(Text(localization::send_shortcut[language]));
strings.set_privacy_enable(Text(localization::privacy_enable[language]));
strings.set_privacy_disable(Text(localization::privacy_disable[language]));
strings.set_control_mouse(Text(localization::control_mouse[language]));
strings.set_release_mouse(Text(localization::release_mouse[language]));
strings.set_video_settings(Text(localization::video_settings[language]));
strings.set_video_settings_unavailable(
Text(localization::video_settings_unavailable[language]));
strings.set_video_quality(Text(localization::video_quality[language]));
strings.set_frame_rate(Text(localization::video_frame_rate[language]));
strings.set_adaptation_policy(
Text(localization::video_adaptation_policy[language]));
strings.set_quality_low(Text(localization::video_quality_low[language]));
strings.set_quality_medium(
Text(localization::video_quality_medium[language]));
strings.set_quality_high(Text(localization::video_quality_high[language]));
strings.set_priority_frame_rate(
Text(localization::video_priority_frame_rate[language]));
strings.set_priority_quality(
Text(localization::video_priority_quality[language]));
strings.set_priority_balanced(
Text(localization::video_priority_balanced[language]));
strings.set_audio(Text(localization::audio_capture[language]));
strings.set_mute(Text(localization::mute[language]));
strings.set_select_file(Text(localization::select_file[language]));
@@ -155,11 +175,20 @@ inline int ApplyStreamWindowStrings(
strings.set_stats_out(Text(localization::out[language]));
strings.set_stats_loss_rate(Text(localization::loss_rate[language]));
strings.set_stats_resolution(Text(localization::resolution[language]));
strings.set_stats_video_latency(Text(localization::video_latency[language]));
strings.set_stats_connection_latency(Text(localization::connection_latency[language]));
strings.set_stats_connection_mode(
Text(localization::connection_mode[language]));
strings.set_stats_encryption(
Text(localization::transport_encryption[language]));
strings.set_stats_encryption_enabled(
Text(localization::transport_encryption_enabled[language]));
strings.set_stats_encryption_disabled(
Text(localization::transport_encryption_disabled[language]));
return language;
}
} // namespace crossdesk::ui_localization
#endif // CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
#endif
@@ -1,303 +0,0 @@
#include <random>
#include "application/gui_application.h"
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
namespace crossdesk {
int GuiApplication::LocalWindow() {
ImGuiIO &io = ImGui::GetIO();
float local_window_width = io.DisplaySize.x * 0.5f;
float local_window_height =
io.DisplaySize.y * (1 - TITLE_BAR_HEIGHT - STATUS_BAR_HEIGHT);
float local_window_button_width = io.DisplaySize.x * 0.046f;
float local_window_button_height = io.DisplaySize.y * 0.075f;
ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * TITLE_BAR_HEIGHT),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
ImGui::BeginChild("LocalDesktopWindow",
ImVec2(local_window_width, local_window_height),
ImGuiChildFlags_None,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleColor();
ImGui::SetCursorPos(
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
ImGui::SetWindowFontScale(0.9f);
ImGui::TextColored(
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
localization::local_desktop[localization_language_index_].c_str());
ImGui::Spacing();
{
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.15f),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(239.0f / 255, 240.0f / 255,
242.0f / 255, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, window_rounding_ * 1.5f);
ImGui::BeginChild(
"LocalDesktopPanel",
ImVec2(local_window_width * 0.8f, local_window_height * 0.43f),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
{
ImGui::SetWindowFontScale(0.8f);
ImGui::Text("%s",
localization::local_id[localization_language_index_].c_str());
ImGui::Spacing();
ImGui::SetNextItemWidth(io.DisplaySize.x * 0.25f);
ImGui::SetWindowFontScale(1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
if (strcmp(client_id_display_, client_id_)) {
for (int i = 0, j = 0; i < sizeof(client_id_); i++, j++) {
client_id_display_[j] = client_id_[i];
if (i == 2 || i == 5) {
client_id_display_[++j] = ' ';
}
}
}
ImGui::InputText(
"##local_id", client_id_display_, IM_ARRAYSIZE(client_id_display_),
ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_ReadOnly);
ImGui::PopStyleVar();
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0, 0, 0, 0));
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(ICON_FA_COPY, ImVec2(local_window_button_width,
local_window_button_height))) {
local_id_copied_ = true;
ImGui::SetClipboardText(client_id_);
copy_start_time_ = ImGui::GetTime();
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
double time_duration = ImGui::GetTime() - copy_start_time_;
if (local_id_copied_ && time_duration < 1.0f) {
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleColor(
ImGuiCol_WindowBg,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f - (float)time_duration));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ConnectionStatusWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
ImGui::SetWindowFontScale(0.8f);
std::string text = localization::local_id_copied_to_clipboard
[localization_language_index_];
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.4f);
ImGui::PushStyleColor(ImGuiCol_Text,
ImVec4(0, 0, 0, 1.0f - (float)time_duration));
ImGui::Text("%s", text.c_str());
ImGui::PopStyleColor();
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::SetWindowFontScale(0.8f);
ImGui::Text("%s",
localization::password[localization_language_index_].c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::SetNextItemWidth(io.DisplaySize.x * 0.25f);
ImGui::Spacing();
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
ImGui::InputTextWithHint(
"##server_pwd",
localization::max_password_len[localization_language_index_].c_str(),
password_saved_, IM_ARRAYSIZE(password_saved_),
show_password_
? ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_ReadOnly
: ImGuiInputTextFlags_CharsNoBlank |
ImGuiInputTextFlags_Password |
ImGuiInputTextFlags_ReadOnly);
ImGui::PopStyleVar();
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0, 0, 0, 0));
ImGui::SetWindowFontScale(0.5f);
auto l_x = ImGui::GetCursorScreenPos().x;
auto l_y = ImGui::GetCursorScreenPos().y;
if (ImGui::Button(
show_password_ ? ICON_FA_EYE : ICON_FA_EYE_SLASH,
ImVec2(local_window_button_width, local_window_button_height))) {
show_password_ = !show_password_;
}
ImGui::SameLine();
if (ImGui::Button(ICON_FA_PEN, ImVec2(local_window_button_width,
local_window_button_height))) {
show_reset_password_window_ = true;
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
if (show_reset_password_window_) {
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding,
window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ResetPasswordWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
std::string text =
localization::new_password[localization_language_index_];
ImGui::SetWindowFontScale(0.5f);
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::Text("%s", text.c_str());
ImGui::SetCursorPosX(window_width * 0.33f);
ImGui::SetCursorPosY(window_height * 0.4f);
ImGui::SetNextItemWidth(window_width * 0.33f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
if (focus_on_input_widget_) {
ImGui::SetKeyboardFocusHere();
focus_on_input_widget_ = false;
}
bool enter_pressed = ImGui::InputText(
"##new_password", new_password_, IM_ARRAYSIZE(new_password_),
ImGuiInputTextFlags_CharsNoBlank |
ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::PopStyleVar();
ImGui::SetCursorPosX(window_width * 0.315f);
ImGui::SetCursorPosY(window_height * 0.75f);
// OK
if (ImGui::Button(
localization::ok[localization_language_index_].c_str()) ||
enter_pressed) {
if (6 != strlen(new_password_)) {
LOG_ERROR("Invalid password length");
show_reset_password_window_ = true;
focus_on_input_widget_ = true;
} else {
show_reset_password_window_ = false;
memset(&password_saved_, 0, sizeof(password_saved_));
strncpy(password_saved_, new_password_,
sizeof(password_saved_) - 1);
password_saved_[sizeof(password_saved_) - 1] = '\0';
// if self hosted
if (config_center_->IsSelfHosted()) {
std::string self_hosted_id_str;
if (strlen(self_hosted_id_) > 0) {
const char *at_pos = strchr(self_hosted_id_, '@');
if (at_pos != nullptr) {
self_hosted_id_str =
std::string(self_hosted_id_, at_pos - self_hosted_id_);
} else {
self_hosted_id_str = self_hosted_id_;
}
} else {
self_hosted_id_str = client_id_;
}
std::string new_self_hosted_id =
self_hosted_id_str + "@" + password_saved_;
memset(&self_hosted_id_, 0, sizeof(self_hosted_id_));
strncpy(self_hosted_id_, new_self_hosted_id.c_str(),
sizeof(self_hosted_id_) - 1);
self_hosted_id_[sizeof(self_hosted_id_) - 1] = '\0';
} else {
std::string client_id_with_password =
std::string(client_id_) + "@" + password_saved_;
strncpy(client_id_with_password_, client_id_with_password.c_str(),
sizeof(client_id_with_password_) - 1);
client_id_with_password_[sizeof(client_id_with_password_) - 1] =
'\0';
}
settings_.Save();
memset(new_password_, 0, sizeof(new_password_));
LeaveConnection(peer_, client_id_);
DestroyPeer(&peer_);
focus_on_input_widget_ = true;
}
}
ImGui::SameLine();
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str())) {
show_reset_password_window_ = false;
focus_on_input_widget_ = true;
memset(new_password_, 0, sizeof(new_password_));
}
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar();
}
}
ImGui::EndChild();
}
ImGui::EndChild();
ImGui::PopStyleVar();
return 0;
}
} // namespace crossdesk
@@ -1,656 +0,0 @@
#include <algorithm>
#include <cctype>
#include "application/gui_application.h"
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
std::string TrimConnectionAlias(const char *value) {
std::string alias = value ? value : "";
auto not_space = [](unsigned char ch) { return !std::isspace(ch); };
alias.erase(alias.begin(),
std::find_if(alias.begin(), alias.end(), not_space));
alias.erase(std::find_if(alias.rbegin(), alias.rend(), not_space).base(),
alias.end());
return alias;
}
void SetDarkTextTooltip(const char *text) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.05f, 0.05f, 0.05f, 1.0f));
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", text);
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
ImGui::PopStyleColor();
}
} // namespace
int GuiApplication::RecentConnectionsWindow() {
ImGuiIO &io = ImGui::GetIO();
float recent_connection_window_width = io.DisplaySize.x;
float recent_connection_window_height =
io.DisplaySize.y * (0.455f - STATUS_BAR_HEIGHT);
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * 0.55f),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::BeginChild(
"RecentConnectionsWindow",
ImVec2(recent_connection_window_width, recent_connection_window_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
ImGui::SetCursorPos(
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
ImGui::SetWindowFontScale(0.9f);
ImGui::TextColored(
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
localization::recent_connections[localization_language_index_].c_str());
ShowRecentConnections();
ImGui::EndChild();
return 0;
}
int GuiApplication::ShowRecentConnections() {
ImGuiIO &io = ImGui::GetIO();
float recent_connection_panel_width = io.DisplaySize.x * 0.912f;
float recent_connection_panel_height = io.DisplaySize.y * 0.29f;
float recent_connection_image_height = recent_connection_panel_height * 0.6f;
float recent_connection_image_width = recent_connection_image_height * 16 / 9;
float recent_connection_sub_container_width =
recent_connection_image_width * 1.2f;
float recent_connection_sub_container_height =
recent_connection_image_height * 1.4f;
float recent_connection_button_width = recent_connection_image_width * 0.15f;
float recent_connection_button_height =
recent_connection_image_height * 0.25f;
float recent_connection_footer_height =
recent_connection_button_height * 1.18f;
float recent_connection_name_width = recent_connection_image_width;
const float recent_connection_spacing = recent_connection_image_width * 0.16f;
const float recent_connections_content_width =
recent_connections_.empty()
? 0.0f
: recent_connections_.size() * recent_connection_sub_container_width +
(recent_connections_.size() - 1) * recent_connection_spacing;
const float recent_connections_available_width =
recent_connection_panel_width - 2.0f * ImGui::GetStyle().WindowPadding.x;
const bool has_horizontal_overflow =
recent_connections_content_width > recent_connections_available_width;
ImGui::SetCursorPos(
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.1f));
std::map<std::string, ImVec2> sub_containers_pos;
ImGui::PushStyleColor(ImGuiCol_ChildBg,
ImVec4(239.0f / 255, 240.0f / 255, 242.0f / 255, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
const ImGuiWindowFlags container_flags =
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoScrollWithMouse |
(has_horizontal_overflow ? ImGuiWindowFlags_AlwaysHorizontalScrollbar
: ImGuiWindowFlags_None);
ImGui::BeginChild(
"RecentConnectionsContainer",
ImVec2(recent_connection_panel_width, recent_connection_panel_height),
ImGuiChildFlags_Borders, container_flags);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
size_t recent_connections_count = recent_connections_.size();
int count = 0;
for (auto &it : recent_connections_) {
sub_containers_pos[it.first] = ImGui::GetCursorPos();
std::string recent_connection_sub_window_name =
"RecentConnectionsSubContainer" + it.first;
// recent connections sub container
ImGui::BeginChild(recent_connection_sub_window_name.c_str(),
ImVec2(recent_connection_sub_container_width,
recent_connection_sub_container_height),
ImGuiChildFlags_None,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
std::string connection_info = it.first;
// remote id length is 9
// password length is 6
// connection_info -> remote_id + 'Y' + host_name + '@' + password
// -> remote_id + 'N' + host_name
bool invalid_connection_info = false;
if (connection_info.size() > 9 && 'Y' == connection_info[9] &&
connection_info.size() >= 16) {
size_t pos_y = connection_info.find('Y');
size_t pos_at = connection_info.find('@');
if (pos_y == std::string::npos || pos_at == std::string::npos ||
pos_y >= pos_at) {
LOG_ERROR("Invalid filename");
invalid_connection_info = true;
} else {
it.second.remote_id = connection_info.substr(0, pos_y);
it.second.remote_host_name =
connection_info.substr(pos_y + 1, pos_at - pos_y - 1);
it.second.password = connection_info.substr(pos_at + 1);
it.second.remember_password = true;
}
} else if (connection_info.size() > 9 && 'N' == connection_info[9] &&
connection_info.size() >= 10) {
size_t pos_n = connection_info.find('N');
if (pos_n == std::string::npos) {
LOG_ERROR("Invalid filename");
invalid_connection_info = true;
} else {
it.second.remote_id = connection_info.substr(0, pos_n);
it.second.remote_host_name = connection_info.substr(pos_n + 1);
it.second.password = "";
it.second.remember_password = false;
}
} else {
invalid_connection_info = true;
}
if (invalid_connection_info) {
it.second.remote_id = connection_info.substr(
0, std::min<size_t>(connection_info.size(), 9));
it.second.remote_host_name = "unknown";
it.second.password = "";
it.second.remember_password = false;
}
std::string display_name = settings_.RecentConnectionDisplayName(it.second);
bool online = device_presence_cache_.IsOnline(it.second.remote_id);
ImVec2 image_pos =
ImVec2(ImGui::GetCursorPosX() + recent_connection_image_width * 0.05f,
ImGui::GetCursorPosY() + recent_connection_image_height * 0.08f);
ImGui::SetCursorPos(image_pos);
ImVec2 image_screen_pos = ImGui::GetCursorScreenPos();
ImGui::Image(
(ImTextureID)(intptr_t)it.second.texture,
ImVec2(recent_connection_image_width, recent_connection_image_height));
const bool image_item_hovered = ImGui::IsItemHovered();
ImVec2 card_screen_min = image_screen_pos;
ImVec2 card_screen_max =
ImVec2(image_screen_pos.x + recent_connection_image_width,
image_screen_pos.y + recent_connection_image_height +
recent_connection_footer_height);
const bool card_hovered =
ImGui::IsMouseHoveringRect(card_screen_min, card_screen_max, true);
const float recent_connection_toolbar_width =
3.0f * recent_connection_button_width;
const float recent_connection_toolbar_padding =
recent_connection_image_width * 0.025f;
const ImVec2 toolbar_pos = ImVec2(
image_pos.x + recent_connection_image_width -
recent_connection_toolbar_width - recent_connection_toolbar_padding,
image_pos.y + recent_connection_image_height +
(recent_connection_footer_height -
recent_connection_button_height) *
0.5f);
const ImVec2 toolbar_screen_pos = ImVec2(
image_screen_pos.x + recent_connection_image_width -
recent_connection_toolbar_width - recent_connection_toolbar_padding,
image_screen_pos.y + recent_connection_image_height +
(recent_connection_footer_height -
recent_connection_button_height) *
0.5f);
const ImVec2 toolbar_screen_end =
ImVec2(toolbar_screen_pos.x + recent_connection_toolbar_width,
toolbar_screen_pos.y + recent_connection_button_height);
const bool toolbar_hovered =
card_hovered && ImGui::IsMouseHoveringRect(toolbar_screen_pos,
toolbar_screen_end, true);
const bool show_image_tooltip = image_item_hovered && !toolbar_hovered;
if (show_image_tooltip) {
const ImVec2 mouse_pos = ImGui::GetMousePos();
const bool place_tooltip_on_left = mouse_pos.x > io.DisplaySize.x * 0.7f;
ImGui::SetNextWindowPos(
ImVec2(mouse_pos.x + (place_tooltip_on_left ? -12.0f : 12.0f),
mouse_pos.y - 8.0f),
ImGuiCond_Always, ImVec2(place_tooltip_on_left ? 1.0f : 0.0f, 1.0f));
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.4f);
ImGui::Text(
"%s: %s",
localization::device_name[localization_language_index_].c_str(),
display_name.c_str());
if (!it.second.remote_host_name.empty() &&
it.second.remote_host_name != display_name) {
ImGui::Text("%s", it.second.remote_host_name.c_str());
}
ImGui::Text("%s: %s",
localization::remote_id[localization_language_index_].c_str(),
it.second.remote_id.c_str());
ImGui::Text("%s",
(online ? localization::online[localization_language_index_]
: localization::offline[localization_language_index_])
.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ImDrawList *draw_list = ImGui::GetWindowDrawList();
// connection name footer
{
ImVec2 footer_pos =
ImVec2(image_pos.x, image_pos.y + recent_connection_image_height);
ImVec2 footer_screen_pos =
ImVec2(image_screen_pos.x,
image_screen_pos.y + recent_connection_image_height);
ImVec2 footer_screen_end =
ImVec2(footer_screen_pos.x + recent_connection_name_width,
footer_screen_pos.y + recent_connection_footer_height);
float footer_rounding = recent_connection_footer_height * 0.16f;
draw_list->AddRectFilled(footer_screen_pos, footer_screen_end,
IM_COL32(0, 0, 0, 40), footer_rounding,
ImDrawFlags_RoundCornersBottom);
const float status_left_margin = recent_connection_footer_height * 0.22f;
const float status_gap = recent_connection_footer_height * 0.20f;
const float dot_radius = recent_connection_footer_height * 0.18f;
const ImVec2 dot_center(
footer_screen_pos.x + status_left_margin + dot_radius,
footer_screen_pos.y + recent_connection_footer_height * 0.5f);
const ImU32 dot_color =
online ? IM_COL32(34, 197, 94, 255) : IM_COL32(156, 163, 175, 255);
// Layered halo simulates a radial gradient glow for the online state.
if (online) {
const float halo_radius = dot_radius * 2.2f;
const int halo_layers = 8;
for (int i = halo_layers; i > 0; --i) {
const float t = static_cast<float>(i) / halo_layers;
const float r = dot_radius + (halo_radius - dot_radius) * t;
const int alpha = static_cast<int>(14.0f * (1.0f - t) + 4.0f);
draw_list->AddCircleFilled(dot_center, r,
IM_COL32(74, 222, 128, alpha));
}
}
draw_list->AddCircleFilled(dot_center, dot_radius, dot_color);
const float status_block_end_x = dot_center.x + dot_radius;
ImVec2 text_min =
ImVec2(status_block_end_x + status_gap, footer_screen_pos.y);
ImVec2 text_max =
ImVec2(card_hovered ? toolbar_screen_pos.x - status_gap
: footer_screen_end.x -
recent_connection_name_width * 0.05f,
footer_screen_end.y);
ImGui::SetWindowFontScale(0.52f);
ImGui::RenderTextClipped(text_min, text_max, display_name.c_str(),
nullptr, nullptr, ImVec2(0.0f, 0.5f));
ImGui::SetWindowFontScale(1.0f);
}
// Toolbar / three buttons, right-aligned in the connection name footer.
if (card_hovered) {
float toolbar_rounding = recent_connection_button_height * 0.22f;
draw_list->AddRectFilled(
ImVec2(toolbar_screen_pos.x, toolbar_screen_pos.y + 1.0f),
ImVec2(toolbar_screen_end.x, toolbar_screen_end.y + 1.0f),
IM_COL32(0, 0, 0, 70), toolbar_rounding);
draw_list->AddRectFilled(toolbar_screen_pos, toolbar_screen_end,
IM_COL32(96, 100, 106, 140), toolbar_rounding);
draw_list->AddRect(toolbar_screen_pos, toolbar_screen_end,
IM_COL32(255, 255, 255, 48), toolbar_rounding);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, toolbar_rounding);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(1.0f, 1.0f, 1.0f, 0.18f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.35f, 0.55f, 0.95f, 0.45f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 0.95f));
ImGui::SetWindowFontScale(0.5f);
// edit alias button
{
ImGui::SetCursorPos(toolbar_pos);
std::string edit = ICON_FA_PEN;
std::string recent_connection_edit_button_name =
edit + "##RecentConnectionAlias" + it.first;
if (ImGui::Button(recent_connection_edit_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
settings_.BeginEditRecentConnectionAlias(it.second);
}
if (ImGui::IsItemHovered()) {
SetDarkTextTooltip(
localization::connection_alias[localization_language_index_]
.c_str());
}
}
// trash button
{
ImVec2 trash_can_button_pos = ImVec2(
toolbar_pos.x + recent_connection_button_width, toolbar_pos.y);
ImGui::SetCursorPos(trash_can_button_pos);
std::string trash_can = ICON_FA_TRASH_CAN;
std::string recent_connection_delete_button_name =
trash_can + "##RecentConnectionDelete" + it.first;
if (ImGui::Button(recent_connection_delete_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
show_confirm_delete_connection_ = true;
delete_connection_name_ = it.first;
}
if (ImGui::IsItemHovered()) {
SetDarkTextTooltip(
localization::delete_connection[localization_language_index_]
.c_str());
}
}
// connect button
{
ImVec2 connect_button_pos = ImVec2(
toolbar_pos.x + 2 * recent_connection_button_width, toolbar_pos.y);
ImGui::SetCursorPos(connect_button_pos);
std::string connect = ICON_FA_ARROW_RIGHT_LONG;
std::string connect_to_this_connection_button_name =
connect + "##ConnectionTo" + it.first;
if (ImGui::Button(connect_to_this_connection_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
ConnectTo(it.second.remote_id, it.second.password.c_str(),
it.second.remember_password);
}
if (ImGui::IsItemHovered()) {
SetDarkTextTooltip(localization::connect_to_this_connection
[localization_language_index_]
.c_str());
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(4);
ImGui::PopStyleVar(3);
}
if (delete_connection_ && delete_connection_name_ == it.first) {
if (!thumbnail_->DeleteThumbnail(it.first)) {
settings_.EraseRecentConnectionAlias(it.second.remote_id);
settings_.SaveRecentConnectionAliases();
reload_recent_connections_ = true;
delete_connection_ = false;
}
}
ImGui::EndChild();
if (count != recent_connections_count - 1) {
ImVec2 line_start =
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.18f,
image_screen_pos.y);
ImVec2 line_end =
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.18f,
image_screen_pos.y + recent_connection_image_height +
recent_connection_footer_height);
ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
IM_COL32(0, 0, 0, 122), 1.0f);
}
count++;
ImGui::SameLine(0, count != recent_connections_count
? recent_connection_spacing
: 0.0f);
}
ImGui::EndChild();
if (show_confirm_delete_connection_) {
ConfirmDeleteConnection();
}
if (show_edit_connection_alias_window_) {
EditRecentConnectionAliasWindow();
}
if (show_offline_warning_window_) {
OfflineWarningWindow();
}
return 0;
}
int GuiApplication::ConfirmDeleteConnection() {
ImGuiIO &io = ImGui::GetIO();
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ConfirmDeleteConnectionWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto connection_status_window_width = ImGui::GetWindowSize().x;
auto connection_status_window_height = ImGui::GetWindowSize().y;
std::string text =
localization::confirm_delete_connection[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.33f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter)) {
delete_connection_ = true;
show_confirm_delete_connection_ = false;
}
ImGui::SameLine();
// cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
delete_connection_ = false;
show_confirm_delete_connection_ = false;
}
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX((connection_status_window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(connection_status_window_height * 0.2f);
ImGui::Text("%s", text.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar();
return 0;
}
int GuiApplication::EditRecentConnectionAliasWindow() {
ImGuiIO &io = ImGui::GetIO();
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("EditRecentConnectionAliasWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
std::string text =
localization::input_connection_alias[localization_language_index_];
ImGui::SetWindowFontScale(0.5f);
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::Text("%s", text.c_str());
ImGui::SetCursorPosX(window_width * 0.2f);
ImGui::SetCursorPosY(window_height * 0.4f);
ImGui::SetNextItemWidth(window_width * 0.6f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
if (focus_on_input_widget_) {
ImGui::SetKeyboardFocusHere();
focus_on_input_widget_ = false;
}
bool enter_pressed =
ImGui::InputText("##recent_connection_alias", edit_connection_alias_,
IM_ARRAYSIZE(edit_connection_alias_),
ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::PopStyleVar();
ImGui::SetCursorPosX(window_width * 0.315f);
ImGui::SetCursorPosY(window_height * 0.75f);
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
enter_pressed) {
std::string alias = TrimConnectionAlias(edit_connection_alias_);
if (alias.empty()) {
settings_.EraseRecentConnectionAlias(edit_connection_alias_remote_id_);
} else {
settings_.SetRecentConnectionAlias(edit_connection_alias_remote_id_,
alias);
}
settings_.SaveRecentConnectionAliases();
show_edit_connection_alias_window_ = false;
focus_on_input_widget_ = true;
memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_));
edit_connection_alias_remote_id_.clear();
}
ImGui::SameLine();
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_edit_connection_alias_window_ = false;
focus_on_input_widget_ = true;
memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_));
edit_connection_alias_remote_id_.clear();
}
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar();
return 0;
}
int GuiApplication::OfflineWarningWindow() {
ImGuiIO &io = ImGui::GetIO();
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("OfflineWarningWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
ImGui::SetCursorPosX(window_width * 0.43f);
ImGui::SetCursorPosY(window_height * 0.67f);
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_offline_warning_window_ = false;
}
auto text_width = ImGui::CalcTextSize(offline_warning_text_.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::Text("%s", offline_warning_text_.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar();
return 0;
}
} // namespace crossdesk
@@ -1,167 +0,0 @@
#include "application/gui_application.h"
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
namespace crossdesk {
static int InputTextCallback(ImGuiInputTextCallbackData *data);
int GuiApplication::RemoteWindow() {
ImGuiIO &io = ImGui::GetIO();
float remote_window_width = io.DisplaySize.x * 0.5f;
float remote_window_height =
io.DisplaySize.y * (1 - TITLE_BAR_HEIGHT - STATUS_BAR_HEIGHT);
float remote_window_arrow_button_width = io.DisplaySize.x * 0.1f;
float remote_window_arrow_button_height = io.DisplaySize.y * 0.078f;
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * TITLE_BAR_HEIGHT),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
ImGui::BeginChild("RemoteDesktopWindow",
ImVec2(remote_window_width, remote_window_height),
ImGuiChildFlags_None,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleColor();
ImGui::SetCursorPos(
ImVec2(io.DisplaySize.x * 0.057f, io.DisplaySize.y * 0.02f));
ImGui::SetWindowFontScale(0.9f);
ImGui::TextColored(
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
localization::remote_desktop[localization_language_index_].c_str());
ImGui::Spacing();
{
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.557f, io.DisplaySize.y * 0.15f),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(239.0f / 255, 240.0f / 255,
242.0f / 255, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
ImGui::BeginChild(
"RemoteDesktopWindow_1",
ImVec2(remote_window_width * 0.8f, remote_window_height * 0.43f),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
{
ImGui::SetWindowFontScale(0.8f);
ImGui::Text(
"%s", localization::remote_id[localization_language_index_].c_str());
ImGui::Spacing();
ImGui::SetNextItemWidth(io.DisplaySize.x * 0.25f);
ImGui::SetWindowFontScale(1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
if (re_enter_remote_id_) {
ImGui::SetKeyboardFocusHere();
re_enter_remote_id_ = false;
memset(remote_id_display_, 0, sizeof(remote_id_display_));
}
bool enter_pressed = ImGui::InputText(
"##remote_id_", remote_id_display_, IM_ARRAYSIZE(remote_id_display_),
ImGuiInputTextFlags_CharsDecimal |
ImGuiInputTextFlags_EnterReturnsTrue |
ImGuiInputTextFlags_CallbackEdit,
InputTextCallback);
ImGui::PopStyleVar();
ImGui::SameLine();
std::string remote_id = remote_id_display_;
remote_id.erase(remove_if(remote_id.begin(), remote_id.end(),
static_cast<int (*)(int)>(&isspace)),
remote_id.end());
if (ImGui::Button(ICON_FA_ARROW_RIGHT_LONG,
ImVec2(remote_window_arrow_button_width,
remote_window_arrow_button_height)) ||
enter_pressed) {
connect_button_pressed_ = true;
bool found = false;
std::string target_remote_id;
std::string target_password;
bool should_connect = false;
bool already_connected = false;
for (auto &[id, props] : recent_connections_) {
if (id.find(remote_id) != std::string::npos) {
found = true;
target_remote_id = props.remote_id;
target_password = props.password;
{
// std::shared_lock lock(remote_sessions_mutex_);
if (remote_sessions_.find(remote_id) !=
remote_sessions_.end()) {
if (!remote_sessions_[remote_id]->connection_established_) {
should_connect = true;
} else {
already_connected = true;
}
} else {
should_connect = true;
}
}
if (should_connect) {
ConnectTo(target_remote_id, target_password.c_str(), false);
} else if (already_connected) {
LOG_INFO("Already connected to [{}]", remote_id);
}
break;
}
}
if (!found) {
ConnectTo(remote_id, "", false);
}
}
// check every 1 second for rejoin
if (need_to_rejoin_) {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
now - last_rejoin_check_time_)
.count();
if (elapsed >= 1000) {
last_rejoin_check_time_ = now;
need_to_rejoin_ = false;
// std::shared_lock lock(remote_sessions_mutex_);
for (const auto &[_, props] : remote_sessions_) {
if (props->rejoin_) {
ConnectTo(props->remote_id_, props->remote_password_,
props->remember_password_);
}
}
}
}
}
ImGui::EndChild();
}
ImGui::EndChild();
ImGui::PopStyleVar();
return 0;
}
static int InputTextCallback(ImGuiInputTextCallbackData *data) {
if (data->BufTextLen > 3 && data->Buf[3] != ' ') {
data->InsertChars(3, " ");
}
if (data->BufTextLen > 7 && data->Buf[7] != ' ') {
data->InsertChars(7, " ");
}
return 0;
}
} // namespace crossdesk
@@ -1,514 +0,0 @@
#include <algorithm>
#include <remote_action.h>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <thread>
#include <vector>
#include "application/gui_application.h"
#include "file_transfer.h"
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
#include "tinyfiledialogs.h"
namespace crossdesk {
namespace {
void ShowControlBarTooltip(const std::string &text) {
if (!ImGui::IsItemHovered() || text.empty()) {
return;
}
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", text.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
} // namespace
int CountDigits(int number) {
if (number == 0)
return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1;
}
int BitrateDisplay(int bitrate) {
int num_of_digits = CountDigits(bitrate);
if (num_of_digits <= 3) {
ImGui::Text("%d bps", bitrate);
} else if (num_of_digits > 3 && num_of_digits <= 6) {
ImGui::Text("%d kbps", bitrate / 1000);
} else {
ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
}
return 0;
}
int LossRateDisplay(float loss_rate) {
if (loss_rate < 0.01f) {
ImGui::Text("0%%");
} else {
ImGui::Text("%.0f%%", loss_rate * 100);
}
return 0;
}
std::string GuiApplication::OpenFileDialog(std::string title) {
const char *path = tinyfd_openFileDialog(title.c_str(),
"", // default path
0, // number of filters
nullptr, // filters
nullptr, // filter description
0 // no multiple selection
);
return path ? path : "";
}
int GuiApplication::ControlBar(
std::shared_ptr<RemoteSession> &props) {
float button_width = title_bar_height_ * 0.8f;
float button_height = title_bar_height_ * 0.8f;
float line_padding = title_bar_height_ * 0.12f;
float line_thickness = title_bar_height_ * 0.07f;
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
if (props->control_bar_expand_) {
ImGui::SetCursorPosX(props->is_control_bar_in_left_
? props->control_window_width_ * 0.03f
: props->control_window_width_ * 0.17f);
ImDrawList *draw_list = ImGui::GetWindowDrawList();
if (!props->is_control_bar_in_left_) {
draw_list->AddLine(
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f,
ImGui::GetCursorScreenPos().y + button_height * 0.2f),
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f,
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
IM_COL32(178, 178, 178, 255), 2.0f);
}
std::string display = ICON_FA_DISPLAY;
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(display.c_str(), ImVec2(button_width, button_height))) {
ImGui::OpenPopup("display");
}
ImVec2 btn_min = ImGui::GetItemRectMin();
ImVec2 btn_size_actual = ImGui::GetItemRectSize();
ShowControlBarTooltip(
localization::select_display[localization_language_index_]);
props->display_selectable_hovered_ = false;
if (ImGui::BeginPopup("display")) {
ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < props->display_info_list_.size(); i++) {
const std::string display_label = localization::FormatDisplayLabel(
static_cast<size_t>(i), props->display_info_list_[i].name,
localization_language_index_);
if (ImGui::Selectable(display_label.c_str())) {
props->selected_display_ = i;
RemoteAction remote_action;
remote_action.type = ControlType::display_id;
remote_action.d = i;
if (props->connection_status_.load() == ConnectionStatus::Connected) {
std::string msg = remote_action.to_json();
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->control_data_label_.c_str());
}
}
}
props->display_selectable_hovered_ =
ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
ImGui::EndPopup();
}
ImGui::SetWindowFontScale(0.35f);
ImVec2 text_size = ImGui::CalcTextSize(
std::to_string(props->selected_display_ + 1).c_str());
ImVec2 text_pos =
ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.55f,
btn_min.y + (btn_size_actual.y - text_size.y) * 0.33f);
ImGui::GetWindowDrawList()->AddText(
text_pos, IM_COL32(0, 0, 0, 255),
std::to_string(props->selected_display_ + 1).c_str());
auto send_service_command = [&](ServiceCommandFlag flag,
const char *log_action) {
if (props->connection_status_.load() == ConnectionStatus::Connected &&
props->peer_) {
RemoteAction remote_action;
remote_action.type = ControlType::service_command;
remote_action.c.flag = flag;
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->control_data_label_.c_str());
if (ret != 0) {
LOG_WARN("Send {} command failed, remote_id={}, ret={}", log_action,
props->remote_id_, ret);
}
}
};
ImGui::SameLine();
std::string shortcut = ICON_FA_KEYBOARD;
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(shortcut.c_str(), ImVec2(button_width, button_height))) {
ImGui::OpenPopup("shortcut");
}
ShowControlBarTooltip(
localization::send_shortcut[localization_language_index_]);
props->shortcut_selectable_hovered_ = false;
if (ImGui::BeginPopup("shortcut")) {
ImGui::SetWindowFontScale(0.5f);
std::string sas_label = "Ctrl+Alt+Del";
std::string lock_label = "Win+L";
if (ImGui::Selectable(sas_label.c_str())) {
send_service_command(ServiceCommandFlag::send_sas, "SAS");
}
if (ImGui::Selectable(lock_label.c_str())) {
send_service_command(ServiceCommandFlag::lock_workstation,
"remote lock");
}
props->shortcut_selectable_hovered_ =
ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
ImGui::SetWindowFontScale(1.0f);
ImGui::EndPopup();
}
ImGui::SameLine();
float mouse_x = ImGui::GetCursorScreenPos().x;
float mouse_y = ImGui::GetCursorScreenPos().y;
float disable_mouse_x = mouse_x + line_padding;
float disable_mouse_y = mouse_y + line_padding;
std::string mouse = ICON_FA_COMPUTER_MOUSE;
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(mouse.c_str(), ImVec2(button_width, button_height))) {
if (props->connection_established_) {
start_keyboard_capturer_ = !start_keyboard_capturer_;
props->control_mouse_ = !props->control_mouse_;
props->enable_mouse_control_ = !props->enable_mouse_control_;
props->mouse_control_button_label_ =
props->enable_mouse_control_
? localization::release_mouse[localization_language_index_]
: localization::control_mouse[localization_language_index_];
}
}
const bool mouse_button_hovered = ImGui::IsItemHovered();
const std::string mouse_tooltip =
props->enable_mouse_control_
? localization::release_mouse[localization_language_index_]
: localization::control_mouse[localization_language_index_];
ShowControlBarTooltip(mouse_tooltip);
if (!props->enable_mouse_control_) {
draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y),
ImVec2(mouse_x + button_width - line_padding,
mouse_y + button_height - line_padding),
IM_COL32(0, 0, 0, 255), line_thickness);
draw_list->AddLine(
ImVec2(disable_mouse_x - line_thickness * 0.7f,
disable_mouse_y + line_thickness * 0.7f),
ImVec2(mouse_x + button_width - line_padding - line_thickness * 0.7f,
mouse_y + button_height - line_padding +
line_thickness * 0.7f),
mouse_button_hovered ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255),
line_thickness);
}
ImGui::SameLine();
// audio capture button
float audio_x = ImGui::GetCursorScreenPos().x;
float audio_y = ImGui::GetCursorScreenPos().y;
float disable_audio_x = audio_x + line_padding;
float disable_audio_y = audio_y + line_padding;
std::string audio = props->audio_capture_button_pressed_
? ICON_FA_VOLUME_HIGH
: ICON_FA_VOLUME_HIGH;
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(audio.c_str(), ImVec2(button_width, button_height))) {
if (props->connection_established_) {
props->audio_capture_button_pressed_ =
!props->audio_capture_button_pressed_;
props->audio_capture_button_label_ =
props->audio_capture_button_pressed_
? localization::audio_capture[localization_language_index_]
: localization::mute[localization_language_index_];
RemoteAction remote_action;
remote_action.type = ControlType::audio_capture;
remote_action.a = props->audio_capture_button_pressed_;
std::string msg = remote_action.to_json();
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->control_data_label_.c_str());
}
}
const bool audio_button_hovered = ImGui::IsItemHovered();
const std::string audio_tooltip =
props->audio_capture_button_pressed_
? localization::mute[localization_language_index_]
: localization::audio_capture[localization_language_index_];
ShowControlBarTooltip(audio_tooltip);
if (!props->audio_capture_button_pressed_) {
draw_list->AddLine(ImVec2(disable_audio_x, disable_audio_y),
ImVec2(audio_x + button_width - line_padding,
audio_y + button_height - line_padding),
IM_COL32(0, 0, 0, 255), line_thickness);
draw_list->AddLine(
ImVec2(disable_audio_x - line_thickness * 0.7f,
disable_audio_y + line_thickness * 0.7f),
ImVec2(audio_x + button_width - line_padding - line_thickness * 0.7f,
audio_y + button_height - line_padding +
line_thickness * 0.7f),
audio_button_hovered ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255),
line_thickness);
}
ImGui::SameLine();
std::string open_folder = ICON_FA_FOLDER_OPEN;
if (ImGui::Button(open_folder.c_str(),
ImVec2(button_width, button_height))) {
std::string title =
localization::select_file[localization_language_index_];
std::string path = OpenFileDialog(title);
transfers_.ProcessSelectedFile(path, props, file_label_);
}
ShowControlBarTooltip(
localization::select_file[localization_language_index_]);
ImGui::SameLine();
// net traffic stats button
bool button_color_style_pushed = false;
if (props->net_traffic_stats_button_pressed_) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(66 / 255.0f, 150 / 255.0f,
250 / 255.0f, 1.0f));
button_color_style_pushed = true;
}
std::string net_traffic_stats = ICON_FA_SIGNAL;
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(net_traffic_stats.c_str(),
ImVec2(button_width, button_height))) {
props->net_traffic_stats_button_pressed_ =
!props->net_traffic_stats_button_pressed_;
props->control_window_height_is_changing_ = true;
props->net_traffic_stats_button_pressed_time_ = ImGui::GetTime();
props->net_traffic_stats_button_label_ =
props->net_traffic_stats_button_pressed_
? localization::hide_net_traffic_stats
[localization_language_index_]
: localization::show_net_traffic_stats
[localization_language_index_];
}
const std::string net_traffic_stats_tooltip =
props->net_traffic_stats_button_pressed_
? localization::hide_net_traffic_stats[localization_language_index_]
: localization::show_net_traffic_stats
[localization_language_index_];
ShowControlBarTooltip(net_traffic_stats_tooltip);
if (button_color_style_pushed) {
ImGui::PopStyleColor();
button_color_style_pushed = false;
}
ImGui::SameLine();
// fullscreen button
std::string fullscreen =
fullscreen_button_pressed_ ? ICON_FA_COMPRESS : ICON_FA_EXPAND;
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(fullscreen.c_str(),
ImVec2(button_width, button_height))) {
fullscreen_button_pressed_ = !fullscreen_button_pressed_;
props->fullscreen_button_label_ =
fullscreen_button_pressed_
? localization::exit_fullscreen[localization_language_index_]
: localization::fullscreen[localization_language_index_];
if (fullscreen_button_pressed_) {
SDL_SetWindowFullscreen(stream_window_, true);
} else {
SDL_SetWindowFullscreen(stream_window_, false);
}
props->reset_control_bar_pos_ = true;
}
const std::string fullscreen_tooltip =
fullscreen_button_pressed_
? localization::exit_fullscreen[localization_language_index_]
: localization::fullscreen[localization_language_index_];
ShowControlBarTooltip(fullscreen_tooltip);
ImGui::SameLine();
// close button
std::string close_button = ICON_FA_XMARK;
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(close_button.c_str(),
ImVec2(button_width, button_height))) {
CloseRemoteSession(props);
}
ShowControlBarTooltip(
localization::disconnect[localization_language_index_]);
ImGui::SameLine();
if (props->is_control_bar_in_left_) {
draw_list->AddLine(
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.13f,
ImGui::GetCursorScreenPos().y + button_height * 0.2f),
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.13f,
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
IM_COL32(178, 178, 178, 255), 2.0f);
}
ImGui::SameLine();
}
float expand_button_pos_x = props->control_bar_expand_
? (props->is_control_bar_in_left_
? props->control_window_width_ * 0.917f
: props->control_window_width_ * 0.03f)
: props->control_window_width_ * 0.11f;
ImGui::SetCursorPosX(expand_button_pos_x);
std::string control_bar =
props->control_bar_expand_
? (props->is_control_bar_in_left_ ? ICON_FA_ANGLE_LEFT
: ICON_FA_ANGLE_RIGHT)
: (props->is_control_bar_in_left_ ? ICON_FA_ANGLE_RIGHT
: ICON_FA_ANGLE_LEFT);
const std::string control_bar_tooltip =
props->control_bar_expand_
? localization::collapse_control_bar[localization_language_index_]
: localization::expand_control_bar[localization_language_index_];
if (ImGui::Button(control_bar.c_str(),
ImVec2(button_height * 0.6f, button_height))) {
props->control_bar_expand_ = !props->control_bar_expand_;
props->control_bar_button_pressed_time_ = ImGui::GetTime();
props->control_window_width_is_changing_ = true;
if (!props->control_bar_expand_) {
props->control_window_height_ = props->control_window_min_height_;
props->net_traffic_stats_button_pressed_ = false;
}
}
ShowControlBarTooltip(control_bar_tooltip);
if (props->net_traffic_stats_button_pressed_ && props->control_bar_expand_) {
NetTrafficStats(props);
}
ImGui::PopStyleVar();
return 0;
}
int GuiApplication::NetTrafficStats(
std::shared_ptr<RemoteSession> &props) {
ImGui::SetCursorPos(ImVec2(props->control_window_width_ * 0.048f,
props->control_window_min_height_));
ImGui::SetWindowFontScale(0.5f);
if (ImGui::BeginTable("NetTrafficStats", 4, ImGuiTableFlags_BordersH,
ImVec2(props->control_window_max_width_ * 0.9f,
props->control_window_max_height_ - 0.9f))) {
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableNextColumn();
ImGui::Text(" ");
ImGui::TableNextColumn();
ImGui::Text("%s", localization::in[localization_language_index_].c_str());
ImGui::TableNextColumn();
ImGui::Text("%s", localization::out[localization_language_index_].c_str());
ImGui::TableNextColumn();
ImGui::Text("%s",
localization::loss_rate[localization_language_index_].c_str());
ImGui::TableNextColumn();
ImGui::Text("%s",
localization::video[localization_language_index_].c_str());
ImGui::TableNextColumn();
BitrateDisplay((int)props->net_traffic_stats_.video_inbound_stats.bitrate);
ImGui::TableNextColumn();
BitrateDisplay((int)props->net_traffic_stats_.video_outbound_stats.bitrate);
ImGui::TableNextColumn();
LossRateDisplay(props->net_traffic_stats_.video_inbound_stats.loss_rate);
ImGui::TableNextColumn();
ImGui::Text("%s",
localization::audio[localization_language_index_].c_str());
ImGui::TableNextColumn();
BitrateDisplay((int)props->net_traffic_stats_.audio_inbound_stats.bitrate);
ImGui::TableNextColumn();
BitrateDisplay((int)props->net_traffic_stats_.audio_outbound_stats.bitrate);
ImGui::TableNextColumn();
LossRateDisplay(props->net_traffic_stats_.audio_inbound_stats.loss_rate);
ImGui::TableNextColumn();
ImGui::Text("%s", localization::data[localization_language_index_].c_str());
ImGui::TableNextColumn();
BitrateDisplay((int)props->net_traffic_stats_.data_inbound_stats.bitrate);
ImGui::TableNextColumn();
BitrateDisplay((int)props->net_traffic_stats_.data_outbound_stats.bitrate);
ImGui::TableNextColumn();
LossRateDisplay(props->net_traffic_stats_.data_inbound_stats.loss_rate);
ImGui::TableNextColumn();
ImGui::Text("%s",
localization::total[localization_language_index_].c_str());
ImGui::TableNextColumn();
BitrateDisplay((int)props->net_traffic_stats_.total_inbound_stats.bitrate);
ImGui::TableNextColumn();
BitrateDisplay((int)props->net_traffic_stats_.total_outbound_stats.bitrate);
ImGui::TableNextColumn();
LossRateDisplay(props->net_traffic_stats_.total_inbound_stats.loss_rate);
ImGui::TableNextColumn();
ImGui::Text("FPS:");
ImGui::TableNextColumn();
ImGui::Text("%d", props->fps_);
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::Text("%s:",
localization::resolution[localization_language_index_].c_str());
ImGui::TableNextColumn();
ImGui::Text("%dx%d", props->video_width_, props->video_height_);
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::Text(
"%s:",
localization::connection_mode[localization_language_index_].c_str());
ImGui::TableNextColumn();
ImGui::Text(
"%s",
props->traversal_mode_ == 0
? localization::connection_mode_direct[localization_language_index_]
.c_str()
: localization::connection_mode_relay[localization_language_index_]
.c_str());
ImGui::EndTable();
}
ImGui::SetWindowFontScale(1.0f);
return 0;
}
} // namespace crossdesk
@@ -1,56 +0,0 @@
#include "application/gui_application.h"
#include "layout_relative.h"
#include "localization.h"
namespace crossdesk {
int GuiApplication::StatusBar() {
ImGuiIO &io = ImGui::GetIO();
float status_bar_width = io.DisplaySize.x;
float status_bar_height = io.DisplaySize.y * STATUS_BAR_HEIGHT;
static bool a, b, c, d, e;
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT)),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::BeginChild("StatusBar", ImVec2(status_bar_width, status_bar_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleColor(2);
ImVec2 dot_pos = ImVec2(status_bar_width * 0.025f,
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.5f));
ImDrawList *draw_list = ImGui::GetWindowDrawList();
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.25f,
ImColor(1.0f, 1.0f, 1.0f), 100);
bool tls_cert_error = signal_status_ == SignalStatus::SignalTlsCertError;
draw_list->AddCircleFilled(
dot_pos, status_bar_height * 0.2f,
tls_cert_error ? ImColor(1.0f, 0.65f, 0.0f)
: ImColor(signal_connected_ ? 0.0f : 1.0f,
signal_connected_ ? 1.0f : 0.0f, 0.0f),
100);
ImGui::SetWindowFontScale(0.6f);
const char *signal_status_text =
tls_cert_error
? localization::signal_tls_cert_error[localization_language_index_]
.c_str()
: (signal_connected_
? localization::signal_connected[localization_language_index_]
.c_str()
: localization::signal_disconnected
[localization_language_index_]
.c_str());
draw_list->AddText(ImVec2(status_bar_width * 0.045f,
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.9f)),
ImColor(0.0f, 0.0f, 0.0f), signal_status_text);
ImGui::SetWindowFontScale(1.0f);
ImGui::EndChild();
return 0;
}
} // namespace crossdesk
@@ -1,329 +0,0 @@
#include "application/gui_application.h"
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "rounded_corner_button.h"
constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
namespace crossdesk {
int GuiApplication::TitleBar(bool main_window) {
ImGuiIO &io = ImGui::GetIO();
float title_bar_width = title_bar_width_;
float title_bar_height = title_bar_height_;
float title_bar_height_padding = title_bar_height_;
float title_bar_button_width = title_bar_button_width_;
float title_bar_button_height = title_bar_button_height_;
if (main_window) {
// When the main window is minimized, Dear ImGui may report DisplaySize as
// (0, 0). Do not overwrite shared title-bar metrics with zeros, otherwise
// stream/server windows (which reuse these metrics) will lose their title
// bars and appear collapsed.
if (io.DisplaySize.x > 0.0f && io.DisplaySize.y > 0.0f) {
title_bar_width = io.DisplaySize.x;
title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT;
title_bar_height_padding = io.DisplaySize.y * TITLE_BAR_HEIGHT;
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
title_bar_height_ = title_bar_height;
title_bar_button_width_ = title_bar_button_width;
title_bar_button_height_ = title_bar_button_height;
} else {
// Keep using last known good values.
title_bar_width = title_bar_width_;
title_bar_height = title_bar_height_;
title_bar_height_padding = title_bar_height_;
title_bar_button_width = title_bar_button_width_;
title_bar_button_height = title_bar_button_height_;
}
} else {
title_bar_width = io.DisplaySize.x;
title_bar_height = title_bar_button_height_;
title_bar_button_width = title_bar_button_width_;
title_bar_button_height = title_bar_button_height_;
}
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::BeginChild(main_window ? "MainTitleBar" : "StreamTitleBar",
ImVec2(title_bar_width, title_bar_height_padding),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
// get draw list
ImDrawList *draw_list = ImGui::GetWindowDrawList();
ImGui::SetCursorPos(
ImVec2(title_bar_width - title_bar_button_width * 3, 0.0f));
if (main_window) {
float bar_pos_x = title_bar_width - title_bar_button_width * 3 +
title_bar_button_width * 0.33f;
float bar_pos_y = title_bar_button_height * 0.5f;
// draw menu icon
float menu_bar_line_size = title_bar_button_width * 0.33f;
draw_list->AddLine(
ImVec2(bar_pos_x, bar_pos_y - title_bar_button_height * 0.15f),
ImVec2(bar_pos_x + menu_bar_line_size,
bar_pos_y - title_bar_button_height * 0.15f),
IM_COL32(0, 0, 0, 255));
draw_list->AddLine(ImVec2(bar_pos_x, bar_pos_y),
ImVec2(bar_pos_x + menu_bar_line_size, bar_pos_y),
IM_COL32(0, 0, 0, 255));
draw_list->AddLine(
ImVec2(bar_pos_x, bar_pos_y + title_bar_button_height * 0.15f),
ImVec2(bar_pos_x + menu_bar_line_size,
bar_pos_y + title_bar_button_height * 0.15f),
IM_COL32(0, 0, 0, 255));
std::string title_bar_menu_button = "##title_bar_menu"; // ICON_FA_BARS;
std::string title_bar_menu = "##title_bar_menu";
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
if (ImGui::Button(
title_bar_menu_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) {
ImGui::OpenPopup(title_bar_menu.c_str());
}
ImGui::PopStyleColor(3);
if (ImGui::BeginPopup(title_bar_menu.c_str())) {
ImGui::SetWindowFontScale(0.6f);
if (ImGui::MenuItem(
localization::settings[localization_language_index_].c_str())) {
show_settings_window_ = true;
}
show_new_version_icon_in_menu_ = false;
std::string about_str = localization::about[localization_language_index_];
if (update_available_) {
const double now_time = ImGui::GetTime();
// every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >=
kNewVersionIconBlinkIntervalSec) {
show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time;
}
// render for 1 second
if (show_new_version_icon_) {
about_str = about_str + " " + ICON_FA_CIRCLE_ARROW_UP;
if (now_time - new_version_icon_render_start_time_ >=
kNewVersionIconBlinkOnTimeSec) {
show_new_version_icon_ = false;
}
} else {
about_str = about_str + " ";
}
}
if (ImGui::MenuItem(about_str.c_str())) {
show_about_window_ = true;
}
if (update_available_ && ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
std::string new_version_available_str =
localization::new_version_available[localization_language_index_] +
": " + latest_version_;
ImGui::Text("%s", new_version_available_str.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ImGui::EndPopup();
} else {
show_new_version_icon_in_menu_ = true;
}
if (update_available_ && show_new_version_icon_in_menu_) {
const double now_time = ImGui::GetTime();
// every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >=
kNewVersionIconBlinkIntervalSec) {
show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time;
}
// render for 1 second
if (show_new_version_icon_) {
ImGui::SetWindowFontScale(0.6f);
ImGui::SetCursorPos(ImVec2(bar_pos_x + title_bar_button_width * 0.21f,
bar_pos_y - title_bar_button_width * 0.24f));
ImGui::Text(ICON_FA_CIRCLE_ARROW_UP);
ImGui::SetWindowFontScale(1.0f);
if (now_time - new_version_icon_render_start_time_ >=
kNewVersionIconBlinkOnTimeSec) {
show_new_version_icon_ = false;
}
}
}
{
SettingWindow();
SelfHostedServerWindow();
AboutWindow();
}
} else {
ImGui::SetWindowFontScale(1.2f);
}
ImGui::SetCursorPos(ImVec2(
title_bar_width - title_bar_button_width * (main_window ? 2 : 3), 0.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
float minimize_pos_x = title_bar_width -
title_bar_button_width * (main_window ? 2 : 3) +
title_bar_button_width * 0.33f;
float minimize_pos_y = title_bar_button_height * 0.5f;
std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS;
if (ImGui::Button(window_minimize_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) {
if (main_window) {
last_main_minimize_request_tick_ = SDL_GetTicks();
} else {
last_stream_minimize_request_tick_ = SDL_GetTicks();
}
SDL_MinimizeWindow(main_window ? main_window_ : stream_window_);
}
draw_list->AddLine(
ImVec2(minimize_pos_x, minimize_pos_y),
ImVec2(minimize_pos_x + title_bar_button_width * 0.33f, minimize_pos_y),
IM_COL32(0, 0, 0, 255));
ImGui::PopStyleColor(3);
if (!main_window) {
ImGui::SetCursorPos(
ImVec2(title_bar_width - title_bar_button_width * 2, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
if (window_maximized_) {
float pos_x_top = title_bar_width - title_bar_button_width * 1.65f;
float pos_y_top = title_bar_button_height * 0.36f;
float pos_x_bottom = title_bar_width - title_bar_button_width * 1.6f;
float pos_y_bottom = title_bar_button_height * 0.28f;
std::string window_restore_button =
"##restore"; // ICON_FA_WINDOW_RESTORE;
if (ImGui::Button(
window_restore_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) {
SDL_RestoreWindow(stream_window_);
window_maximized_ = false;
}
draw_list->AddRect(ImVec2(pos_x_top, pos_y_top),
ImVec2(pos_x_top + title_bar_button_height * 0.33f,
pos_y_top + title_bar_button_height * 0.33f),
IM_COL32(0, 0, 0, 255));
draw_list->AddRect(ImVec2(pos_x_bottom, pos_y_bottom),
ImVec2(pos_x_bottom + title_bar_button_height * 0.33f,
pos_y_bottom + title_bar_button_height * 0.33f),
IM_COL32(0, 0, 0, 255));
if (ImGui::IsItemHovered()) {
draw_list->AddRectFilled(
ImVec2(pos_x_top + title_bar_button_height * 0.02f,
pos_y_top + title_bar_button_height * 0.01f),
ImVec2(pos_x_top + title_bar_button_height * 0.32f,
pos_y_top + title_bar_button_height * 0.31f),
IM_COL32(229, 229, 229, 255));
} else {
draw_list->AddRectFilled(
ImVec2(pos_x_top + title_bar_button_height * 0.02f,
pos_y_top + title_bar_button_height * 0.01f),
ImVec2(pos_x_top + title_bar_button_height * 0.32f,
pos_y_top + title_bar_button_height * 0.31f),
IM_COL32(255, 255, 255, 255));
}
} else {
float maximize_pos_x = title_bar_width - title_bar_button_width * 1.5f -
title_bar_button_height * 0.165f;
float maximize_pos_y = title_bar_button_height * 0.33f;
std::string window_maximize_button = "##maximize"; // ICON_FA_SQUARE_FULL;
if (ImGui::Button(
window_maximize_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) {
SDL_MaximizeWindow(stream_window_);
window_maximized_ = !window_maximized_;
}
draw_list->AddRect(
ImVec2(maximize_pos_x, maximize_pos_y),
ImVec2(maximize_pos_x + title_bar_button_height * 0.33f,
maximize_pos_y + title_bar_button_height * 0.33f),
IM_COL32(0, 0, 0, 255));
}
ImGui::PopStyleColor(3);
}
float xmark_button_pos_x = title_bar_width - title_bar_button_width;
ImGui::SetCursorPos(ImVec2(xmark_button_pos_x, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0, 0, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0, 0, 0.5f));
float xmark_pos_x = xmark_button_pos_x + title_bar_button_width * 0.5f;
float xmark_pos_y = title_bar_button_height * 0.5f;
float xmark_size = title_bar_button_width * 0.33f;
std::string close_button = "##xmark"; // ICON_FA_XMARK;
bool close_button_clicked = false;
if (main_window) {
close_button_clicked = RoundedCornerButton(
close_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height), 8.5f,
ImDrawFlags_RoundCornersTopRight, true, IM_COL32(0, 0, 0, 0),
IM_COL32(250, 0, 0, 255), IM_COL32(255, 0, 0, 128));
} else {
close_button_clicked =
ImGui::Button(close_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height));
}
if (close_button_clicked) {
if (main_window) {
if (!MinimizeMainWindowToTray()) {
SDL_HideWindow(main_window_);
}
} else {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
}
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
xmark_pos_y - xmark_size / 2 + 0.75f),
ImVec2(xmark_pos_x + xmark_size / 2 - 1.5f,
xmark_pos_y + xmark_size / 2 - 0.5f),
IM_COL32(0, 0, 0, 255));
draw_list->AddLine(
ImVec2(xmark_pos_x + xmark_size / 2 - 1.75f,
xmark_pos_y - xmark_size / 2 + 0.75f),
ImVec2(xmark_pos_x - xmark_size / 2, xmark_pos_y + xmark_size / 2 - 1.0f),
IM_COL32(0, 0, 0, 255));
ImGui::PopStyleColor(3);
ImGui::EndChild();
return 0;
}
} // namespace crossdesk
@@ -1,148 +0,0 @@
#include <cstdlib>
#include <string>
#if defined(_WIN32)
#include <windows.h>
#endif
#include "application/gui_application.h"
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
namespace crossdesk {
bool GuiApplication::OpenUrl(const std::string &url) {
#if defined(_WIN32)
int wide_len = MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0);
if (wide_len <= 0) {
return false;
}
std::wstring wide_url(static_cast<size_t>(wide_len), L'\0');
MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, &wide_url[0], wide_len);
if (!wide_url.empty() && wide_url.back() == L'\0') {
wide_url.pop_back();
}
std::wstring cmd = L"cmd.exe /c start \"\" \"" + wide_url + L"\"";
STARTUPINFOW startup_info = {sizeof(startup_info)};
PROCESS_INFORMATION process_info = {};
if (!CreateProcessW(nullptr, &cmd[0], nullptr, nullptr, FALSE,
CREATE_NO_WINDOW, nullptr, nullptr, &startup_info,
&process_info)) {
return false;
}
CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
return true;
#elif defined(__APPLE__)
std::string cmd = "open " + url;
return system(cmd.c_str()) == 0;
#else
std::string cmd = "xdg-open " + url;
return system(cmd.c_str()) == 0;
#endif
}
void GuiApplication::Hyperlink(const std::string &label, const std::string &url,
const float window_width) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255));
ImGui::TextUnformatted(label.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.6f);
ImGui::TextUnformatted(url.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
OpenUrl(url);
}
}
}
int GuiApplication::AboutWindow() {
if (show_about_window_) {
float about_window_width = title_bar_button_width_ * 7.5f;
float about_window_height = latest_version_.empty()
? title_bar_button_width_ * 4.0f
: title_bar_button_width_ * 4.9f;
const ImGuiViewport *viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(
(viewport->WorkSize.x - viewport->WorkPos.x - about_window_width) / 2,
(viewport->WorkSize.y - viewport->WorkPos.y - about_window_height) /
2));
ImGui::SetNextWindowSize(ImVec2(about_window_width, about_window_height));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::SetWindowFontScale(0.5f);
ImGui::Begin(
localization::about[localization_language_index_].c_str(), nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
std::string version;
#ifdef CROSSDESK_VERSION
version = CROSSDESK_VERSION;
#else
version = "Unknown";
#endif
std::string text = localization::version[localization_language_index_] +
": CrossDesk " + version;
ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", text.c_str());
if (update_available_ && show_new_version_icon_in_menu_) {
std::string new_version_available =
localization::new_version_available[localization_language_index_] +
": ";
ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", new_version_available.c_str());
std::string access_website =
localization::access_website[localization_language_index_];
ImGui::SetCursorPosX((about_window_width -
ImGui::CalcTextSize(latest_version_.c_str()).x) /
2.0f);
Hyperlink(latest_version_, "https://crossdesk.cn", about_window_width);
ImGui::Spacing();
} else {
ImGui::Text("%s", "");
}
std::string copyright_text = "© 2025 by JUNKUN DI. All rights reserved.";
std::string license_text = "Licensed under GNU GPL v3.";
ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", copyright_text.c_str());
ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", license_text.c_str());
ImGui::SetCursorPosX(about_window_width * 0.445f);
ImGui::SetCursorPosY(about_window_height * 0.8f);
// OK
if (ImGui::Button(localization::ok[localization_language_index_].c_str())) {
show_about_window_ = false;
}
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::End();
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
}
return 0;
}
} // namespace crossdesk
@@ -1,209 +0,0 @@
#include "application/gui_application.h"
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
namespace crossdesk {
bool GuiApplication::ConnectionStatusWindow(
std::shared_ptr<RemoteSession> &props) {
ImGuiIO &io = ImGui::GetIO();
bool ret_flag = false;
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ConnectionStatusWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto connection_status_window_width = ImGui::GetWindowSize().x;
auto connection_status_window_height = ImGui::GetWindowSize().y;
ImGui::SetWindowFontScale(0.5f);
std::string text;
const ConnectionStatus status = props->connection_status_.load();
if (ConnectionStatus::Connecting == status) {
text = localization::p2p_connecting[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
re_enter_remote_id_ = true;
LOG_INFO("User cancelled connecting to [{}]", props->remote_id_);
if (props->peer_) {
LeaveConnection(props->peer_, props->remote_id_.c_str());
}
ret_flag = true;
}
} else if (ConnectionStatus::Gathering == status) {
text = localization::p2p_gathering[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
re_enter_remote_id_ = true;
LOG_INFO("User cancelled connecting to [{}]", props->remote_id_);
if (props->peer_) {
LeaveConnection(props->peer_, props->remote_id_.c_str());
}
ret_flag = true;
}
} else if (ConnectionStatus::Connected == status) {
text = localization::p2p_connected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
}
} else if (ConnectionStatus::Disconnected == status) {
text = localization::p2p_disconnected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
}
} else if (ConnectionStatus::Failed == status) {
text = localization::p2p_failed[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
}
} else if (ConnectionStatus::Closed == status) {
text = localization::p2p_closed[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
}
} else if (ConnectionStatus::IncorrectPassword == status) {
if (!password_validating_) {
if (password_validating_time_ == 1) {
text = localization::input_password[localization_language_index_];
} else {
text = localization::reinput_password[localization_language_index_];
}
ImGui::SetCursorPosX(connection_status_window_width * 0.336f);
ImGui::SetCursorPosY(connection_status_window_height * 0.4f);
ImGui::SetNextItemWidth(connection_status_window_width * 0.33f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
if (focus_on_input_widget_) {
ImGui::SetKeyboardFocusHere();
focus_on_input_widget_ = false;
}
ImGui::InputText("##password", props->remote_password_,
IM_ARRAYSIZE(props->remote_password_),
ImGuiInputTextFlags_CharsNoBlank);
ImGui::SetWindowFontScale(0.4f);
ImVec2 text_size = ImGui::CalcTextSize(
localization::remember_password[localization_language_index_]
.c_str());
ImGui::SetCursorPosX((connection_status_window_width - text_size.x) *
0.45f);
ImGui::Checkbox(
localization::remember_password[localization_language_index_].c_str(),
&(props->remember_password_));
ImGui::SetWindowFontScale(0.5f);
ImGui::PopStyleVar();
ImGui::SetCursorPosX(connection_status_window_width * 0.325f);
ImGui::SetCursorPosY(connection_status_window_height * 0.75f);
// ok
if (ImGui::Button(
localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter)) {
show_connection_status_window_ = true;
password_validating_ = true;
props->rejoin_ = true;
need_to_rejoin_ = true;
focus_on_input_widget_ = true;
}
ImGui::SameLine();
// cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
memset(props->remote_password_, 0, sizeof(props->remote_password_));
show_connection_status_window_ = false;
focus_on_input_widget_ = true;
}
} else if (password_validating_) {
text = localization::validate_password[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
}
} else if (ConnectionStatus::NoSuchTransmissionId == status) {
text = localization::no_such_id[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter)) {
show_connection_status_window_ = false;
re_enter_remote_id_ = true;
DestroyPeer(&props->peer_);
ret_flag = true;
}
} else if (ConnectionStatus::RemoteUnavailable == status) {
text = localization::device_offline[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
re_enter_remote_id_ = true;
DestroyPeer(&props->peer_);
ret_flag = true;
}
}
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX((connection_status_window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(connection_status_window_height * 0.2f);
ImGui::Text("%s", text.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar();
return ret_flag;
}
} // namespace crossdesk
@@ -1,264 +0,0 @@
#include "application/gui_application.h"
#include "rd_log.h"
namespace crossdesk {
int GuiApplication::ControlWindow(
std::shared_ptr<RemoteSession> &props) {
double time_duration =
ImGui::GetTime() - props->control_bar_button_pressed_time_;
if (props->control_window_width_is_changing_) {
if (props->control_bar_expand_) {
props->control_window_width_ =
(float)(props->control_window_min_width_ +
(props->control_window_max_width_ -
props->control_window_min_width_) *
4 * time_duration);
} else {
props->control_window_width_ =
(float)(props->control_window_max_width_ -
(props->control_window_max_width_ -
props->control_window_min_width_) *
4 * time_duration);
}
}
time_duration =
ImGui::GetTime() - props->net_traffic_stats_button_pressed_time_;
if (props->control_window_height_is_changing_) {
if (props->control_bar_expand_ &&
props->net_traffic_stats_button_pressed_) {
props->control_window_height_ =
(float)(props->control_window_min_height_ +
(props->control_window_max_height_ -
props->control_window_min_height_) *
4 * time_duration);
} else if (props->control_bar_expand_ &&
!props->net_traffic_stats_button_pressed_) {
props->control_window_height_ =
(float)(props->control_window_max_height_ -
(props->control_window_max_height_ -
props->control_window_min_height_) *
4 * time_duration);
}
}
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_ * 1.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, window_rounding_ * 1.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
float y_boundary = fullscreen_button_pressed_ ? 0.0f : title_bar_height_;
float container_x = 0.0f;
float container_y = y_boundary;
float container_w = stream_window_width_;
float container_h = stream_window_height_ - y_boundary;
ImGui::SetNextWindowSize(ImVec2(container_w, container_h), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(container_x, container_y), ImGuiCond_Always);
float pos_x = 0;
float pos_y = 0;
std::string container_window_title =
props->remote_id_ + "ControlContainerWindow";
ImGui::Begin(container_window_title.c_str(), nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoBackground);
ImGui::PopStyleVar();
ImVec2 container_pos = ImGui::GetWindowPos();
if (ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
props->control_bar_hovered_) {
float current_x_rel = props->control_window_pos_.x - container_pos.x;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
ImVec2 delta = ImGui::GetIO().MouseDelta;
pos_x = current_x_rel + delta.x;
pos_y = current_y_rel + delta.y;
if (pos_x < 0.0f)
pos_x = 0.0f;
if (pos_y < 0.0f)
pos_y = 0.0f;
if (pos_x + props->control_window_width_ > container_w)
pos_x = container_w - props->control_window_width_;
if (pos_y + props->control_window_height_ > container_h)
pos_y = container_h - props->control_window_height_;
} else if (props->reset_control_bar_pos_) {
float new_cursor_pos_x = 0.0f;
float new_cursor_pos_y = 0.0f;
// set control window pos
float current_y_rel = props->control_window_pos_.y - container_pos.y;
if (current_y_rel + props->control_window_height_ > container_h) {
pos_y = container_h - props->control_window_height_;
} else if (current_y_rel < 0.0f) {
pos_y = 0.0f;
} else {
pos_y = current_y_rel;
}
if (props->is_control_bar_in_left_) {
pos_x = 0.0f;
} else {
pos_x = container_w - props->control_window_width_;
}
if (0 != props->mouse_diff_control_bar_pos_x_ &&
0 != props->mouse_diff_control_bar_pos_y_) {
// set cursor pos
new_cursor_pos_x =
container_pos.x + pos_x + props->mouse_diff_control_bar_pos_x_;
new_cursor_pos_y =
container_pos.y + pos_y + props->mouse_diff_control_bar_pos_y_;
SDL_WarpMouseInWindow(stream_window_, (int)new_cursor_pos_x,
(int)new_cursor_pos_y);
}
props->reset_control_bar_pos_ = false;
} else if (!props->reset_control_bar_pos_ &&
ImGui::IsMouseReleased(ImGuiMouseButton_Left) ||
props->control_window_width_is_changing_) {
float current_x_rel = props->control_window_pos_.x - container_pos.x;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
if (current_x_rel <= container_w * 0.5f) {
pos_x = 0.0f;
if (current_y_rel + props->control_window_height_ > container_h) {
pos_y = container_h - props->control_window_height_;
} else {
pos_y = current_y_rel;
}
if (props->control_bar_expand_) {
if (props->control_window_width_ >= props->control_window_max_width_) {
props->control_window_width_ = props->control_window_max_width_;
props->control_window_width_is_changing_ = false;
} else {
props->control_window_width_is_changing_ = true;
}
} else {
if (props->control_window_width_ <= props->control_window_min_width_) {
props->control_window_width_ = props->control_window_min_width_;
props->control_window_width_is_changing_ = false;
} else {
props->control_window_width_is_changing_ = true;
}
}
props->is_control_bar_in_left_ = true;
} else if (current_x_rel > container_w * 0.5f) {
pos_x = container_w - props->control_window_width_;
pos_y = (current_y_rel >= 0.0f &&
current_y_rel <= container_h - props->control_window_height_)
? current_y_rel
: (current_y_rel < 0.0f
? 0.0f
: (container_h - props->control_window_height_));
if (props->control_bar_expand_) {
if (props->control_window_width_ >= props->control_window_max_width_) {
props->control_window_width_ = props->control_window_max_width_;
props->control_window_width_is_changing_ = false;
pos_x = container_w - props->control_window_max_width_;
} else {
props->control_window_width_is_changing_ = true;
pos_x = container_w - props->control_window_width_;
}
} else {
if (props->control_window_width_ <= props->control_window_min_width_) {
props->control_window_width_ = props->control_window_min_width_;
props->control_window_width_is_changing_ = false;
pos_x = container_w - props->control_window_min_width_;
} else {
props->control_window_width_is_changing_ = true;
pos_x = container_w - props->control_window_width_;
}
}
props->is_control_bar_in_left_ = false;
}
if (current_y_rel + props->control_window_height_ > container_h) {
pos_y = container_h - props->control_window_height_;
} else if (current_y_rel < 0.0f) {
pos_y = 0.0f;
}
} else {
float current_x_rel = props->control_window_pos_.x - container_pos.x;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
pos_x = current_x_rel;
pos_y = current_y_rel;
if (pos_x < 0.0f)
pos_x = 0.0f;
if (pos_y < 0.0f)
pos_y = 0.0f;
if (pos_x + props->control_window_width_ > container_w)
pos_x = container_w - props->control_window_width_;
if (pos_y + props->control_window_height_ > container_h)
pos_y = container_h - props->control_window_height_;
}
if (props->control_bar_expand_ && props->control_window_height_is_changing_) {
if (props->net_traffic_stats_button_pressed_) {
if (props->control_window_height_ >= props->control_window_max_height_) {
props->control_window_height_ = props->control_window_max_height_;
props->control_window_height_is_changing_ = false;
} else {
props->control_window_height_is_changing_ = true;
}
} else {
if (props->control_window_height_ <= props->control_window_min_height_) {
props->control_window_height_ = props->control_window_min_height_;
props->control_window_height_is_changing_ = false;
} else {
props->control_window_height_is_changing_ = true;
}
}
}
std::string control_window_title = props->remote_id_ + "ControlWindow";
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
static bool a, b, c, d, e;
float child_cursor_x = pos_x;
float child_cursor_y = pos_y;
ImGui::SetCursorPos(ImVec2(child_cursor_x, child_cursor_y));
std::string control_child_window_title =
props->remote_id_ + "ControlChildWindow";
ImGui::BeginChild(
control_child_window_title.c_str(),
ImVec2(props->control_window_width_, props->control_window_height_),
ImGuiChildFlags_Borders, ImGuiWindowFlags_NoDecoration);
ImGui::PopStyleColor();
props->control_window_pos_ = ImGui::GetWindowPos();
SDL_GetMouseState(&props->mouse_pos_x_, &props->mouse_pos_y_);
props->mouse_diff_control_bar_pos_x_ =
props->mouse_pos_x_ - props->control_window_pos_.x;
props->mouse_diff_control_bar_pos_y_ =
props->mouse_pos_y_ - props->control_window_pos_.y;
if (props->control_window_pos_.y < container_pos.y ||
props->control_window_pos_.y + props->control_window_height_ >
(container_pos.y + container_h) ||
props->control_window_pos_.x < container_pos.x ||
props->control_window_pos_.x + props->control_window_width_ >
(container_pos.x + container_w)) {
ImGui::ClearActiveID();
props->reset_control_bar_pos_ = true;
props->mouse_diff_control_bar_pos_x_ = 0;
props->mouse_diff_control_bar_pos_y_ = 0;
}
ControlBar(props);
props->control_bar_hovered_ = ImGui::IsWindowHovered();
ImGui::EndChild();
ImGui::End();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
return 0;
}
} // namespace crossdesk
@@ -1,253 +0,0 @@
#include <algorithm>
#include <cmath>
#include "IconsFontAwesome6.h"
#include "application/gui_application.h"
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
int CountDigits(int number) {
if (number == 0)
return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1;
}
int BitrateDisplay(int bitrate) {
int num_of_digits = CountDigits(bitrate);
if (num_of_digits <= 3) {
ImGui::Text("%d bps", bitrate);
} else if (num_of_digits > 3 && num_of_digits <= 6) {
ImGui::Text("%d kbps", bitrate / 1000);
} else {
ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
}
return 0;
}
} // namespace
int GuiApplication::FileTransferWindow(
std::shared_ptr<RemoteSession> &props) {
FileTransferState *state = &transfers_.state_for(props);
state->file_transfer_window_hovered_ = false;
// Only show window if there are files in transfer list or currently
// transferring
std::vector<RemoteSession::FileTransferInfo> file_list;
{
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
file_list = state->file_transfer_list_;
}
// Sort file list: Sending first, then Completed, then Queued, then Failed
std::sort(
file_list.begin(), file_list.end(),
[](const RemoteSession::FileTransferInfo &a,
const RemoteSession::FileTransferInfo &b) {
// Priority: Sending > Completed > Queued > Failed
auto get_priority =
[](RemoteSession::FileTransferStatus status) {
switch (status) {
case RemoteSession::FileTransferStatus::Sending:
return 0;
case RemoteSession::FileTransferStatus::Completed:
return 1;
case RemoteSession::FileTransferStatus::Queued:
return 2;
case RemoteSession::FileTransferStatus::Failed:
return 3;
}
return 3;
};
return get_priority(a.status) < get_priority(b.status);
});
// Only show window if file_transfer_window_visible_ is true
// Window can be closed by user even during transfer
// It will be reopened automatically when:
// 1. A file transfer completes (via FileTransferManager)
// 2. FileTransferManager starts the next queued file.
if (!state->file_transfer_window_visible_) {
return 0;
}
// Position window at bottom-left of stream window
// Adjust window size based on number of files
float file_transfer_window_width = main_window_width_ * 0.6f;
float file_transfer_window_height =
main_window_height_ * 0.3f; // Dynamic height
float pos_x = file_transfer_window_width * 0.05f;
float pos_y = stream_window_height_ - file_transfer_window_height -
file_transfer_window_width * 0.05;
const ImVec2 mouse_pos = ImGui::GetMousePos();
const bool mouse_in_window_rect =
mouse_pos.x >= pos_x &&
mouse_pos.x <= pos_x + file_transfer_window_width &&
mouse_pos.y >= pos_y &&
mouse_pos.y <= pos_y + file_transfer_window_height;
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
ImGui::SetNextWindowSize(
ImVec2(file_transfer_window_width, file_transfer_window_height),
ImGuiCond_Always);
if (mouse_in_window_rect) {
ImGui::SetNextWindowFocus();
}
// Set Chinese font for proper display
const bool has_chinese_font = stream_windows_system_chinese_font_ != nullptr;
if (has_chinese_font) {
ImGui::PushFont(stream_windows_system_chinese_font_);
}
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.3f));
ImGui::PushStyleColor(ImGuiCol_TitleBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::SetWindowFontScale(0.5f);
bool window_opened = true;
const bool show_contents = ImGui::Begin(
localization::file_transfer_progress[localization_language_index_]
.c_str(),
&window_opened,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoScrollbar);
ImGui::PopStyleColor(4);
ImGui::PopStyleVar(2);
state->file_transfer_window_hovered_ =
mouse_in_window_rect ||
ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
if (!window_opened) {
state->file_transfer_window_visible_ = false;
}
if (show_contents && window_opened) {
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
// Display file list
if (file_list.empty()) {
ImGui::Text("No files in transfer queue");
} else {
// Use a scrollable child window for the file list
ImGui::SetWindowFontScale(0.5f);
ImGui::BeginChild(
"FileList", ImVec2(0, file_transfer_window_height * 0.75f),
ImGuiChildFlags_Borders, ImGuiWindowFlags_HorizontalScrollbar);
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
for (size_t i = 0; i < file_list.size(); ++i) {
const auto &info = file_list[i];
ImGui::PushID(static_cast<int>(i));
// Status icon and file name
const char *status_icon = "";
ImVec4 status_color(0.5f, 0.5f, 0.5f, 1.0f);
const char *status_text = "";
switch (info.status) {
case RemoteSession::FileTransferStatus::Queued:
status_icon = ICON_FA_CLOCK;
status_color =
ImVec4(0.5f, 0.6f, 0.7f, 1.0f); // Common blue-gray for queued
status_text =
localization::queued[localization_language_index_].c_str();
break;
case RemoteSession::FileTransferStatus::Sending:
status_icon = ICON_FA_ARROW_UP;
status_color = ImVec4(0.2f, 0.6f, 1.0f, 1.0f);
status_text =
localization::sending[localization_language_index_].c_str();
break;
case RemoteSession::FileTransferStatus::Completed:
status_icon = ICON_FA_CHECK;
status_color = ImVec4(0.0f, 0.8f, 0.0f, 1.0f);
status_text =
localization::completed[localization_language_index_].c_str();
break;
case RemoteSession::FileTransferStatus::Failed:
status_icon = ICON_FA_XMARK;
status_color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
status_text =
localization::failed[localization_language_index_].c_str();
break;
}
ImGui::TextColored(status_color, "%s", status_icon);
ImGui::SameLine();
ImGui::Text("%s", info.file_name.c_str());
ImGui::SameLine();
ImGui::TextColored(status_color, "%s", status_text);
// Progress bar for sending files
if (info.status ==
RemoteSession::FileTransferStatus::Sending &&
info.file_size > 0) {
float progress = static_cast<float>(info.sent_bytes) /
static_cast<float>(info.file_size);
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
float text_height = ImGui::GetTextLineHeight();
ImGui::ProgressBar(
progress, ImVec2(file_transfer_window_width * 0.5f, text_height),
"");
ImGui::SameLine();
ImGui::Text("%.1f%%", progress * 100.0f);
ImGui::SameLine();
float speed_x_pos = file_transfer_window_width * 0.65f;
ImGui::SetCursorPosX(speed_x_pos);
BitrateDisplay(static_cast<int>(info.rate_bps));
} else if (info.status ==
RemoteSession::FileTransferStatus::Completed) {
// Show completed size
char size_str[64];
if (info.file_size < 1024) {
snprintf(size_str, sizeof(size_str), "%llu B",
(unsigned long long)info.file_size);
} else if (info.file_size < 1024 * 1024) {
snprintf(size_str, sizeof(size_str), "%.2f KB",
info.file_size / 1024.0f);
} else {
snprintf(size_str, sizeof(size_str), "%.2f MB",
info.file_size / (1024.0f * 1024.0f));
}
ImGui::Text("Size: %s", size_str);
}
ImGui::PopID();
ImGui::Spacing();
}
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::EndChild();
ImGui::SetWindowFontScale(1.0f);
}
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
}
ImGui::End();
ImGui::SetWindowFontScale(1.0f);
if (has_chinese_font) {
ImGui::PopFont();
}
return 0;
}
} // namespace crossdesk
@@ -1,670 +0,0 @@
#include "application/gui_application.h"
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
#include "tinyfiledialogs.h"
#if _WIN32 && CROSSDESK_PORTABLE
#include "service_host.h"
#endif
namespace crossdesk {
int GuiApplication::SettingWindow() {
ImGuiIO &io = ImGui::GetIO();
float portable_y_padding = 0.0f;
#if _WIN32 && CROSSDESK_PORTABLE
portable_y_padding = 0.05f;
#endif
if (show_settings_window_) {
if (settings_window_pos_reset_) {
const ImGuiViewport *viewport = ImGui::GetMainViewport();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f,
io.DisplaySize.y * (0.9f + portable_y_padding)));
#else
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f,
io.DisplaySize.y * (0.85f + portable_y_padding)));
#endif
} else {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.42f,
io.DisplaySize.y * (0.9f + portable_y_padding)));
#else
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.42f,
io.DisplaySize.y * (0.85f + portable_y_padding)));
#endif
}
settings_window_pos_reset_ = false;
}
// Settings
{
static int settings_items_padding = title_bar_button_width_ * 0.75f;
int settings_items_offset = 0;
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin(localization::settings[localization_language_index_].c_str(),
nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoSavedSettings);
ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
{
const auto &supported_languages = localization::GetSupportedLanguages();
language_button_value_ =
localization::detail::ClampLanguageIndex(language_button_value_);
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s", localization::language[localization_language_index_].c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
}
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo("##language",
localization::GetSupportedLanguages()
[localization::detail::ClampLanguageIndex(
language_button_value_)]
.display_name.c_str())) {
ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < static_cast<int>(supported_languages.size());
++i) {
bool selected = (i == language_button_value_);
if (ImGui::Selectable(supported_languages[i].display_name.c_str(),
selected))
language_button_value_ = i;
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
}
ImGui::Separator();
if (stream_window_inited_) {
ImGui::BeginDisabled();
}
{
const char *video_quality_items[] = {
localization::video_quality_low[localization_language_index_]
.c_str(),
localization::video_quality_medium[localization_language_index_]
.c_str(),
localization::video_quality_high[localization_language_index_]
.c_str()};
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::video_quality[localization_language_index_].c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
}
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo(
"##video_quality",
video_quality_items[video_quality_button_value_])) {
ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < IM_ARRAYSIZE(video_quality_items); i++) {
bool selected = (i == video_quality_button_value_);
if (ImGui::Selectable(video_quality_items[i], selected))
video_quality_button_value_ = i;
}
ImGui::EndCombo();
}
}
ImGui::Separator();
{
const char *video_frame_rate_items[] = {"30 fps", "60 fps"};
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s",
localization::video_frame_rate[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
}
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo(
"##video_frame_rate",
video_frame_rate_items[video_frame_rate_button_value_])) {
ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < IM_ARRAYSIZE(video_frame_rate_items); i++) {
bool selected = (i == video_frame_rate_button_value_);
if (ImGui::Selectable(video_frame_rate_items[i], selected))
video_frame_rate_button_value_ = i;
}
ImGui::EndCombo();
}
}
ImGui::Separator();
{
const char *video_encode_format_items[] = {
localization::h264[localization_language_index_].c_str(),
localization::av1[localization_language_index_].c_str()};
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::video_encode_format[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
}
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo(
"##video_encode_format",
video_encode_format_items[video_encode_format_button_value_])) {
ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < IM_ARRAYSIZE(video_encode_format_items); i++) {
bool selected = (i == video_encode_format_button_value_);
if (ImGui::Selectable(video_encode_format_items[i], selected))
video_encode_format_button_value_ = i;
}
ImGui::EndCombo();
}
}
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", localization::enable_hardware_video_codec
[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
}
ImGui::Checkbox("##enable_hardware_video_codec",
&enable_hardware_video_codec_);
}
#endif
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::enable_turn[localization_language_index_].c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
}
ImGui::Checkbox("##enable_turn", &enable_turn_);
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::enable_srtp[localization_language_index_].c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
}
ImGui::Checkbox("##enable_srtp", &enable_srtp_);
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 1);
ImGui::AlignTextToFramePadding();
if (ImGui::Button(localization::self_hosted_server_config
[localization_language_index_]
.c_str())) {
show_self_hosted_server_config_window_ = true;
}
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
}
ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_);
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s",
localization::enable_autostart[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
}
ImGui::Checkbox("##enable_autostart_", &enable_autostart_);
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::enable_daemon[localization_language_index_].c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
}
ImGui::Checkbox("##enable_daemon_", &enable_daemon_);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", localization::takes_effect_after_restart
[localization_language_index_]
.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::file_transfer_save_path[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
}
std::string display_path =
strlen(file_transfer_save_path_buf_) > 0
? file_transfer_save_path_buf_
: localization::default_desktop[localization_language_index_];
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.95f, 0.95f, 0.95f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.9f, 0.9f, 0.9f, 1.0f));
ImGui::PushFont(main_windows_system_chinese_font_);
if (ImGui::Button(display_path.c_str(),
ImVec2(title_bar_button_width_ * 2.0f, 0))) {
const char *folder =
tinyfd_selectFolderDialog(localization::file_transfer_save_path
[localization_language_index_]
.c_str(),
strlen(file_transfer_save_path_buf_) > 0
? file_transfer_save_path_buf_
: nullptr);
if (folder) {
strncpy(file_transfer_save_path_buf_, folder,
sizeof(file_transfer_save_path_buf_) - 1);
file_transfer_save_path_buf_[sizeof(file_transfer_save_path_buf_) -
1] = '\0';
}
}
if (ImGui::IsItemHovered() &&
strlen(file_transfer_save_path_buf_) > 0) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", file_transfer_save_path_buf_);
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ImGui::PopFont();
ImGui::PopStyleColor(3);
}
if (stream_window_inited_) {
ImGui::EndDisabled();
}
#if _WIN32 && CROSSDESK_PORTABLE
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", localization::windows_service_settings_label
[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.0f);
} else if (ConfigCenter::LANGUAGE::ENGLISH == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.42f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
}
const PortableServiceInstallState state =
portable_service_install_state_.load(std::memory_order_acquire);
const bool service_installed =
IsCrossDeskServiceInstalled() ||
state == PortableServiceInstallState::succeeded;
if (service_installed) {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.9f);
} else if (ConfigCenter::LANGUAGE::ENGLISH ==
localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.32f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
}
ImGui::Text("%s", localization::windows_service_installed
[localization_language_index_]
.c_str());
} else {
if (state == PortableServiceInstallState::installing) {
ImGui::BeginDisabled();
}
if (ImGui::Button(localization::install_windows_service
[localization_language_index_]
.c_str())) {
StartPortableWindowsServiceInstall();
}
if (state == PortableServiceInstallState::installing) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::Text("%s", localization::installing_windows_service
[localization_language_index_]
.c_str());
} else if (state == PortableServiceInstallState::failed) {
ImGui::SameLine();
ImGui::Text(
"%s",
localization::failed[localization_language_index_].c_str());
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::PushTextWrapPos(title_bar_button_width_ * 10.0f);
ImGui::TextWrapped("%s",
localization::windows_service_install_failed
[localization_language_index_]
.c_str());
ImGui::PopTextWrapPos();
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
}
}
}
#endif
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.22f);
}
settings_items_offset +=
settings_items_padding + title_bar_button_width_ * 0.15f;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::PopStyleVar();
// OK
if (ImGui::Button(
localization::ok[localization_language_index_].c_str())) {
show_settings_window_ = false;
show_self_hosted_server_config_window_ = false;
// Language
language_button_value_ =
localization::detail::ClampLanguageIndex(language_button_value_);
if (language_button_value_ == 0) {
localization_language_ = ConfigCenter::LANGUAGE::CHINESE;
} else if (language_button_value_ == 1) {
localization_language_ = ConfigCenter::LANGUAGE::ENGLISH;
} else {
localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN;
}
config_center_->SetLanguage(localization_language_);
language_button_value_last_ = language_button_value_;
localization_language_index_ = language_button_value_;
LOG_INFO("Set localization language: {}",
localization::GetSupportedLanguages()
[localization::detail::ClampLanguageIndex(
localization_language_index_)]
.code.c_str());
// Video quality
if (video_quality_button_value_ == 0) {
config_center_->SetVideoQuality(ConfigCenter::VIDEO_QUALITY::LOW);
} else if (video_quality_button_value_ == 1) {
config_center_->SetVideoQuality(ConfigCenter::VIDEO_QUALITY::MEDIUM);
} else {
config_center_->SetVideoQuality(ConfigCenter::VIDEO_QUALITY::HIGH);
}
video_quality_button_value_last_ = video_quality_button_value_;
if (video_frame_rate_button_value_ == 0) {
config_center_->SetVideoFrameRate(
ConfigCenter::VIDEO_FRAME_RATE::FPS_30);
} else if (video_frame_rate_button_value_ == 1) {
config_center_->SetVideoFrameRate(
ConfigCenter::VIDEO_FRAME_RATE::FPS_60);
}
video_frame_rate_button_value_last_ = video_frame_rate_button_value_;
// Video encode format
if (video_encode_format_button_value_ == 0) {
config_center_->SetVideoEncodeFormat(
ConfigCenter::VIDEO_ENCODE_FORMAT::H264);
} else if (video_encode_format_button_value_ == 1) {
config_center_->SetVideoEncodeFormat(
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1);
}
video_encode_format_button_value_last_ =
video_encode_format_button_value_;
// Hardware video codec
if (enable_hardware_video_codec_) {
config_center_->SetHardwareVideoCodec(true);
} else {
config_center_->SetHardwareVideoCodec(false);
}
enable_hardware_video_codec_last_ = enable_hardware_video_codec_;
// TURN mode
if (enable_turn_) {
config_center_->SetTurn(true);
} else {
config_center_->SetTurn(false);
}
enable_turn_last_ = enable_turn_;
// SRTP
if (enable_srtp_) {
config_center_->SetSrtp(true);
} else {
config_center_->SetSrtp(false);
}
enable_srtp_last_ = enable_srtp_;
if (enable_self_hosted_) {
config_center_->SetSelfHosted(true);
} else {
config_center_->SetSelfHosted(false);
}
enable_self_hosted_last_ = enable_self_hosted_;
if (enable_autostart_) {
config_center_->SetAutostart(true);
} else {
config_center_->SetAutostart(false);
}
enable_autostart_last_ = enable_autostart_;
if (enable_daemon_) {
config_center_->SetDaemon(true);
} else {
config_center_->SetDaemon(false);
}
enable_daemon_last_ = enable_daemon_;
// File transfer save path
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
file_transfer_save_path_last_ = file_transfer_save_path_buf_;
settings_window_pos_reset_ = true;
// Recreate peer instance
settings_.Load();
// Recreate peer instance
if (!stream_window_inited_) {
LOG_INFO("Recreate peer instance");
CloseAllRemoteSessions();
CreateConnectionPeer();
}
}
ImGui::SameLine();
// Cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str())) {
show_settings_window_ = false;
show_self_hosted_server_config_window_ = false;
if (language_button_value_ != language_button_value_last_) {
language_button_value_ = language_button_value_last_;
}
if (video_quality_button_value_ != video_quality_button_value_last_) {
video_quality_button_value_ = video_quality_button_value_last_;
}
if (video_frame_rate_button_value_ !=
video_frame_rate_button_value_last_) {
video_frame_rate_button_value_ = video_frame_rate_button_value_last_;
}
if (video_encode_format_button_value_ !=
video_encode_format_button_value_last_) {
video_encode_format_button_value_ =
video_encode_format_button_value_last_;
}
if (enable_hardware_video_codec_ != enable_hardware_video_codec_last_) {
enable_hardware_video_codec_ = enable_hardware_video_codec_last_;
}
if (enable_turn_ != enable_turn_last_) {
enable_turn_ = enable_turn_last_;
}
// Restore file transfer save path
strncpy(file_transfer_save_path_buf_,
file_transfer_save_path_last_.c_str(),
sizeof(file_transfer_save_path_buf_) - 1);
file_transfer_save_path_buf_[sizeof(file_transfer_save_path_buf_) - 1] =
'\0';
settings_window_pos_reset_ = true;
}
ImGui::SetWindowFontScale(0.5f);
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
}
}
return 0;
}
} // namespace crossdesk
@@ -1,60 +0,0 @@
#include "application/gui_application.h"
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
namespace crossdesk {
int GuiApplication::MainWindow() {
ImGuiIO &io = ImGui::GetIO();
float local_remote_window_width = io.DisplaySize.x;
float local_remote_window_height =
io.DisplaySize.y * (0.56f - TITLE_BAR_HEIGHT);
ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * (TITLE_BAR_HEIGHT)),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::BeginChild(
"DeskWindow",
ImVec2(local_remote_window_width, local_remote_window_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor(2);
LocalWindow();
ImDrawList *draw_list = ImGui::GetWindowDrawList();
draw_list->AddLine(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.1f),
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.53f),
IM_COL32(0, 0, 0, 122), 1.0f);
RemoteWindow();
ImGui::EndChild();
RecentConnectionsWindow();
StatusBar();
if (show_connection_status_window_) {
// std::unique_lock lock(remote_sessions_mutex_);
for (auto it = remote_sessions_.begin();
it != remote_sessions_.end();) {
auto &props = it->second;
if (focused_remote_id_ == props->remote_id_) {
if (ConnectionStatusWindow(props)) {
it = remote_sessions_.erase(it);
} else {
++it;
}
} else {
++it;
}
}
}
return 0;
}
} // namespace crossdesk
@@ -1,359 +0,0 @@
#include "application/gui_application.h"
#if _WIN32 && CROSSDESK_PORTABLE
#include <shellapi.h>
#include <algorithm>
#include <vector>
#include "localization.h"
#include "rd_log.h"
#include "service_host.h"
namespace crossdesk {
namespace {
std::filesystem::path GetCurrentExecutablePath() {
std::vector<wchar_t> buffer(MAX_PATH);
while (true) {
DWORD length = GetModuleFileNameW(nullptr, buffer.data(),
static_cast<DWORD>(buffer.size()));
if (length == 0) {
return {};
}
if (length < buffer.size()) {
return std::filesystem::path(buffer.data(), buffer.data() + length);
}
if (buffer.size() >= 32768) {
return {};
}
buffer.resize(buffer.size() * 2);
}
}
bool InstallServiceWithElevation() {
const std::filesystem::path executable_path = GetCurrentExecutablePath();
if (executable_path.empty()) {
LOG_ERROR("Portable service install failed: current executable not found");
return false;
}
const std::filesystem::path service_path =
executable_path.parent_path() / L"crossdesk_service.exe";
const std::filesystem::path helper_path =
executable_path.parent_path() / L"crossdesk_session_helper.exe";
if (!std::filesystem::exists(service_path) ||
!std::filesystem::exists(helper_path)) {
LOG_ERROR("Portable service install failed: service binaries missing, "
"service={}, "
"helper={}",
service_path.string(), helper_path.string());
return false;
}
std::wstring executable = executable_path.wstring();
std::wstring working_dir = executable_path.parent_path().wstring();
std::wstring parameters = L"--service-install";
SHELLEXECUTEINFOW execute_info{};
execute_info.cbSize = sizeof(execute_info);
execute_info.fMask = SEE_MASK_NOCLOSEPROCESS;
execute_info.hwnd = nullptr;
execute_info.lpVerb = L"runas";
execute_info.lpFile = executable.c_str();
execute_info.lpParameters = parameters.c_str();
execute_info.lpDirectory = working_dir.c_str();
execute_info.nShow = SW_HIDE;
if (!ShellExecuteExW(&execute_info)) {
LOG_ERROR("Portable service install failed: ShellExecuteExW error={}",
GetLastError());
return false;
}
DWORD wait_result = WaitForSingleObject(execute_info.hProcess, INFINITE);
DWORD exit_code = 1;
if (wait_result == WAIT_OBJECT_0) {
GetExitCodeProcess(execute_info.hProcess, &exit_code);
} else {
LOG_ERROR("Portable service install wait failed, result={}", wait_result);
}
CloseHandle(execute_info.hProcess);
if (exit_code != 0) {
LOG_ERROR("Portable service install command failed, exit_code={}",
exit_code);
return false;
}
const bool started = StartCrossDeskService();
if (!started) {
LOG_WARN("Portable service installed but start failed");
}
return IsCrossDeskServiceInstalled() && started;
}
} // namespace
void GuiApplication::CheckPortableWindowsService() {
if (portable_service_prompt_checked_) {
return;
}
portable_service_prompt_checked_ = true;
if (IsCrossDeskServiceInstalled()) {
return;
}
if (portable_service_prompt_suppressed_) {
return;
}
portable_service_install_state_.store(PortableServiceInstallState::idle,
std::memory_order_relaxed);
show_portable_service_install_window_ = true;
}
void GuiApplication::StartPortableWindowsServiceInstall() {
portable_service_do_not_remind_ = false;
PortableServiceInstallState expected = PortableServiceInstallState::idle;
if (!portable_service_install_state_.compare_exchange_strong(
expected, PortableServiceInstallState::installing,
std::memory_order_acq_rel)) {
if (expected != PortableServiceInstallState::failed) {
return;
}
portable_service_install_state_.store(
PortableServiceInstallState::installing, std::memory_order_release);
}
JoinPortableWindowsServiceInstallThread();
portable_service_install_thread_ = std::thread([this]() {
const bool installed = InstallServiceWithElevation();
portable_service_install_state_.store(
installed ? PortableServiceInstallState::succeeded
: PortableServiceInstallState::failed,
std::memory_order_release);
});
}
void GuiApplication::JoinPortableWindowsServiceInstallThread() {
if (portable_service_install_thread_.joinable()) {
portable_service_install_thread_.join();
}
}
int GuiApplication::PortableServiceInstallWindow() {
if (!show_portable_service_install_window_ &&
!show_portable_service_prompt_suppressed_window_) {
return 0;
}
const ImGuiViewport *viewport = ImGui::GetMainViewport();
const float window_width =
(std::min)(viewport->WorkSize.x * 0.6f, title_bar_button_width_ * 18.0f);
const float window_height =
(std::min)(viewport->WorkSize.y * 0.5f, title_bar_button_width_ * 8.0f);
if (show_portable_service_prompt_suppressed_window_) {
const float notice_width = window_width;
const float notice_height = (std::min)(viewport->WorkSize.y * 0.35f,
title_bar_button_width_ * 4.6f);
ImGui::SetNextWindowPos(
ImVec2(viewport->WorkPos.x +
(viewport->WorkSize.x - notice_width) / 2.0f,
viewport->WorkPos.y +
(viewport->WorkSize.y - notice_height) / 2.0f),
ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(notice_width, notice_height),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin(
localization::notification[localization_language_index_].c_str(),
nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoTitleBar);
ImGui::Spacing();
ImGui::SetWindowFontScale(0.55f);
ImGui::SetCursorPosX(notice_width * 0.08f);
ImGui::Text(
"%s", localization::notification[localization_language_index_].c_str());
ImGui::SetWindowFontScale(0.5f);
ImGui::SetCursorPosX(notice_width * 0.06f);
ImGui::SetCursorPosY(notice_height * 0.28f);
ImGui::PushTextWrapPos(notice_width * 0.88f);
ImGui::TextWrapped("%s",
localization::windows_service_prompt_suppressed_message
[localization_language_index_]
.c_str());
ImGui::PopTextWrapPos();
const std::string ok_label = localization::ok[localization_language_index_];
const ImGuiStyle &style = ImGui::GetStyle();
const float ok_width =
ImGui::CalcTextSize(ok_label.c_str()).x + style.FramePadding.x * 2.0f;
ImGui::SetCursorPosX((notice_width - ok_width) * 0.5f);
ImGui::SetCursorPosY(notice_height * 0.75f);
if (ImGui::Button(ok_label.c_str())) {
show_portable_service_prompt_suppressed_window_ = false;
}
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
}
if (!show_portable_service_install_window_) {
return 0;
}
ImGui::SetNextWindowPos(
ImVec2(viewport->WorkPos.x + (viewport->WorkSize.x - window_width) / 2.0f,
viewport->WorkPos.y +
(viewport->WorkSize.y - window_height) / 2.0f),
ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin(
localization::windows_service_setup_title[localization_language_index_]
.c_str(),
nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoTitleBar);
ImGui::Spacing();
ImGui::SetWindowFontScale(0.55f);
ImGui::SetCursorPosX(window_width * 0.08f);
ImGui::Text(
"%s",
localization::windows_service_setup_title[localization_language_index_]
.c_str());
const PortableServiceInstallState state =
portable_service_install_state_.load(std::memory_order_acquire);
const char *status_text = nullptr;
if (state == PortableServiceInstallState::installing ||
state == PortableServiceInstallState::succeeded ||
state == PortableServiceInstallState::failed) {
status_text =
localization::installing_windows_service[localization_language_index_]
.c_str();
if (state == PortableServiceInstallState::succeeded) {
status_text = localization::windows_service_install_success
[localization_language_index_]
.c_str();
} else if (state == PortableServiceInstallState::failed) {
status_text = localization::windows_service_install_failed
[localization_language_index_]
.c_str();
}
}
ImGui::SetWindowFontScale(0.45f);
ImGui::SetCursorPosX(window_width * 0.04f);
ImGui::SetCursorPosY(window_height * 0.22f);
ImGui::BeginChild("PortableServiceInstallContent",
ImVec2(window_width * 0.92f, window_height * 0.45f),
ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
ImGui::SetWindowFontScale(0.5f);
const float wrap_pos = ImGui::GetContentRegionAvail().x;
ImGui::PushTextWrapPos(wrap_pos);
ImGui::TextWrapped(
"%s",
localization::windows_service_setup_message[localization_language_index_]
.c_str());
if (status_text != nullptr) {
ImGui::Spacing();
ImGui::TextWrapped("%s", status_text);
}
ImGui::PopTextWrapPos();
ImGui::EndChild();
ImGui::SetWindowFontScale(0.5f);
ImGui::SetCursorPosX(window_width * 0.08f);
ImGui::SetCursorPosY(window_height * 0.71f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
ImGui::Checkbox(
localization::do_not_remind_again[localization_language_index_].c_str(),
&portable_service_do_not_remind_);
ImGui::PopStyleVar();
const float button_y = window_height * 0.84f;
const ImGuiStyle &style = ImGui::GetStyle();
const auto default_button_width = [&style](const std::string &label) {
return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f;
};
const std::string install_label =
localization::install_windows_service[localization_language_index_];
const std::string cancel_label =
localization::cancel[localization_language_index_];
const std::string ok_label = localization::ok[localization_language_index_];
const float buttons_width = state == PortableServiceInstallState::succeeded
? default_button_width(ok_label)
: default_button_width(install_label) +
style.ItemSpacing.x +
default_button_width(cancel_label);
ImGui::SetCursorPosX((window_width - buttons_width) * 0.5f);
ImGui::SetCursorPosY(button_y);
if (state == PortableServiceInstallState::succeeded) {
if (ImGui::Button(ok_label.c_str())) {
show_portable_service_install_window_ = false;
JoinPortableWindowsServiceInstallThread();
}
} else {
if (state == PortableServiceInstallState::installing) {
ImGui::BeginDisabled();
}
if (ImGui::Button(install_label.c_str())) {
StartPortableWindowsServiceInstall();
}
if (state == PortableServiceInstallState::installing) {
ImGui::EndDisabled();
}
ImGui::SameLine();
if (state == PortableServiceInstallState::installing) {
ImGui::BeginDisabled();
}
if (ImGui::Button(cancel_label.c_str())) {
if (portable_service_do_not_remind_) {
portable_service_prompt_suppressed_ = true;
config_center_->SetPortableServicePromptSuppressed(true);
show_portable_service_prompt_suppressed_window_ = true;
}
show_portable_service_install_window_ = false;
}
if (state == PortableServiceInstallState::installing) {
ImGui::EndDisabled();
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
return 0;
}
} // namespace crossdesk
#endif
@@ -1,197 +0,0 @@
#include <filesystem>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#endif
#include "application/gui_application.h"
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
namespace crossdesk {
std::vector<std::string> GetRootEntries() {
std::vector<std::string> roots;
#ifdef _WIN32
DWORD mask = GetLogicalDrives();
for (char letter = 'A'; letter <= 'Z'; ++letter) {
if (mask & 1) {
roots.push_back(std::string(1, letter) + ":\\");
}
mask >>= 1;
}
#else
roots.push_back("/");
#endif
return roots;
}
int GuiApplication::SelfHostedServerWindow() {
ImGuiIO &io = ImGui::GetIO();
if (show_self_hosted_server_config_window_) {
if (self_hosted_server_config_window_pos_reset_) {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.298f, io.DisplaySize.y * 0.25f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.35f));
} else {
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.27f, io.DisplaySize.y * 0.3f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.465f, io.DisplaySize.y * 0.35f));
}
self_hosted_server_config_window_pos_reset_ = false;
}
// Settings
{
ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin(localization::self_hosted_server_settings
[localization_language_index_]
.c_str(),
nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoSavedSettings);
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
{
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", localization::self_hosted_server_address
[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
}
ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
ImGui::InputText("##signal_server_ip_self_", signal_server_ip_self_,
IM_ARRAYSIZE(signal_server_ip_self_),
ImGuiInputTextFlags_AlwaysOverwrite);
}
ImGui::Separator();
{
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::self_hosted_server_port[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
}
ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
ImGui::InputText("##signal_server_port_self_", signal_server_port_self_,
IM_ARRAYSIZE(signal_server_port_self_));
}
ImGui::Separator();
{
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", localization::self_hosted_server_coturn_server_port
[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
}
ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
ImGui::InputText("##coturn_server_port_self_", coturn_server_port_self_,
IM_ARRAYSIZE(coturn_server_port_self_));
}
if (stream_window_inited_) {
ImGui::EndDisabled();
}
ImGui::Dummy(ImVec2(0.0f, title_bar_button_width_ * 0.25f));
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.32f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.7f);
}
ImGui::PopStyleVar();
// OK
if (ImGui::Button(
localization::ok[localization_language_index_].c_str())) {
show_self_hosted_server_config_window_ = false;
config_center_->SetServerHost(signal_server_ip_self_);
config_center_->SetServerPort(atoi(signal_server_port_self_));
config_center_->SetCoturnServerPort(atoi(coturn_server_port_self_));
strncpy(signal_server_ip_, signal_server_ip_self_,
sizeof(signal_server_ip_) - 1);
signal_server_ip_[sizeof(signal_server_ip_) - 1] = '\0';
strncpy(signal_server_port_, signal_server_port_self_,
sizeof(signal_server_port_) - 1);
signal_server_port_[sizeof(signal_server_port_) - 1] = '\0';
strncpy(coturn_server_port_, coturn_server_port_self_,
sizeof(coturn_server_port_) - 1);
coturn_server_port_[sizeof(coturn_server_port_) - 1] = '\0';
self_hosted_server_config_window_pos_reset_ = true;
}
ImGui::SameLine();
// Cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str())) {
show_self_hosted_server_config_window_ = false;
self_hosted_server_config_window_pos_reset_ = true;
strncpy(signal_server_ip_self_,
config_center_->GetSignalServerHost().c_str(),
sizeof(signal_server_ip_self_) - 1);
signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0';
int signal_port = config_center_->GetSignalServerPort();
if (signal_port > 0) {
strncpy(signal_server_port_self_, std::to_string(signal_port).c_str(),
sizeof(signal_server_port_self_) - 1);
signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0';
} else {
signal_server_port_self_[0] = '\0';
}
int coturn_port = config_center_->GetCoturnServerPort();
if (coturn_port > 0) {
strncpy(coturn_server_port_self_, std::to_string(coturn_port).c_str(),
sizeof(coturn_server_port_self_) - 1);
coturn_server_port_self_[sizeof(coturn_server_port_self_) - 1] = '\0';
} else {
coturn_server_port_self_[0] = '\0';
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
ImGui::SetWindowFontScale(1.0f);
}
}
return 0;
}
} // namespace crossdesk
@@ -1,388 +0,0 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <memory>
#include <shared_mutex>
#include <string>
#include <vector>
#include "application/gui_application.h"
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "rounded_corner_button.h"
namespace crossdesk {
namespace {
int CountDigits(int number) {
if (number == 0)
return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1;
}
void BitrateDisplay(uint32_t bitrate) {
const int num_of_digits = CountDigits(static_cast<int>(bitrate));
if (num_of_digits <= 3) {
ImGui::Text("%u bps", bitrate);
} else if (num_of_digits > 3 && num_of_digits <= 6) {
ImGui::Text("%u kbps", bitrate / 1000);
} else {
ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
}
}
std::string FormatBytes(uint64_t bytes) {
char buf[64];
if (bytes < 1024ULL) {
std::snprintf(buf, sizeof(buf), "%llu B", (unsigned long long)bytes);
} else if (bytes < 1024ULL * 1024ULL) {
std::snprintf(buf, sizeof(buf), "%.2f KB", bytes / 1024.0);
} else if (bytes < 1024ULL * 1024ULL * 1024ULL) {
std::snprintf(buf, sizeof(buf), "%.2f MB", bytes / (1024.0 * 1024.0));
} else {
std::snprintf(buf, sizeof(buf), "%.2f GB",
bytes / (1024.0 * 1024.0 * 1024.0));
}
return std::string(buf);
}
} // namespace
int GuiApplication::ServerWindow() {
ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_),
ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("##server_window", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
server_window_title_bar_height_ = title_bar_height_;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::BeginChild(
"ServerTitleBar",
ImVec2(server_window_width_, server_window_title_bar_height_),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
float server_title_bar_button_width = server_window_title_bar_height_;
float server_title_bar_button_height = server_window_title_bar_height_;
// Collapse/expand toggle button (FontAwesome icon).
{
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::SetWindowFontScale(0.5f);
const char *icon =
server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP;
std::string toggle_label = std::string(icon) + "##server_toggle";
bool toggle_clicked = RoundedCornerButton(
toggle_label.c_str(),
ImVec2(server_title_bar_button_width, server_title_bar_button_height),
8.5f, ImDrawFlags_RoundCornersTopLeft, true, IM_COL32(0, 0, 0, 0),
IM_COL32(0, 0, 0, 25), IM_COL32(255, 255, 255, 255));
if (toggle_clicked) {
if (server_window_) {
int w = 0;
int h = 0;
int x = 0;
int y = 0;
SDL_GetWindowSize(server_window_, &w, &h);
SDL_GetWindowPosition(server_window_, &x, &y);
if (server_window_collapsed_) {
const int normal_h = server_window_normal_height_;
SDL_SetWindowSize(server_window_, w, normal_h);
SDL_SetWindowPosition(server_window_, x, y);
server_window_collapsed_ = false;
} else {
const int collapsed_h = (int)server_window_title_bar_height_;
// Collapse upward: keep top edge stable.
SDL_SetWindowSize(server_window_, w, collapsed_h);
SDL_SetWindowPosition(server_window_, x, y);
server_window_collapsed_ = true;
}
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
}
ImGui::EndChild();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
RemoteClientInfoWindow();
ImGui::End();
return 0;
}
int GuiApplication::RemoteClientInfoWindow() {
float remote_client_info_window_width = server_window_width_ * 0.8f;
float remote_client_info_window_height =
(server_window_height_ - server_window_title_bar_height_) * 0.9f;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
ImGui::BeginChild(
"RemoteClientInfoWindow",
ImVec2(remote_client_info_window_width, remote_client_info_window_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
std::vector<std::pair<std::string, std::string>> remote_entries;
{
std::shared_lock lock(connection_status_mutex_);
remote_entries.reserve(connection_status_.size());
for (const auto &kv : connection_status_) {
const auto host_it = connection_host_names_.find(kv.first);
const std::string display_name =
(host_it != connection_host_names_.end() && !host_it->second.empty())
? host_it->second
: kv.first;
remote_entries.emplace_back(kv.first, display_name);
}
}
auto find_display_name_by_remote_id =
[&remote_entries](const std::string &remote_id) -> std::string {
for (const auto &entry : remote_entries) {
if (entry.first == remote_id) {
return entry.second;
}
}
return {};
};
if (!selected_server_remote_id_.empty() &&
find_display_name_by_remote_id(selected_server_remote_id_).empty()) {
selected_server_remote_id_.clear();
selected_server_remote_hostname_.clear();
}
if (selected_server_remote_id_.empty() && !remote_entries.empty()) {
selected_server_remote_id_ = remote_entries.front().first;
}
if (!selected_server_remote_id_.empty()) {
selected_server_remote_hostname_ =
find_display_name_by_remote_id(selected_server_remote_id_);
}
ImGui::SetWindowFontScale(font_scale);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s",
localization::controller[localization_language_index_].c_str());
ImGui::SameLine();
const char *selected_preview = "-";
if (!selected_server_remote_hostname_.empty()) {
selected_preview = selected_server_remote_hostname_.c_str();
} else if (!remote_client_id_.empty()) {
selected_preview = remote_client_id_.c_str();
}
ImGui::SetNextItemWidth(remote_client_info_window_width *
(localization_language_index_ == 0 ? 0.68f : 0.62f));
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
ImGui::AlignTextToFramePadding();
if (ImGui::BeginCombo("##server_remote_id", selected_preview)) {
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
for (int i = 0; i < static_cast<int>(remote_entries.size()); i++) {
const bool selected =
(remote_entries[i].first == selected_server_remote_id_);
if (ImGui::Selectable(remote_entries[i].second.c_str(), selected)) {
selected_server_remote_id_ = remote_entries[i].first;
selected_server_remote_hostname_ = remote_entries[i].second;
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
ImGui::Separator();
ImGui::SetWindowFontScale(font_scale);
if (!selected_server_remote_id_.empty()) {
ConnectionStatus status = ConnectionStatus::Closed;
{
std::shared_lock lock(connection_status_mutex_);
auto it = connection_status_.find(selected_server_remote_id_);
if (it != connection_status_.end()) {
status = it->second;
}
}
ImGui::Text(
"%s",
localization::connection_status[localization_language_index_].c_str());
ImGui::SameLine();
switch (status) {
case ConnectionStatus::Connected:
ImGui::Text(
"%s",
localization::p2p_connected[localization_language_index_].c_str());
break;
case ConnectionStatus::Connecting:
ImGui::Text(
"%s",
localization::p2p_connecting[localization_language_index_].c_str());
break;
case ConnectionStatus::Disconnected:
ImGui::Text(
"%s",
localization::p2p_disconnected[localization_language_index_].c_str());
break;
case ConnectionStatus::Failed:
ImGui::Text(
"%s", localization::p2p_failed[localization_language_index_].c_str());
break;
case ConnectionStatus::Closed:
ImGui::Text(
"%s", localization::p2p_closed[localization_language_index_].c_str());
break;
default:
ImGui::Text(
"%s", localization::p2p_failed[localization_language_index_].c_str());
break;
}
}
ImGui::Separator();
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s", localization::file_transfer[localization_language_index_].c_str());
ImGui::SameLine();
if (ImGui::Button(
localization::select_file[localization_language_index_].c_str())) {
std::string title = localization::select_file[localization_language_index_];
std::string path = OpenFileDialog(title);
LOG_INFO("Selected file path: {}", path.c_str());
transfers_.ProcessSelectedFile(path, nullptr, file_label_,
selected_server_remote_id_);
}
auto &file_transfer = transfers_.global_state();
if (file_transfer.file_transfer_window_visible_) {
ImGui::SameLine();
const bool is_sending = file_transfer.file_sending_.load();
if (is_sending) {
// Simple animation: cycle icon every 0.5s while sending.
static const char *kFileTransferIcons[] = {ICON_FA_ANGLE_UP,
ICON_FA_ANGLES_UP};
const int icon_index = static_cast<int>(ImGui::GetTime() / 0.5) %
(static_cast<int>(sizeof(kFileTransferIcons) /
sizeof(kFileTransferIcons[0])));
ImGui::Text("%s", kFileTransferIcons[icon_index]);
} else {
// Completed.
ImGui::Text("%s", ICON_FA_CHECK);
}
if (ImGui::IsItemHovered()) {
const uint64_t sent_bytes = file_transfer.file_sent_bytes_.load();
const uint64_t total_bytes = file_transfer.file_total_bytes_.load();
const uint32_t rate_bps = file_transfer.file_send_rate_bps_.load();
float progress = 0.0f;
if (total_bytes > 0) {
progress =
static_cast<float>(sent_bytes) / static_cast<float>(total_bytes);
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
}
std::string current_file_name;
const uint32_t current_file_id = file_transfer.current_file_id_.load();
if (current_file_id != 0) {
std::lock_guard<std::mutex> lock(
file_transfer.file_transfer_list_mutex_);
for (const auto &info : file_transfer.file_transfer_list_) {
if (info.file_id == current_file_id) {
current_file_name = info.file_name;
break;
}
}
}
ImGui::BeginTooltip();
if (server_windows_system_chinese_font_) {
ImGui::PushFont(server_windows_system_chinese_font_);
}
ImGui::SetWindowFontScale(0.5f);
if (!current_file_name.empty()) {
ImGui::Text("%s", current_file_name.c_str());
}
if (total_bytes > 0) {
const std::string sent_str = FormatBytes(sent_bytes);
const std::string total_str = FormatBytes(total_bytes);
ImGui::Text("%s / %s", sent_str.c_str(), total_str.c_str());
}
const float text_height = ImGui::GetTextLineHeight();
char overlay[32];
std::snprintf(overlay, sizeof(overlay), "%.1f%%", progress * 100.0f);
ImGui::ProgressBar(progress, ImVec2(180.0f, text_height), overlay);
BitrateDisplay(rate_bps);
ImGui::SetWindowFontScale(1.0f);
if (server_windows_system_chinese_font_) {
ImGui::PopFont();
}
ImGui::EndTooltip();
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::EndChild();
ImGui::SameLine();
float close_connection_button_width = server_window_width_ * 0.1f;
float close_connection_button_height = remote_client_info_window_height;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0.5f, 0.5f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_);
ImGui::SetWindowFontScale(font_scale);
if (ImGui::Button(ICON_FA_XMARK, ImVec2(close_connection_button_width,
close_connection_button_height))) {
if (peer_ && !selected_server_remote_id_.empty()) {
LeaveConnection(peer_, selected_server_remote_id_.c_str());
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
ImGui::PopStyleVar();
return 0;
}
} // namespace crossdesk

Some files were not shown because too many files have changed in this diff Show More