ci: update Linux build image to Ubuntu 20.04

This commit is contained in:
dijunkun
2026-07-30 12:32:46 +08:00
50 changed files with 2823 additions and 1120 deletions
+6 -4
View File
@@ -26,8 +26,9 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
IMAGE_NAME: crossdesk/ubuntu22.04 IMAGE_NAME: crossdesk/ubuntu20.04
STACK_TAG: buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0 STACK_TAG: buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
CMAKE_VERSION: 3.31.6
XMAKE_VERSION: 3.0.9 XMAKE_VERSION: 3.0.9
RUST_VERSION: 1.92.0 RUST_VERSION: 1.92.0
RUSTUP_VERSION: 1.28.2 RUSTUP_VERSION: 1.28.2
@@ -44,11 +45,11 @@ jobs:
- arch: amd64 - arch: amd64
platform: linux/amd64 platform: linux/amd64
runner: ubuntu-24.04 runner: ubuntu-24.04
base_image: nvidia/cuda:12.6.3-devel-ubuntu22.04 base_image: nvidia/cuda:12.6.3-devel-ubuntu20.04
- arch: arm64 - arch: arm64
platform: linux/arm64 platform: linux/arm64
runner: ubuntu-24.04-arm runner: ubuntu-24.04-arm
base_image: ubuntu:22.04 base_image: ubuntu:20.04
steps: steps:
- name: Set candidate tag - name: Set candidate tag
id: image id: image
@@ -86,6 +87,7 @@ jobs:
sbom: false sbom: false
build-args: | build-args: |
BASE_IMAGE=${{ matrix.base_image }} BASE_IMAGE=${{ matrix.base_image }}
CMAKE_VERSION=${{ env.CMAKE_VERSION }}
XMAKE_VERSION=${{ env.XMAKE_VERSION }} XMAKE_VERSION=${{ env.XMAKE_VERSION }}
RUST_VERSION=${{ env.RUST_VERSION }} RUST_VERSION=${{ env.RUST_VERSION }}
RUSTUP_VERSION=${{ env.RUSTUP_VERSION }} RUSTUP_VERSION=${{ env.RUSTUP_VERSION }}
+70 -12
View File
@@ -29,12 +29,12 @@ jobs:
include: include:
- arch: amd64 - arch: amd64
runner: ubuntu-24.04 runner: ubuntu-24.04
image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0 image: crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
package_script: ./scripts/linux/pkg_amd64.sh package_script: ./scripts/linux/pkg_amd64.sh
- arch: arm64 - arch: arm64
runner: ubuntu-24.04-arm runner: ubuntu-24.04-arm
image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0 image: crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
package_script: ./scripts/linux/pkg_arm64.sh package_script: ./scripts/linux/pkg_arm64.sh
container: container:
@@ -103,12 +103,33 @@ jobs:
- name: Verify Linux build image - name: Verify Linux build image
run: verify-crossdesk-build-image run: verify-crossdesk-build-image
- name: Ensure Wayland build dependencies
shell: bash
run: |
if pkg-config --exists dbus-1 libdrm && \
test -f /opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h && \
test -f /opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h; then
exit 0
fi
apt-get update
apt-get install -y --no-install-recommends \
libdbus-1-dev \
libdrm-dev
rm -rf /var/lib/apt/lists/*
install-crossdesk-pipewire-sdk
pkg-config --exists dbus-1 libdrm
test -f /opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h
test -f /opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h
- name: Build CrossDesk - name: Build CrossDesk
env: env:
CUDA_PATH: /usr/local/cuda CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data XMAKE_GLOBALDIR: /data
run: | run: |
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --USE_WAYLAND=true \
--cc=gcc-10 --cxx=g++-10 --root -y
xmake b -vy --root crossdesk xmake b -vy --root crossdesk
- name: Package - name: Package
@@ -116,13 +137,19 @@ jobs:
chmod +x ${{ matrix.package_script }} chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${LEGAL_VERSION} ${{ matrix.package_script }} ${LEGAL_VERSION}
- name: Verify packaged Ubuntu 22.04 glibc baseline - name: Verify Ubuntu 20.04 package dependency resolution
run: |
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb"
apt-get update
apt-get install --simulate "${PACKAGE_FILE}"
- name: Verify packaged Ubuntu 20.04 glibc baseline
run: | run: |
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb" PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb"
VERIFY_DIR="${RUNNER_TEMP}/crossdesk-glibc-${{ matrix.arch }}" VERIFY_DIR="${RUNNER_TEMP}/crossdesk-glibc-${{ matrix.arch }}"
install -d "${VERIFY_DIR}" install -d "${VERIFY_DIR}"
dpkg-deb --extract "${PACKAGE_FILE}" "${VERIFY_DIR}" dpkg-deb --extract "${PACKAGE_FILE}" "${VERIFY_DIR}"
verify-crossdesk-glibc-baseline "${VERIFY_DIR}" 2.35 verify-crossdesk-glibc-baseline "${VERIFY_DIR}" 2.31
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v6
@@ -134,17 +161,22 @@ jobs:
build-macos: build-macos:
name: Build on macOS name: Build on macOS
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
env:
MACOSX_DEPLOYMENT_TARGET: "14.0"
EXPECTED_MACOS_SDK: "26.5"
strategy: strategy:
matrix: matrix:
include: include:
- arch: x64 - arch: x64
runner: macos-15-intel binary_arch: x86_64
cache-key: intel runner: macos-26-intel
cache-key: intel-macos26-xcode26.6-min14
out-dir: ./build/macosx/x86_64/release/crossdesk out-dir: ./build/macosx/x86_64/release/crossdesk
package_script: ./scripts/macosx/pkg_x64.sh package_script: ./scripts/macosx/pkg_x64.sh
- arch: arm64 - arch: arm64
runner: macos-14 binary_arch: arm64
cache-key: arm runner: macos-26
cache-key: arm-macos26-xcode26.6-min14
out-dir: ./build/macosx/arm64/release/crossdesk out-dir: ./build/macosx/arm64/release/crossdesk
package_script: ./scripts/macosx/pkg_arm64.sh package_script: ./scripts/macosx/pkg_arm64.sh
@@ -189,6 +221,17 @@ jobs:
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
- name: Select Xcode 26.6
shell: bash
run: |
sudo xcode-select -s /Applications/Xcode_26.6.app/Contents/Developer
xcodebuild -version
SDK_VERSION="$(xcrun --sdk macosx --show-sdk-version)"
if [[ "${SDK_VERSION}" != "${EXPECTED_MACOS_SDK}" ]]; then
echo "Unexpected macOS SDK: expected ${EXPECTED_MACOS_SDK}, got ${SDK_VERSION}" >&2
exit 1
fi
- name: Cache xmake dependencies - name: Cache xmake dependencies
uses: actions/cache@v5 uses: actions/cache@v5
with: with:
@@ -211,7 +254,7 @@ jobs:
- name: Build CrossDesk - name: Build CrossDesk
run: | run: |
xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
xmake b -vy crossdesk xmake b -vy crossdesk
- name: Package CrossDesk app - name: Package CrossDesk app
@@ -219,6 +262,22 @@ jobs:
chmod +x ${{ matrix.package_script }} chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${VERSION_NUM} ${{ matrix.package_script }} ${VERSION_NUM}
- name: Verify packaged app
shell: bash
run: |
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-macos-${{ matrix.arch }}-${VERSION_NUM}.pkg"
VERIFY_DIR="$(mktemp -d "${RUNNER_TEMP}/crossdesk-macos-pkg.XXXXXX")"
trap 'rm -rf "${VERIFY_DIR}"' EXIT
pkgutil --expand-full "${PACKAGE_FILE}" "${VERIFY_DIR}/expanded"
APP_PATH="$(find "${VERIFY_DIR}/expanded" -type d -name 'CrossDesk.app' -print -quit)"
if [[ -z "${APP_PATH}" ]]; then
echo "CrossDesk.app is missing from ${PACKAGE_FILE}" >&2
exit 1
fi
./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" "${EXPECTED_MACOS_SDK}"
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v6
with: with:
@@ -404,8 +463,7 @@ jobs:
release: release:
name: Publish Release name: Publish Release
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
needs: needs: [build-linux, build-macos, build-windows-x64]
[build-linux, build-macos, build-windows-x64]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
+27 -8
View File
@@ -32,7 +32,7 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
|----------------|---------------------------| |----------------|---------------------------|
| **Windows** | Windows 10 及以上 (64 位) | | **Windows** | Windows 10 及以上 (64 位) |
| **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 | | **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 |
| **Linux** | Ubuntu 22.04 及以上 ( 低版本可自行编译实现兼容 ) | | **Linux** | Ubuntu 20.04 及以上 |
## 使用 ## 使用
@@ -80,27 +80,39 @@ Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_
依赖: 依赖:
- [xmake](https://xmake.io/#/guide/installation) - [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/) - [cmake](https://cmake.org/download/) 3.21 及以上
### Linux ### Linux
Linux 构建目前支持 Ubuntu 22.04 及以上版本的 amd64 和 arm64。先安装基础编译依赖: Linux 构建支持 Ubuntu 20.04 及以上版本的 amd64 和 arm64。发布安装包以
Ubuntu 20.04/glibc 2.31 为兼容基线。先安装基础编译依赖:
```bash ```bash
sudo apt-get update sudo apt-get update
sudo apt-get install -y \ sudo apt-get install -y \
git curl unzip build-essential cmake pkg-config binutils dpkg-dev \ 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 \ libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \ libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \ libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \ libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
libasound2-dev libsndio-dev libpulse-dev libasound2-dev libsndio-dev libpulse-dev
# Ubuntu 20.04 自带的 CMake 3.16 不满足 Slint 1.17 的要求
python3 -m pip install --user cmake==3.31.6
export PATH="$HOME/.local/bin:$PATH"
``` ```
Ubuntu 20.04 默认的 GCC 9 缺少 Slint C++ API 所需的部分 C++20 标准库,
因此上述命令同时安装并在后续配置中指定 GCC 10。
需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖: 需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖:
```bash ```bash
sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev 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 版本: 下载子模块并编译 Release 版本:
@@ -112,7 +124,11 @@ cd crossdesk
# 已经克隆过仓库时执行这一行 # 已经克隆过仓库时执行这一行
git submodule update --init --recursive git submodule update --init --recursive
xmake f -m release --USE_CUDA=false -y # Ubuntu 20.04 没有 PipeWire 0.3 开发包,安装仅用于编译的头文件 SDK。
# 已安装发行版 PipeWire 0.3 开发包时可跳过此步骤。
sudo ./docker/linux-build/install-pipewire-sdk.sh
xmake f -m release --USE_CUDA=false --cc=gcc-10 --cxx=g++-10 -y
xmake b -vy crossdesk xmake b -vy crossdesk
``` ```
@@ -121,7 +137,8 @@ amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`arm64 构建
启用 Wayland 和 DRM 的配置示例: 启用 Wayland 和 DRM 的配置示例:
```bash ```bash
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
--cc=gcc-10 --cxx=g++-10 -y
xmake b -vy crossdesk xmake b -vy crossdesk
``` ```
@@ -136,6 +153,8 @@ xmake b -vy crossdesk
``` ```
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so` 打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`
PipeWire 不属于强制运行时依赖:程序在运行时检测宿主系统的 PipeWire 0.3
没有该运行库时仍可使用 X11(以及构建时启用的 DRM)捕获。
### 通用编译选项 ### 通用编译选项
@@ -158,7 +177,7 @@ xmake r crossdesk
### 无 CUDA 环境下的开发支持 ### 无 CUDA 环境下的开发支持
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 22.04 Docker 镜像](https://hub.docker.com/r/crossdesk/ubuntu22.04)。该镜像内置必要的构建依赖,可在容器中开箱即用,无需额外配置即可直接编译项目 对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 20.04 兼容构建镜像](https://hub.docker.com/r/crossdesk/ubuntu20.04)。该镜像内置必要的构建依赖,可生成兼容 glibc 2.31 的单一 Linux 安装包
进入容器,下载工程后执行: 进入容器,下载工程后执行:
``` ```
+32 -9
View File
@@ -32,7 +32,7 @@ CrossDesk is an experimental application of [MiniRTC](https://github.com/kunkund
|-----------|-----------------| |-----------|-----------------|
| **Windows** | Windows 10 or later (64-bit) | | **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 | | **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 22.04 or later *(older versions can be built manually for compatibility)* | | **Linux** | Ubuntu 20.04 or later |
## Usage ## Usage
@@ -85,27 +85,41 @@ If the remote Windows service is not installed, not running, or temporarily unav
Requirements: Requirements:
- [xmake](https://xmake.io/#/guide/installation) - [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/) - [cmake](https://cmake.org/download/) 3.21 or later
### Linux ### Linux
Linux builds currently support Ubuntu 22.04 or later on amd64 and arm64. Install the base build dependencies first: 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:
```bash ```bash
sudo apt-get update sudo apt-get update
sudo apt-get install -y \ sudo apt-get install -y \
git curl unzip build-essential cmake pkg-config binutils dpkg-dev \ 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 \ libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \ libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \ libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \ libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
libasound2-dev libsndio-dev libpulse-dev libasound2-dev libsndio-dev libpulse-dev
# Ubuntu 20.04's CMake 3.16 is too old for Slint 1.17.
python3 -m pip install --user cmake==3.31.6
export PATH="$HOME/.local/bin:$PATH"
``` ```
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.
Install the optional dependencies when Wayland capture or DRM capture is enabled: Install the optional dependencies when Wayland capture or DRM capture is enabled:
```bash ```bash
sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev sudo apt-get install -y libdbus-1-dev libdrm-dev
# 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: Clone the submodules and build a release binary:
@@ -117,7 +131,12 @@ cd crossdesk
# Run this when the repository has already been cloned # Run this when the repository has already been cloned
git submodule update --init --recursive git submodule update --init --recursive
xmake f -m release --USE_CUDA=false -y # 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 -m release --USE_CUDA=false --cc=gcc-10 --cxx=g++-10 -y
xmake b -vy crossdesk xmake b -vy crossdesk
``` ```
@@ -126,7 +145,8 @@ The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64
Example configuration with Wayland and DRM capture enabled: Example configuration with Wayland and DRM capture enabled:
```bash ```bash
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
--cc=gcc-10 --cxx=g++-10 -y
xmake b -vy crossdesk xmake b -vy crossdesk
``` ```
@@ -141,6 +161,9 @@ Build a Debian package after compiling a release binary on the matching architec
``` ```
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. 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 ### Common build options
@@ -163,8 +186,8 @@ xmake r crossdesk
#### Development Without CUDA Environment #### 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 22.04 Docker image](https://hub.docker.com/r/crossdesk/ubuntu22.04) is provided. 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 comes with all required build dependencies and allows you to build the project directly inside the container without any additional setup. 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: After entering the container, download the project and run:
``` ```
+25 -4
View File
@@ -1,15 +1,16 @@
# syntax=docker/dockerfile:1.7 # syntax=docker/dockerfile:1.7
ARG BASE_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu22.04 ARG BASE_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu20.04
FROM ${BASE_IMAGE} FROM ${BASE_IMAGE}
ARG TARGETARCH ARG TARGETARCH
ARG CMAKE_VERSION=3.31.6
ARG XMAKE_VERSION=3.0.9 ARG XMAKE_VERSION=3.0.9
ARG RUST_VERSION=1.92.0 ARG RUST_VERSION=1.92.0
ARG RUSTUP_VERSION=1.28.2 ARG RUSTUP_VERSION=1.28.2
LABEL org.opencontainers.image.title="CrossDesk Linux build environment" \ LABEL org.opencontainers.image.title="CrossDesk Linux build environment" \
org.opencontainers.image.description="Ubuntu 22.04 build environment for CrossDesk" \ org.opencontainers.image.description="Ubuntu 20.04 compatibility build environment for CrossDesk" \
org.opencontainers.image.source="https://github.com/kunkundi/crossdesk" \ org.opencontainers.image.source="https://github.com/kunkundi/crossdesk" \
org.opencontainers.image.licenses="GPL-3.0-only" org.opencontainers.image.licenses="GPL-3.0-only"
@@ -22,6 +23,8 @@ ENV DEBIAN_FRONTEND=noninteractive \
XMAKE_GLOBALDIR=/data \ XMAKE_GLOBALDIR=/data \
XMAKE_ROOT=y \ XMAKE_ROOT=y \
CUDA_PATH=/usr/local/cuda \ CUDA_PATH=/usr/local/cuda \
CC=/usr/bin/gcc-10 \
CXX=/usr/bin/g++-10 \
PATH=/root/.local/bin:/opt/cargo/bin:/usr/local/bin:${PATH} PATH=/root/.local/bin:/opt/cargo/bin:/usr/local/bin:${PATH}
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
@@ -35,13 +38,14 @@ RUN apt-get update \
build-essential \ build-essential \
ca-certificates \ ca-certificates \
ccache \ ccache \
cmake \
curl \ curl \
dbus \ dbus \
dpkg-dev \ dpkg-dev \
file \ file \
flex \ flex \
gettext \ gettext \
gcc-10 \
g++-10 \
git \ git \
libasound2-dev \ libasound2-dev \
libdbus-1-dev \ libdbus-1-dev \
@@ -93,6 +97,22 @@ RUN apt-get update \
zlib1g-dev \ zlib1g-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Slint 1.17 requires CMake 3.21 or newer, while Ubuntu 20.04 provides 3.16.
# PyPI publishes pinned manylinux2014 wheels for both amd64 and arm64.
RUN python3 -m pip install --no-cache-dir "cmake==${CMAKE_VERSION}" \
&& cmake --version | grep -F "cmake version ${CMAKE_VERSION}"
# Ubuntu 20.04 only ships PipeWire 0.2. Install the Ubuntu 22.04-era 0.3.48
# headers as a build-only SDK. CrossDesk loads the host's PipeWire 0.3 runtime
# with dlopen(), so no PipeWire shared library is copied into release packages.
COPY docker/linux-build/install-pipewire-sdk.sh \
/usr/local/bin/install-crossdesk-pipewire-sdk
RUN chmod 0755 /usr/local/bin/install-crossdesk-pipewire-sdk \
&& install-crossdesk-pipewire-sdk \
&& grep -F "#define PW_MICRO 48" \
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/version.h
RUN export GITHUB_ACTIONS=true \ RUN export GITHUB_ACTIONS=true \
&& curl -fsSL https://xmake.io/shget.text \ && curl -fsSL https://xmake.io/shget.text \
| bash -s -- "v${XMAKE_VERSION}" \ | bash -s -- "v${XMAKE_VERSION}" \
@@ -134,7 +154,8 @@ COPY submodules/minirtc/thirdparty ./submodules/minirtc/thirdparty
# CrossDesk's exact dependency graph. Source files are intentionally excluded # CrossDesk's exact dependency graph. Source files are intentionally excluded
# from the Docker context; source-only changes therefore reuse this layer. # from the Docker context; source-only changes therefore reuse this layer.
RUN xmake repo -u --root \ RUN xmake repo -u --root \
&& xmake f -m release --USE_CUDA=true --root -y \ && xmake f -m release --USE_CUDA=true --USE_WAYLAND=true \
--cc=gcc-10 --cxx=g++-10 --root -y \
&& rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \ && rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \
&& mkdir -p /workspace && mkdir -p /workspace
+15 -8
View File
@@ -1,7 +1,9 @@
# CrossDesk Linux 构建镜像 # CrossDesk Linux 构建镜像
这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake 这个目录维护 CrossDesk 专用的 Ubuntu 20.04 兼容构建环境。镜像包含 GCC 10
Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。 CMake 3.31、xmake、Rust、Linux 开发库(包括 D-Bus、DRM,以及仅用于编译的
PipeWire 0.3/SPA 头文件 SDK)以及根据项目
`xmake.lua` 提前编译好的依赖。
## 发布方式 ## 发布方式
@@ -10,14 +12,19 @@ Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。
全部通过后再合并为同一个 multi-arch 镜像: 全部通过后再合并为同一个 multi-arch 镜像:
```text ```text
crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0 crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
``` ```
Docker 会根据运行机器自动选择正确架构。amd64 变体以 Docker 会根据运行机器自动选择正确架构。amd64 变体以
`nvidia/cuda:12.6.3-devel-ubuntu22.04` 为基础;arm64 变体不包含当前构建 `nvidia/cuda:12.6.3-devel-ubuntu20.04` 为基础;arm64 变体不包含当前构建
用不到的 CUDA SDK。两个变体都使用 Ubuntu 22.04/glibc 2.35。 用不到的 CUDA SDK。两个变体都使用 Ubuntu 20.04/glibc 2.31,因此生成的
因此该镜像生成的 Linux 安装包最低支持 Ubuntu 22.04,不再保证兼容 单一 Linux 安装包可运行在 Ubuntu 20.04 及更高版本。
Ubuntu 20.04。
Ubuntu 20.04 仓库只提供 PipeWire 0.2。镜像通过
`install-pipewire-sdk.sh` 安装固定版本的 PipeWire 0.3.48/SPA 头文件,但不
安装或打包 PipeWire 动态库。CrossDesk 在运行时通过 `dlopen()` 使用宿主系统
提供的 PipeWire 0.3Ubuntu 20.04 没有该运行库时仍可使用 X11(以及构建时
启用的 DRM),Ubuntu 22.04 及更高版本则可使用 Wayland 捕获。
每次成功发布还会生成 `sha-<完整提交 SHA>`,用于精确回滚;`latest` 和上面的 每次成功发布还会生成 `sha-<完整提交 SHA>`,用于精确回滚;`latest` 和上面的
工具链版本 tag 只会在两个架构都验证成功后更新。 工具链版本 tag 只会在两个架构都验证成功后更新。
@@ -26,7 +33,7 @@ Ubuntu 20.04。
在仓库的 Actions secrets 中配置: 在仓库的 Actions secrets 中配置:
- `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu22.04` 的 Docker Hub 用户名 - `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu20.04` 的 Docker Hub 用户名
- `DOCKERHUB_TOKEN`:该用户的 Docker Hub access token - `DOCKERHUB_TOKEN`:该用户的 Docker Hub access token
工作流文件进入默认分支后,也可以从 Actions 页面手动运行。为了避免首次发布 工作流文件进入默认分支后,也可以从 Actions 页面手动运行。为了避免首次发布
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
PIPEWIRE_VERSION="0.3.48"
PIPEWIRE_ARCHIVE_SHA256="0d218be86b3d2b548c06259c47ad8d110ee1c09f071e17c4393eeef5c880fa6f"
INSTALL_PREFIX="${1:-/opt/crossdesk-pipewire-sdk}"
IFS='.' read -r PIPEWIRE_VERSION_MAJOR PIPEWIRE_VERSION_MINOR \
PIPEWIRE_VERSION_MICRO <<< "${PIPEWIRE_VERSION}"
if [[ "${INSTALL_PREFIX}" != /* ]]; then
echo "Install prefix must be an absolute path: ${INSTALL_PREFIX}" >&2
exit 2
fi
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/crossdesk-pipewire-sdk.XXXXXX")"
trap 'rm -rf "${WORK_DIR}"' EXIT
ARCHIVE_PATH="${WORK_DIR}/pipewire-${PIPEWIRE_VERSION}.tar.gz"
SOURCE_DIR="${WORK_DIR}/pipewire-${PIPEWIRE_VERSION}"
ARCHIVE_URL="https://gitlab.freedesktop.org/pipewire/pipewire/-/archive/${PIPEWIRE_VERSION}/pipewire-${PIPEWIRE_VERSION}.tar.gz"
curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \
--retry 3 "${ARCHIVE_URL}" -o "${ARCHIVE_PATH}"
echo "${PIPEWIRE_ARCHIVE_SHA256} ${ARCHIVE_PATH}" | sha256sum --check -
tar -xzf "${ARCHIVE_PATH}" -C "${WORK_DIR}"
PIPEWIRE_INCLUDE_DIR="${INSTALL_PREFIX}/include/pipewire-0.3/pipewire"
SPA_INCLUDE_DIR="${INSTALL_PREFIX}/include/spa-0.2/spa"
while IFS= read -r -d '' header_path; do
relative_path="${header_path#${SOURCE_DIR}/src/pipewire/}"
install -D -m 0644 "${header_path}" \
"${PIPEWIRE_INCLUDE_DIR}/${relative_path}"
done < <(find "${SOURCE_DIR}/src/pipewire" -type f -name '*.h' -print0)
while IFS= read -r -d '' header_path; do
relative_path="${header_path#${SOURCE_DIR}/spa/include/spa/}"
install -D -m 0644 "${header_path}" \
"${SPA_INCLUDE_DIR}/${relative_path}"
done < <(find "${SOURCE_DIR}/spa/include/spa" -type f -name '*.h' -print0)
sed \
-e "s/@PIPEWIRE_VERSION_MAJOR@/${PIPEWIRE_VERSION_MAJOR}/g" \
-e "s/@PIPEWIRE_VERSION_MINOR@/${PIPEWIRE_VERSION_MINOR}/g" \
-e "s/@PIPEWIRE_VERSION_MICRO@/${PIPEWIRE_VERSION_MICRO}/g" \
-e 's/@PIPEWIRE_API_VERSION@/"0.3"/g' \
"${SOURCE_DIR}/src/pipewire/version.h.in" \
> "${PIPEWIRE_INCLUDE_DIR}/version.h"
chmod 0644 "${PIPEWIRE_INCLUDE_DIR}/version.h"
test -f "${PIPEWIRE_INCLUDE_DIR}/pipewire.h"
test -f "${PIPEWIRE_INCLUDE_DIR}/version.h"
test -f "${SPA_INCLUDE_DIR}/param/video/format-utils.h"
echo "Installed PipeWire ${PIPEWIRE_VERSION} header SDK under ${INSTALL_PREFIX}"
+49 -7
View File
@@ -2,19 +2,22 @@
set -euo pipefail set -euo pipefail
EXPECTED_ARCH="${EXPECTED_ARCH:-}" EXPECTED_ARCH="${EXPECTED_ARCH:-}"
EXPECTED_CMAKE_VERSION="${EXPECTED_CMAKE_VERSION:-3.31.6}"
EXPECTED_XMAKE_VERSION="${EXPECTED_XMAKE_VERSION:-3.0.9}" EXPECTED_XMAKE_VERSION="${EXPECTED_XMAKE_VERSION:-3.0.9}"
EXPECTED_RUST_VERSION="${EXPECTED_RUST_VERSION:-1.92.0}" EXPECTED_RUST_VERSION="${EXPECTED_RUST_VERSION:-1.92.0}"
BUILD_CC="${CC:-/usr/bin/gcc-10}"
BUILD_CXX="${CXX:-/usr/bin/g++-10}"
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source /etc/os-release source /etc/os-release
if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "22.04" ]]; then if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "20.04" ]]; then
echo "Expected Ubuntu 22.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2 echo "Expected Ubuntu 20.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2
exit 1 exit 1
fi fi
GLIBC_VERSION="$(getconf GNU_LIBC_VERSION | awk '{print $2}')" GLIBC_VERSION="$(getconf GNU_LIBC_VERSION | awk '{print $2}')"
if [[ "${GLIBC_VERSION}" != "2.35" ]]; then if [[ "${GLIBC_VERSION}" != "2.31" ]]; then
echo "Expected glibc 2.35, found ${GLIBC_VERSION}" >&2 echo "Expected glibc 2.31, found ${GLIBC_VERSION}" >&2
exit 1 exit 1
fi fi
@@ -28,9 +31,48 @@ XMAKE_VERSION_OUTPUT="$(xmake --version 2>&1)"
grep -F "v${EXPECTED_XMAKE_VERSION}" <<<"${XMAKE_VERSION_OUTPUT}" >/dev/null grep -F "v${EXPECTED_XMAKE_VERSION}" <<<"${XMAKE_VERSION_OUTPUT}" >/dev/null
rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null
cargo --version >/dev/null cargo --version >/dev/null
cmake --version >/dev/null cmake --version | grep -F "cmake version ${EXPECTED_CMAKE_VERSION}" >/dev/null
g++ --version >/dev/null "${BUILD_CC}" --version | grep -F 'gcc-10' >/dev/null
pkg-config --exists xft "${BUILD_CXX}" --version | grep -F 'g++-10' >/dev/null
printf '%s\n' \
'#include <concepts>' \
'#include <span>' \
'template <typename T> concept TestConcept = true;' \
'int main() {' \
' int value = 0;' \
' std::span<int> values(&value, 1);' \
' static_assert(TestConcept<int>);' \
' return values.empty();' \
'}' \
| "${BUILD_CXX}" -std=c++20 -fsyntax-only -x c++ -
for pkg_config_package in \
dbus-1 \
libdrm \
xft; do
if ! pkg-config --exists "${pkg_config_package}"; then
echo "Required pkg-config package is missing: ${pkg_config_package}" >&2
exit 1
fi
done
for sdk_header in \
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h \
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/version.h \
/opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h; do
if [[ ! -f "${sdk_header}" ]]; then
echo "Required PipeWire build SDK header is missing: ${sdk_header}" >&2
exit 1
fi
done
printf '%s\n' \
'#include <pipewire/pipewire.h>' \
'#include <spa/param/video/format-utils.h>' \
'static_assert(PW_CHECK_VERSION(0, 3, 48));' \
'int main() { return 0; }' \
| "${BUILD_CXX}" -std=c++17 -fsyntax-only -x c++ \
-I/opt/crossdesk-pipewire-sdk/include/pipewire-0.3 \
-I/opt/crossdesk-pipewire-sdk/include/spa-0.2 -
for package_path in \ for package_path in \
"s/slint" \ "s/slint" \
+1 -1
View File
@@ -7,7 +7,7 @@ if [[ $# -lt 1 || $# -gt 2 ]]; then
fi fi
INPUT_PATH="$1" INPUT_PATH="$1"
MAXIMUM_VERSION="${2:-2.35}" MAXIMUM_VERSION="${2:-2.31}"
verify_elf() { verify_elf() {
local elf_file="$1" local elf_file="$1"
+10 -1
View File
@@ -104,6 +104,15 @@ OUTPUT_FILE="$PROJECT_ROOT/${PKG_NAME}-linux-${DEBIAN_ARCH}-${APP_VERSION}.deb"
install -d "$DEBIAN_DIR" "$BIN_DIR" "$PRIVATE_DIR" "$DESKTOP_DIR" install -d "$DEBIAN_DIR" "$BIN_DIR" "$PRIVATE_DIR" "$DESKTOP_DIR"
install -m 0755 "$BUILD_BINARY" "$PRIVATE_DIR/crossdesk-bin" install -m 0755 "$BUILD_BINARY" "$PRIVATE_DIR/crossdesk-bin"
# PipeWire is deliberately optional. The Wayland capturer resolves PipeWire
# 0.3 with dlopen() so the same package can run on Ubuntu 20.04 without the
# runtime while using the host runtime on newer Ubuntu releases.
if readelf -d "$BUILD_BINARY" \
| grep -qE 'Shared library: \[libpipewire-0\.3\.so'; then
echo "CrossDesk must not link directly to the optional PipeWire runtime" >&2
exit 1
fi
# Slint is intentionally built as a shared library. Keep that runtime private # Slint is intentionally built as a shared library. Keep that runtime private
# to CrossDesk instead of relying on a non-existent distribution package. # to CrossDesk instead of relying on a non-existent distribution package.
SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \ SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \
@@ -147,7 +156,7 @@ Version: $DEB_VERSION
Architecture: $DEBIAN_ARCH Architecture: $DEBIAN_ARCH
Maintainer: $MAINTAINER Maintainer: $MAINTAINER
Description: $DESCRIPTION Description: $DESCRIPTION
Depends: libc6 (>= 2.35), libstdc++6 (>= 9), libx11-6, libxext6, Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0, libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0,
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <app-bundle> <expected-architecture>" >&2
exit 2
fi
APP_BUNDLE="$1"
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
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "Required bundling command is missing: $command_name" >&2
exit 1
fi
done
if [[ ! -x "$APP_EXECUTABLE" ]]; then
echo "Missing app executable: $APP_EXECUTABLE" >&2
exit 1
fi
if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "App executable does not contain the expected architecture: $EXPECTED_ARCH" >&2
lipo -archs "$APP_EXECUTABLE" >&2 || true
exit 1
fi
SLINT_REFERENCE="$(otool -L "$APP_EXECUTABLE" | awk \
'$1 ~ /(^|\/)libslint_cpp[^\/[:space:]]*\.dylib$/ { print $1; exit }')"
if [[ -z "$SLINT_REFERENCE" ]]; then
echo "The app executable does not reference the Slint runtime" >&2
exit 1
fi
SLINT_NAME="${SLINT_REFERENCE##*/}"
SLINT_CURRENT_VERSION="$(otool -L "$APP_EXECUTABLE" | awk -v dependency="$SLINT_REFERENCE" '
$1 == dependency {
for (i = 1; i <= NF; ++i) {
if ($i == "current") {
version = $(i + 2)
gsub(/[),]/, "", version)
print version
exit
}
}
}')"
slint_runtime_version() {
local library="$1"
otool -L "$library" | awk -v name="$SLINT_NAME" '
index($1, "/" name) || $1 == "@rpath/" name {
for (i = 1; i <= NF; ++i) {
if ($i == "current") {
version = $(i + 2)
gsub(/[),]/, "", version)
print version
exit
}
}
}'
}
find_slint_runtime() {
local candidate=""
local candidate_version=""
local package_root=""
local package_roots=()
if [[ "$SLINT_REFERENCE" == /* && -f "$SLINT_REFERENCE" ]]; then
if lipo "$SLINT_REFERENCE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "$SLINT_REFERENCE"
return 0
fi
fi
if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
package_roots+=("$XMAKE_GLOBALDIR/.xmake/packages")
fi
package_roots+=("$HOME/.xmake/packages" "/data/.xmake/packages")
for package_root in "${package_roots[@]}"; do
[[ -d "$package_root/s/slint" ]] || continue
while IFS= read -r candidate; do
if ! lipo "$candidate" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
continue
fi
candidate_version="$(slint_runtime_version "$candidate")"
if [[ -n "$SLINT_CURRENT_VERSION" && "$candidate_version" != "$SLINT_CURRENT_VERSION" ]]; then
continue
fi
echo "$candidate"
return 0
done < <(find "$package_root/s/slint" -type f -path "*/lib/$SLINT_NAME" -print 2>/dev/null)
done
return 1
}
SLINT_LIBRARY="$(find_slint_runtime || true)"
if [[ -z "$SLINT_LIBRARY" ]]; then
echo "Unable to locate $SLINT_NAME for $EXPECTED_ARCH (current version $SLINT_CURRENT_VERSION)" >&2
exit 1
fi
install -d "$FRAMEWORKS_DIR"
install -m 0755 "$SLINT_LIBRARY" "$FRAMEWORKS_DIR/$SLINT_NAME"
install_name_tool -id "@rpath/$SLINT_NAME" "$FRAMEWORKS_DIR/$SLINT_NAME"
if [[ "$SLINT_REFERENCE" != "@rpath/$SLINT_NAME" ]]; then
install_name_tool -change "$SLINT_REFERENCE" "@rpath/$SLINT_NAME" "$APP_EXECUTABLE"
fi
if ! otool -l "$APP_EXECUTABLE" | awk '
$1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next }
in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 }
END { exit(found ? 0 : 1) }'; then
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP_EXECUTABLE"
fi
# install_name_tool invalidates 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"
echo "Bundled Slint runtime: $SLINT_LIBRARY"
+8 -3
View File
@@ -1,14 +1,16 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -euo pipefail
APP_NAME="crossdesk" APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk" APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk" EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
PLATFORM="macos" PLATFORM="macos"
ARCH="arm64" ARCH="arm64"
BINARY_ARCH="arm64"
IDENTIFIER="cn.crossdesk.app" IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns" ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12" MACOS_MIN_VERSION="14.0"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
normalize_app_version() { normalize_app_version() {
local input="$1" local input="$1"
@@ -95,6 +97,9 @@ EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete find "${APP_BUNDLE}" -name '._*' -delete
"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
echo ".app created successfully." echo ".app created successfully."
mkdir -p build_pkg_scripts mkdir -p build_pkg_scripts
+8 -3
View File
@@ -1,14 +1,16 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -euo pipefail
APP_NAME="crossdesk" APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk" APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk" EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
PLATFORM="macos" PLATFORM="macos"
ARCH="x64" ARCH="x64"
BINARY_ARCH="x86_64"
IDENTIFIER="cn.crossdesk.app" IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns" ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12" MACOS_MIN_VERSION="14.0"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
normalize_app_version() { normalize_app_version() {
local input="$1" local input="$1"
@@ -95,6 +97,9 @@ EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete find "${APP_BUNDLE}" -name '._*' -delete
"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
echo ".app created successfully." echo ".app created successfully."
mkdir -p build_pkg_scripts mkdir -p build_pkg_scripts
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 2 || $# -gt 3 ]]; then
echo "Usage: $0 <app-bundle> <expected-architecture> [expected-sdk-version]" >&2
exit 2
fi
APP_BUNDLE="$1"
EXPECTED_ARCH="$2"
EXPECTED_SDK_VERSION="${3:-}"
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
INFO_PLIST="$APP_BUNDLE/Contents/Info.plist"
if [[ ! -x "$APP_EXECUTABLE" ]]; then
echo "Missing app executable: $APP_EXECUTABLE" >&2
exit 1
fi
if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "Unexpected app architecture; expected $EXPECTED_ARCH" >&2
lipo -archs "$APP_EXECUTABLE" >&2 || true
exit 1
fi
if ! otool -l "$APP_EXECUTABLE" | awk '
$1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next }
in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 }
END { exit(found ? 0 : 1) }'; then
echo "Missing app rpath: @executable_path/../Frameworks" >&2
exit 1
fi
verify_dependencies() {
local macho="$1"
local dependency=""
local relative_path=""
while IFS= read -r dependency; do
case "$dependency" in
/System/Library/*|/usr/lib/*)
;;
@rpath/*)
relative_path="${dependency#@rpath/}"
if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then
echo "Unbundled dependency in $macho: $dependency" >&2
return 1
fi
;;
@executable_path/../Frameworks/*)
relative_path="${dependency#@executable_path/../Frameworks/}"
if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then
echo "Unbundled dependency in $macho: $dependency" >&2
return 1
fi
;;
@loader_path/*)
relative_path="${dependency#@loader_path/}"
if [[ ! -f "$(dirname "$macho")/$relative_path" ]]; then
echo "Unbundled dependency in $macho: $dependency" >&2
return 1
fi
;;
*)
echo "External non-system dependency in $macho: $dependency" >&2
return 1
;;
esac
done < <(otool -L "$macho" | tail -n +2 | awk '{ print $1 }')
}
verify_dependencies "$APP_EXECUTABLE"
SLINT_LIBRARY="$(find "$FRAMEWORKS_DIR" -maxdepth 1 -type f -name 'libslint_cpp*.dylib' -print -quit 2>/dev/null || true)"
if [[ -z "$SLINT_LIBRARY" ]]; then
echo "The packaged app does not contain libslint_cpp.dylib" >&2
exit 1
fi
if ! lipo "$SLINT_LIBRARY" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "Unexpected Slint runtime architecture; expected $EXPECTED_ARCH" >&2
lipo -archs "$SLINT_LIBRARY" >&2 || true
exit 1
fi
verify_dependencies "$SLINT_LIBRARY"
PLIST_MIN_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' "$INFO_PLIST")"
BINARY_MIN_VERSION="$(otool -l "$APP_EXECUTABLE" | awk '
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); version_min = ($2 == "LC_VERSION_MIN_MACOSX"); next }
build_version && $1 == "minos" { print $2; exit }
version_min && $1 == "version" { print $2; exit }')"
if [[ -z "$BINARY_MIN_VERSION" || "$PLIST_MIN_VERSION" != "$BINARY_MIN_VERSION" ]]; then
echo "Minimum macOS version mismatch: plist=$PLIST_MIN_VERSION binary=${BINARY_MIN_VERSION:-unknown}" >&2
exit 1
fi
if [[ -n "$EXPECTED_SDK_VERSION" ]]; then
verify_sdk_version() {
local macho="$1"
local actual_sdk=""
actual_sdk="$(otool -l "$macho" | awk '
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); next }
build_version && $1 == "sdk" { print $2; exit }')"
if [[ "$actual_sdk" != "$EXPECTED_SDK_VERSION" ]]; then
echo "Unexpected macOS SDK in $macho: expected $EXPECTED_SDK_VERSION, got ${actual_sdk:-unknown}" >&2
return 1
fi
}
verify_sdk_version "$APP_EXECUTABLE"
verify_sdk_version "$SLINT_LIBRARY"
fi
codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"
echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)"
@@ -1,5 +1,9 @@
#include "keyboard_capturer.h" #include "keyboard_capturer.h"
#include <X11/Xlib.h>
#include <X11/extensions/XTest.h>
#include <X11/keysym.h>
#include <errno.h> #include <errno.h>
#include <poll.h> #include <poll.h>
@@ -7,10 +7,6 @@
#ifndef _KEYBOARD_CAPTURER_H_ #ifndef _KEYBOARD_CAPTURER_H_
#define _KEYBOARD_CAPTURER_H_ #define _KEYBOARD_CAPTURER_H_
#include <X11/Xlib.h>
#include <X11/extensions/XTest.h>
#include <X11/keysym.h>
#include <atomic> #include <atomic>
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
@@ -21,6 +17,7 @@
struct DBusConnection; struct DBusConnection;
struct DBusMessageIter; struct DBusMessageIter;
struct _XDisplay;
namespace crossdesk { namespace crossdesk {
@@ -48,8 +45,8 @@ class KeyboardCapturer : public DeviceController {
append_args); append_args);
private: private:
Display* display_; _XDisplay* display_;
Window root_; unsigned long root_;
std::atomic<bool> running_; std::atomic<bool> running_;
std::thread event_thread_; std::thread event_thread_;
bool use_wayland_portal_ = false; bool use_wayland_portal_ = false;
@@ -5,6 +5,9 @@
#include <map> #include <map>
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER #if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <dbus/dbus.h> #include <dbus/dbus.h>
#endif #endif
@@ -1,5 +1,6 @@
#include "keyboard_capturer.h" #include "keyboard_capturer.h"
#include <cstdint>
#include <unordered_map> #include <unordered_map>
#include "keyboard_converter.h" #include "keyboard_converter.h"
@@ -10,6 +11,7 @@ namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr; static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr; static void* g_user_ptr = nullptr;
static std::unordered_map<int, int> g_unmapped_keycode_to_vk; static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
constexpr int64_t kCrossDeskInjectedKeyboardEvent = 0x43524f5353444553;
static int VkCodeFromUnicode(UniChar ch) { static int VkCodeFromUnicode(UniChar ch) {
if (ch >= 'a' && ch <= 'z') { if (ch >= 'a' && ch <= 'z') {
@@ -103,6 +105,10 @@ static int ResolveVkCodeFromMacEvent(CGEventRef event, CGKeyCode key_code,
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
CGEventRef event, void* userInfo) { CGEventRef event, void* userInfo) {
(void)proxy; (void)proxy;
if (CGEventGetIntegerValueField(event, kCGEventSourceUserData) ==
kCrossDeskInjectedKeyboardEvent) {
return event;
}
if (!g_on_key_action) { if (!g_on_key_action) {
return event; return event;
} }
@@ -302,6 +308,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
} }
CGEventSetFlags(event, ToCGEventFlags(injected_flags)); CGEventSetFlags(event, ToCGEventFlags(injected_flags));
CGEventSetIntegerValueField(event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, event); CGEventPost(kCGHIDEventTap, event);
CFRelease(event); CFRelease(event);
@@ -314,6 +322,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release"); LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
return -1; return -1;
} }
CGEventSetIntegerValueField(fn_release_event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, fn_release_event); CGEventPost(kCGHIDEventTap, fn_release_event);
CFRelease(fn_release_event); CFRelease(fn_release_event);
} }
@@ -49,6 +49,9 @@ static bool PreferSideSpecificVkInjection(int key_code) {
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION && g_on_key_action) { if (nCode == HC_ACTION && g_on_key_action) {
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam); KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
if ((kbData->flags & LLKHF_INJECTED) != 0) {
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
const int key_code = NormalizeModifierVkCode(kbData); const int key_code = NormalizeModifierVkCode(kbData);
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) { if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
@@ -1,6 +1,9 @@
#include "mouse_controller.h" #include "mouse_controller.h"
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/XTest.h> #include <X11/extensions/XTest.h>
#include <unistd.h>
#include "platform.h" #include "platform.h"
#include "rd_log.h" #include "rd_log.h"
@@ -7,10 +7,6 @@
#ifndef _MOUSE_CONTROLLER_H_ #ifndef _MOUSE_CONTROLLER_H_
#define _MOUSE_CONTROLLER_H_ #define _MOUSE_CONTROLLER_H_
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <unistd.h>
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
#include <string> #include <string>
@@ -20,6 +16,7 @@
struct DBusConnection; struct DBusConnection;
struct DBusMessageIter; struct DBusMessageIter;
struct _XDisplay;
namespace crossdesk { namespace crossdesk {
@@ -53,8 +50,8 @@ class MouseController : public DeviceController {
enum class WaylandAbsoluteMode { kUnknown, kPixels, kNormalized, kDisabled }; enum class WaylandAbsoluteMode { kUnknown, kPixels, kNormalized, kDisabled };
Display* display_ = nullptr; _XDisplay* display_ = nullptr;
Window root_ = 0; unsigned long root_ = 0;
std::vector<DisplayInfo> display_info_list_; std::vector<DisplayInfo> display_info_list_;
int screen_width_ = 0; int screen_width_ = 0;
int screen_height_ = 0; int screen_height_ = 0;
+7 -3
View File
@@ -1,6 +1,7 @@
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_ #ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
#define CROSSDESK_GUI_APPLICATION_STATE_H_ #define CROSSDESK_GUI_APPLICATION_STATE_H_
#include <atomic>
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
@@ -64,7 +65,7 @@ struct InteractionState {
bool start_keyboard_capturer_ = false; bool start_keyboard_capturer_ = false;
bool show_cursor_ = false; bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false; bool keyboard_capturer_is_started_ = false;
bool keyboard_capturer_uses_sdl_events_ = false; bool keyboard_capturer_uses_window_events_ = false;
bool foucs_on_main_window_ = false; bool foucs_on_main_window_ = false;
bool focus_on_stream_window_ = false; bool focus_on_stream_window_ = false;
bool audio_capture_ = false; bool audio_capture_ = false;
@@ -119,8 +120,11 @@ struct StreamWindowState {
}; };
struct ServerWindowState { struct ServerWindowState {
bool need_to_create_server_window_ = false; // Transport callbacks run outside the UI thread. Keep the two lifecycle
bool need_to_destroy_server_window_ = false; // requests atomic so a disconnect cannot be lost while the Slint timer is
// deciding whether to create or destroy the controlled-side window.
std::atomic<bool> need_to_create_server_window_{false};
std::atomic<bool> need_to_destroy_server_window_{false};
bool server_window_created_ = false; bool server_window_created_ = false;
bool server_window_inited_ = false; bool server_window_inited_ = false;
int server_window_width_default_ = 250; int server_window_width_default_ = 250;
File diff suppressed because it is too large Load Diff
+8
View File
@@ -35,7 +35,12 @@ private:
void SyncConnectionDialog(); void SyncConnectionDialog();
void SyncPlatformDialogs(); void SyncPlatformDialogs();
void SyncStreamWindow(); void SyncStreamWindow();
void SyncStreamKeyboardFocus();
void SetStreamKeyboardFocus(bool focused);
void SyncServerWindow(); void SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
void SyncXWaylandWindowActivation();
#endif
void UpdateLocalization(); void UpdateLocalization();
void ResetSettingsUi(); void ResetSettingsUi();
void SaveSettingsFromUi(); void SaveSettingsFromUi();
@@ -59,6 +64,9 @@ private:
bool OpenUrl(const std::string &url); bool OpenUrl(const std::string &url);
std::unique_ptr<SlintUi> ui_; std::unique_ptr<SlintUi> ui_;
#if defined(__linux__) && !defined(__APPLE__)
bool use_xwayland_gui_ = false;
#endif
}; };
} // namespace crossdesk } // namespace crossdesk
+2 -1
View File
@@ -200,7 +200,8 @@ void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP: case SDL_EVENT_KEY_UP:
if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ && if (keyboard_capturer_is_started_ &&
keyboard_capturer_uses_window_events_ &&
focus_on_stream_window_ && stream_window_ && focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == event.key.windowID) { SDL_GetWindowID(stream_window_) == event.key.windowID) {
ProcessKeyboardEvent(event); ProcessKeyboardEvent(event);
+2 -4
View File
@@ -220,10 +220,8 @@ int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
return -1; return -1;
} }
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) { // SDL represents key auto-repeat as additional key-down events. Forward
return 0; // them so the fallback path has the same long-press behavior as native hooks.
}
const int key_code = TranslateSdlKeyboardEventToVk(event.key); const int key_code = TranslateSdlKeyboardEventToVk(event.key);
if (key_code < 0) { if (key_code < 0) {
return 0; return 0;
@@ -179,6 +179,7 @@ struct TranslationRow {
X(notification, u8"通知", "Notification", u8"Уведомление") \ X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \ X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \ u8"Доступна новая версия") \
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
X(version, u8"版本", "Version", u8"Версия") \ X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \ X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
X(access_website, u8"访问官网: ", \ X(access_website, u8"访问官网: ", \
@@ -228,27 +228,27 @@ int SessionDeviceManager::StopMouseController() {
} }
int SessionDeviceManager::StartKeyboardCapturer() { int SessionDeviceManager::StartKeyboardCapturer() {
owner_.keyboard_capturer_uses_sdl_events_ = false; owner_.keyboard_capturer_uses_window_events_ = false;
#ifdef __APPLE__ #ifdef __APPLE__
if (!owner_.EnsureMacAccessibilityPermission()) { if (!owner_.EnsureMacAccessibilityPermission()) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
return 0; return 0;
} }
#endif #endif
#if defined(__linux__) && !defined(__APPLE__) #if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) { if (IsWaylandSession()) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
LOG_INFO("Start keyboard capturer with SDL Wayland backend"); LOG_INFO("Start keyboard capturer with Slint Wayland backend");
return 0; return 0;
} }
#endif #endif
if (!keyboard_capturer_) { if (!keyboard_capturer_) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN( LOG_WARN(
"keyboard capturer is nullptr, falling back to SDL keyboard events"); "keyboard capturer is nullptr, falling back to Slint keyboard events");
return 0; return 0;
} }
@@ -263,9 +263,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
}, },
&owner_); &owner_);
if (hook_ret != 0) { if (hook_ret != 0) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN( LOG_WARN(
"Start keyboard capturer failed, falling back to SDL keyboard events"); "Start keyboard capturer failed, falling back to Slint keyboard events");
} else { } else {
LOG_INFO("Start keyboard capturer with native hook"); LOG_INFO("Start keyboard capturer with native hook");
} }
@@ -273,9 +273,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
} }
int SessionDeviceManager::StopKeyboardCapturer() { int SessionDeviceManager::StopKeyboardCapturer() {
if (owner_.keyboard_capturer_uses_sdl_events_) { if (owner_.keyboard_capturer_uses_window_events_) {
owner_.keyboard_capturer_uses_sdl_events_ = false; owner_.keyboard_capturer_uses_window_events_ = false;
LOG_INFO("Stop keyboard capturer with SDL keyboard backend"); LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
return 0; return 0;
} }
+4
View File
@@ -16,6 +16,10 @@ bool HideDisabledMainWindowZoomButton();
// fullscreen action with an immediate, single-window fullscreen transition. // fullscreen action with an immediate, single-window fullscreen transition.
bool ConfigureStreamWindowLiveResize(); bool ConfigureStreamWindowLiveResize();
// Returns whether the configured stream window is currently the active AppKit
// key window.
bool IsStreamWindowActive();
// Enters or leaves the stream window's animation-free fullscreen mode. // Enters or leaves the stream window's animation-free fullscreen mode.
bool SetStreamWindowFullscreen(bool fullscreen); bool SetStreamWindowFullscreen(bool fullscreen);
bool IsStreamWindowFullscreen(); bool IsStreamWindowFullscreen();
+7
View File
@@ -146,6 +146,13 @@ bool ConfigureStreamWindowLiveResize() {
} }
} }
bool IsStreamWindowActive() {
@autoreleasepool {
return stream_window != nil && [NSApp isActive] &&
[stream_window isKeyWindow];
}
}
bool SetStreamWindowFullscreen(bool fullscreen) { bool SetStreamWindowFullscreen(bool fullscreen) {
@autoreleasepool { @autoreleasepool {
if (stream_window == nil || stream_fullscreen == fullscreen) { if (stream_window == nil || stream_fullscreen == fullscreen) {
+73 -65
View File
@@ -1,5 +1,3 @@
#include "runtime/gui_runtime.h"
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <cstring> #include <cstring>
@@ -11,17 +9,14 @@
#include <vector> #include <vector>
#include "localization.h" #include "localization.h"
#include "platform.h"
#include "rd_log.h" #include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk { namespace crossdesk {
namespace { namespace {
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5); constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
constexpr auto kConnectionAttemptTimeout = std::chrono::seconds(20);
bool IsConnectionAttemptPending(ConnectionStatus status) {
return status == ConnectionStatus::Connecting ||
status == ConnectionStatus::Gathering;
}
} // namespace } // namespace
void GuiRuntime::HandleConnectionStatusChange() { void GuiRuntime::HandleConnectionStatusChange() {
@@ -31,7 +26,7 @@ void GuiRuntime::HandleConnectionStatusChange() {
j["type"] = "recent_connections_presence"; j["type"] = "recent_connections_presence";
j["user_id"] = client_id_; j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array(); j["devices"] = nlohmann::json::array();
for (const auto &id : recent_connection_ids_) { for (const auto& id : recent_connection_ids_) {
std::string pure_id = id; std::string pure_id = id;
size_t pos_y = pure_id.find('Y'); size_t pos_y = pure_id.find('Y');
size_t pos_n = pure_id.find('N'); size_t pos_n = pure_id.find('N');
@@ -95,7 +90,7 @@ void GuiRuntime::HandlePendingPresenceProbe() {
show_offline_warning_window_ = true; show_offline_warning_window_ = true;
} }
void GuiRuntime::HandleConnectionTimeouts() { void GuiRuntime::HandlePresenceProbeTimeout() {
const auto now = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now();
bool presence_probe_timed_out = false; bool presence_probe_timed_out = false;
@@ -121,48 +116,67 @@ void GuiRuntime::HandleConnectionTimeouts() {
show_offline_warning_window_ = true; show_offline_warning_window_ = true;
LOG_WARN("Presence probe timed out for [{}]", presence_remote_id); LOG_WARN("Presence probe timed out for [{}]", presence_remote_id);
} }
bool rejoin_state_changed = false;
for (auto &[_, props] : remote_sessions_) {
if (!props || !props->connection_attempt_active_.load()) {
continue;
}
const ConnectionStatus status = props->connection_status_.load();
if (!IsConnectionAttemptPending(status)) {
props->connection_attempt_active_.store(false);
continue;
}
if (now - props->connection_attempt_started_at_ <
kConnectionAttemptTimeout) {
continue;
}
LOG_WARN("Connection to [{}] timed out, status={}", props->remote_id_,
static_cast<int>(status));
props->connection_attempt_active_.store(false);
props->connection_established_ = false;
props->rejoin_ = false;
props->connection_status_.store(ConnectionStatus::Failed);
focused_remote_id_ = props->remote_id_;
show_connection_status_window_ = true;
rejoin_state_changed = true;
}
if (rejoin_state_changed) {
need_to_rejoin_ = false;
for (const auto &[_, props] : remote_sessions_) {
if (props && props->rejoin_) {
need_to_rejoin_ = true;
break;
}
}
}
} }
int GuiRuntime::RequestSingleDevicePresence(const std::string &remote_id, void GuiRuntime::HandleServerControllerDisconnected(
const char *password, const std::string& remote_id, const char* reason) {
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
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);
connection_host_names_.erase(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;
}
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;
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
// Keep Wayland capture session warm to avoid black screen on subsequent
// reconnects.
start_screen_capturer_ = true;
LOG_INFO(
"Keeping Wayland screen capturer running after disconnect to "
"preserve reconnect stability");
} else {
start_screen_capturer_ = false;
}
#else
start_screen_capturer_ = false;
#endif
start_speaker_capturer_ = false;
start_mouse_controller_ = false;
start_keyboard_capturer_ = false;
remote_client_id_.clear();
if (audio_capture_) {
devices_.StopSpeakerCapturer();
audio_capture_ = false;
}
devices_.ResetToInitialDisplay();
}
int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
const char* password,
bool remember_password) { bool remember_password) {
if (!signal_connected_ || !peer_) { if (!signal_connected_ || !peer_) {
return -1; return -1;
@@ -221,7 +235,7 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
std::thread save_thread([buffer_copy, video_width, video_height, remote_id, std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
remote_host_name, password, remote_host_name, password,
thumbnail = thumbnail_]() { thumbnail = thumbnail_]() {
thumbnail->SaveToThumbnail((char *)buffer_copy.data(), video_width, thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width,
video_height, remote_id, remote_host_name, video_height, remote_id, remote_host_name,
password); password);
}); });
@@ -255,7 +269,7 @@ void GuiRuntime::CloseAllRemoteSessions() {
{ {
// std::shared_lock lock(remote_sessions_mutex_); // std::shared_lock lock(remote_sessions_mutex_);
for (auto &it : remote_sessions_) { for (auto& it : remote_sessions_) {
auto props = it.second; auto props = it.second;
CloseRemoteSession(props); CloseRemoteSession(props);
} }
@@ -279,7 +293,7 @@ void GuiRuntime::WaitForThumbnailSaveTasks() {
return; return;
} }
for (auto &thread : threads_to_join) { for (auto& thread : threads_to_join) {
if (thread.joinable()) { if (thread.joinable()) {
thread.join(); thread.join();
} }
@@ -300,9 +314,8 @@ void GuiRuntime::ResetRemoteSessionResources(
} }
} }
std::shared_ptr<GuiRuntime::RemoteSession> std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
GuiRuntime::FindRemoteSession( const std::string& remote_id) {
const std::string &remote_id) {
if (remote_id.empty()) { if (remote_id.empty()) {
return nullptr; return nullptr;
} }
@@ -315,8 +328,7 @@ GuiRuntime::FindRemoteSession(
return it->second; return it->second;
} }
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
bool remember_password, bool bypass_presence_check) { bool remember_password, bool bypass_presence_check) {
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) { if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
int ret = int ret =
@@ -336,19 +348,17 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
focused_remote_id_ = remote_id; focused_remote_id_ = remote_id;
// std::shared_lock shared_lock(remote_sessions_mutex_); // std::shared_lock shared_lock(remote_sessions_mutex_);
bool exists = bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
(remote_sessions_.find(remote_id) != remote_sessions_.end());
// shared_lock.unlock(); // shared_lock.unlock();
if (!exists) { if (!exists) {
PeerPtr *peer_to_init = nullptr; PeerPtr* peer_to_init = nullptr;
std::string local_id; 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()) { if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
remote_sessions_[remote_id] = remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
std::make_shared<RemoteSession>();
auto props = remote_sessions_[remote_id]; auto props = remote_sessions_[remote_id];
props->local_id_ = "C-" + std::string(client_id_); props->local_id_ = "C-" + std::string(client_id_);
props->remote_id_ = remote_id; props->remote_id_ = remote_id;
@@ -371,7 +381,7 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
return -1; return -1;
} }
for (const auto &display_info : devices_.display_info_list()) { for (const auto& display_info : devices_.display_info_list()) {
AddVideoStream(props->peer_, display_info.name.c_str()); AddVideoStream(props->peer_, display_info.name.c_str());
} }
AddAudioStream(props->peer_, props->audio_label_.c_str()); AddAudioStream(props->peer_, props->audio_label_.c_str());
@@ -402,8 +412,6 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
auto props = remote_sessions_[remote_id]; auto props = remote_sessions_[remote_id];
if (!props->connection_established_) { if (!props->connection_established_) {
props->connection_status_.store(ConnectionStatus::Connecting); props->connection_status_.store(ConnectionStatus::Connecting);
props->connection_attempt_active_.store(true);
props->connection_attempt_started_at_ = std::chrono::steady_clock::now();
show_connection_status_window_ = true; show_connection_status_window_ = true;
props->remember_password_ = remember_password; props->remember_password_ = remember_password;
+18 -18
View File
@@ -8,16 +8,16 @@
#include "features/devices/session_device_manager.h" #include "features/devices/session_device_manager.h"
#include "features/file_transfer/file_transfer_manager.h" #include "features/file_transfer/file_transfer_manager.h"
#include "features/input/keyboard_controller.h" #include "features/input/keyboard_controller.h"
#include "features/settings/settings_manager.h"
#include "runtime/gui_state.h" #include "runtime/gui_state.h"
#include "runtime/peer_event_handler.h" #include "runtime/peer_event_handler.h"
#include "features/settings/settings_manager.h"
namespace crossdesk { namespace crossdesk {
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session // 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, ImGui view, or transport callback methods.
class GuiRuntime : protected gui_detail::GuiState { class GuiRuntime : protected gui_detail::GuiState {
protected: protected:
using FileTransferState = gui_detail::FileTransferState; using FileTransferState = gui_detail::FileTransferState;
using RemoteSession = gui_detail::RemoteSession; using RemoteSession = gui_detail::RemoteSession;
@@ -32,35 +32,35 @@ protected:
GuiRuntime(); GuiRuntime();
~GuiRuntime(); ~GuiRuntime();
static void SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len); static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
static void SdlCaptureAudioOut(void *userdata, Uint8 *stream, int len); static void SdlCaptureAudioOut(void* userdata, Uint8* stream, int len);
int CreateConnectionPeer(); int CreateConnectionPeer();
int ConnectTo(const std::string &remote_id, const char *password, int ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check = false); bool remember_password, bool bypass_presence_check = false);
int RequestSingleDevicePresence(const std::string &remote_id, int RequestSingleDevicePresence(const std::string& remote_id,
const char *password, bool remember_password); const char* password, bool remember_password);
void UpdateLabels(); void UpdateLabels();
void HandleRecentConnections(); void HandleRecentConnections();
void HandleConnectionStatusChange(); void HandleConnectionStatusChange();
void HandlePendingPresenceProbe(); void HandlePendingPresenceProbe();
void HandleConnectionTimeouts(); void HandlePresenceProbeTimeout();
void HandleServerControllerDisconnected(const std::string& remote_id,
const char* reason);
void HandleWindowsServiceIntegration(); void HandleWindowsServiceIntegration();
void CloseRemoteSession(std::shared_ptr<RemoteSession> props); void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions(); void CloseAllRemoteSessions();
void ResetRemoteSessionResources( void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
std::shared_ptr<RemoteSession> props);
void WaitForThumbnailSaveTasks(); void WaitForThumbnailSaveTasks();
std::shared_ptr<RemoteSession> std::shared_ptr<RemoteSession> FindRemoteSession(
FindRemoteSession(const std::string &remote_id); const std::string& remote_id);
void ResetRemoteServiceStatus(RemoteSession &props); void ResetRemoteServiceStatus(RemoteSession& props);
void ApplyRemoteServiceStatus(RemoteSession &props, void ApplyRemoteServiceStatus(RemoteSession& props,
const ServiceStatus &status); const ServiceStatus& status);
RemoteUnlockState RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
GetRemoteUnlockState(const RemoteSession &props) const;
#if _WIN32 #if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas); void ResetLocalWindowsServiceState(bool clear_pending_sas);
#endif #endif
@@ -82,7 +82,7 @@ protected:
KeyboardController keyboard_; KeyboardController keyboard_;
PeerEventHandler peer_events_; PeerEventHandler peer_events_;
private: private:
friend class ClipboardController; friend class ClipboardController;
friend class SessionDeviceManager; friend class SessionDeviceManager;
friend class FileTransferManager; friend class FileTransferManager;
+56 -107
View File
@@ -26,12 +26,12 @@
namespace crossdesk { namespace crossdesk {
PeerEventHandler::PeerEventHandler(GuiRuntime &owner) : owner_(owner) {} PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
void PeerEventHandler::OnSignalMessage(const char *message, size_t size, void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
void *user_data) { void* user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data); auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime || !message || size == 0) { if (!runtime || !message || size == 0) {
return; return;
} }
@@ -43,7 +43,7 @@ void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
std::string type = j["type"].get<std::string>(); std::string type = j["type"].get<std::string>();
if (type == "presence") { if (type == "presence") {
if (j.contains("devices") && j["devices"].is_array()) { if (j.contains("devices") && j["devices"].is_array()) {
for (auto &dev : j["devices"]) { for (auto& dev : j["devices"]) {
if (!dev.is_object()) { if (!dev.is_object()) {
continue; continue;
} }
@@ -88,11 +88,10 @@ void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
} }
} }
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id, size_t user_id_size, void* user_data) {
size_t user_id_size, void *user_data) { auto* handler = static_cast<PeerEventHandler*>(user_data);
auto *handler = static_cast<PeerEventHandler *>(user_data); GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) { if (!runtime) {
return; return;
} }
@@ -150,13 +149,12 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id,
} }
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
const char *user_id, const char* user_id,
const size_t user_id_size, const size_t user_id_size,
void *user_data) { void* user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data); auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) if (!runtime) return;
return;
std::string remote_id(user_id, user_id_size); std::string remote_id(user_id, user_id_size);
std::shared_ptr<GuiRuntime::RemoteSession> props; std::shared_ptr<GuiRuntime::RemoteSession> props;
@@ -172,10 +170,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->is_client_mode_ = true; runtime->is_client_mode_ = true;
runtime->show_connection_status_window_ = true; runtime->show_connection_status_window_ = true;
props->connection_status_.store(status); props->connection_status_.store(status);
if (status != ConnectionStatus::Connecting &&
status != ConnectionStatus::Gathering) {
props->connection_attempt_active_.store(false);
}
switch (status) { switch (status) {
case ConnectionStatus::Connected: { case ConnectionStatus::Connected: {
@@ -185,28 +179,30 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
remote_action.i.display_num = remote_action.i.display_num =
runtime->devices_.display_info_list().size(); runtime->devices_.display_info_list().size();
remote_action.i.display_list = remote_action.i.display_list =
(char **)malloc(remote_action.i.display_num * sizeof(char *)); (char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left = remote_action.i.left =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top = remote_action.i.top =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right = remote_action.i.right =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom = remote_action.i.bottom =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) { for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1, LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name); runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char *)malloc( remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1); runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i], strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(), runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length()); runtime->devices_.display_info_list()[i].name.length());
remote_action.i.display_list remote_action.i
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0'; .display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] = remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left; runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top; remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] = remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right; runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] = remote_action.i.bottom[i] =
@@ -215,7 +211,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::string host_name = GetHostName(); std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation; remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size()); 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[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size(); remote_action.i.host_name_size = host_name.size();
@@ -298,28 +295,30 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
remote_action.i.display_num = remote_action.i.display_num =
runtime->devices_.display_info_list().size(); runtime->devices_.display_info_list().size();
remote_action.i.display_list = remote_action.i.display_list =
(char **)malloc(remote_action.i.display_num * sizeof(char *)); (char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left = remote_action.i.left =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top = remote_action.i.top =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right = remote_action.i.right =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom = remote_action.i.bottom =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) { for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1, LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name); runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char *)malloc( remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1); runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i], strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(), runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length()); runtime->devices_.display_info_list()[i].name.length());
remote_action.i.display_list remote_action.i
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0'; .display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] = remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left; runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top; remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] = remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right; runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] = remote_action.i.bottom[i] =
@@ -328,17 +327,22 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::string host_name = GetHostName(); std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation; remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size()); 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[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size(); remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json(); std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(), int ret =
SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str()); runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action); remote_action_codec::Free(remote_action);
} }
runtime->need_to_create_server_window_ = true; runtime->need_to_destroy_server_window_.store(
false, std::memory_order_release);
runtime->need_to_create_server_window_.store(true,
std::memory_order_release);
runtime->is_server_mode_ = true; runtime->is_server_mode_ = true;
runtime->start_screen_capturer_ = true; runtime->start_screen_capturer_ = true;
runtime->start_speaker_capturer_ = true; runtime->start_speaker_capturer_ = true;
@@ -347,7 +351,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
{ {
std::shared_lock lock(runtime->connection_status_mutex_); std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(), if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) { runtime->connection_status_.end(),
[](const auto& kv) {
return kv.first.find("web") != std::string::npos; return kv.first.find("web") != std::string::npos;
})) { })) {
runtime->show_cursor_ = true; runtime->show_cursor_ = true;
@@ -359,64 +364,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
case ConnectionStatus::Disconnected: case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed: case ConnectionStatus::Failed:
case ConnectionStatus::Closed: { case ConnectionStatus::Closed: {
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id, runtime->HandleServerControllerDisconnected(remote_id,
"connection_closed"); "connection_closed");
bool all_disconnected = false;
{
std::shared_lock lock(runtime->connection_status_mutex_);
all_disconnected =
std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) {
return kv.second == ConnectionStatus::Closed ||
kv.second == ConnectionStatus::Failed ||
kv.second == ConnectionStatus::Disconnected;
});
}
if (all_disconnected) {
runtime->need_to_destroy_server_window_ = true;
runtime->is_server_mode_ = false;
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
// Keep Wayland capture session warm to avoid black screen on
// subsequent reconnects.
runtime->start_screen_capturer_ = true;
LOG_INFO("Keeping Wayland screen capturer running after "
"disconnect to preserve reconnect stability");
} else {
runtime->start_screen_capturer_ = false;
}
#else
runtime->start_screen_capturer_ = false;
#endif
runtime->start_speaker_capturer_ = false;
runtime->start_mouse_controller_ = false;
runtime->start_keyboard_capturer_ = false;
runtime->remote_client_id_ = "";
if (props)
props->connection_established_ = false;
if (runtime->audio_capture_) {
runtime->devices_.StopSpeakerCapturer();
runtime->audio_capture_ = false;
}
{
std::unique_lock lock(runtime->connection_status_mutex_);
runtime->connection_status_.erase(remote_id);
runtime->connection_host_names_.erase(remote_id);
}
runtime->devices_.ResetToInitialDisplay();
}
{
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_ = false;
}
}
break; break;
} }
default: default:
@@ -426,18 +375,18 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
} }
void PeerEventHandler::OnNetStatusReport( void PeerEventHandler::OnNetStatusReport(
const char *client_id, size_t client_id_size, TraversalMode mode, const char* client_id, size_t client_id_size, TraversalMode mode,
const XNetTrafficStats *net_traffic_stats, const char *user_id, const XNetTrafficStats* net_traffic_stats, const char* user_id,
const size_t user_id_size, void *user_data) { const size_t user_id_size, void* user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data); auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) { if (!runtime) {
return; return;
} }
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) { if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
std::string id, password; std::string id, password;
const char *at_pos = strchr(client_id, '@'); const char* at_pos = strchr(client_id, '@');
if (at_pos == nullptr) { if (at_pos == nullptr) {
id = client_id; id = client_id;
password.clear(); password.clear();
+1 -3
View File
@@ -58,7 +58,7 @@ struct FileTransferState {
// connection, media, input, window and transfer data that share one lifetime. // connection, media, input, window and transfer data that share one lifetime.
struct RemoteSession { struct RemoteSession {
Params params_; Params params_;
PeerPtr *peer_ = nullptr; PeerPtr* peer_ = nullptr;
std::string audio_label_ = "control_audio"; std::string audio_label_ = "control_audio";
std::string data_label_ = "data"; std::string data_label_ = "data";
std::string mouse_label_ = "mouse"; std::string mouse_label_ = "mouse";
@@ -74,8 +74,6 @@ struct RemoteSession {
SignalStatus signal_status_ = SignalStatus::SignalClosed; SignalStatus signal_status_ = SignalStatus::SignalClosed;
bool connection_established_ = false; bool connection_established_ = false;
bool rejoin_ = false; bool rejoin_ = false;
std::atomic<bool> connection_attempt_active_ = false;
std::chrono::steady_clock::time_point connection_attempt_started_at_;
bool net_traffic_stats_button_pressed_ = false; bool net_traffic_stats_button_pressed_ = false;
bool enable_mouse_control_ = true; bool enable_mouse_control_ = true;
bool mouse_controller_is_started_ = false; bool mouse_controller_is_started_ = false;
+1 -1
View File
@@ -295,7 +295,7 @@ export component ImGuiCheckBox inherits Rectangle {
border-radius: 4px; border-radius: 4px;
if root.checked: Text { if root.checked: Text {
text: FontAwesomeIcons.check; text: FontAwesomeIcons.check;
color: #2e86de; color: root.enabled ? #2e86de : #8b8f95;
font-family: "Font Awesome 6 Free"; font-family: "Font Awesome 6 Free";
font-weight: 900; font-weight: 900;
font-size: 16px; font-size: 16px;
+369 -86
View File
@@ -9,6 +9,11 @@ export struct RecentConnection {
thumbnail: image, thumbnail: image,
} }
export struct ReleaseNoteBlock {
content: styled-text,
section-gap: bool,
}
export global UiStrings { export global UiStrings {
in-out property <string> local-desktop: "Local Desktop"; in-out property <string> local-desktop: "Local Desktop";
in-out property <string> remote-desktop: "Remote Desktop"; in-out property <string> remote-desktop: "Remote Desktop";
@@ -48,8 +53,8 @@ export global UiStrings {
in-out property <string> server-port: "Signal port"; in-out property <string> server-port: "Signal port";
in-out property <string> coturn-port: "TURN port"; in-out property <string> coturn-port: "TURN port";
in-out property <string> version: "Version"; in-out property <string> version: "Version";
in-out property <string> copyright: "Copyright © CrossDesk contributors"; in-out property <string> copyright: "© 2026 by JUNKUN DI. All right reserved.";
in-out property <string> license: "Licensed under GPL-3.0-only"; in-out property <string> license: "Licensed under GNU GPL v3.";
in-out property <string> signal-connected: "Signal server connected"; in-out property <string> signal-connected: "Signal server connected";
in-out property <string> signal-disconnected: "Signal server disconnected"; in-out property <string> signal-disconnected: "Signal server disconnected";
in-out property <string> tls-error: "Signal server TLS certificate error"; in-out property <string> tls-error: "Signal server TLS certificate error";
@@ -179,15 +184,21 @@ component ConnectionDialogButton inherits Rectangle {
export component MainWindow inherits Window { export component MainWindow inherits Window {
title: "CrossDesk"; title: "CrossDesk";
preferred-width: 640px; preferred-width: 640px;
preferred-height: 450px; preferred-height: 450px + root.titlebar-height;
min-width: 640px; min-width: 640px;
max-width: 640px; max-width: 640px;
min-height: 450px; min-height: 450px + root.titlebar-height;
max-height: 450px; max-height: 450px + root.titlebar-height;
no-frame: false; no-frame: root.custom-titlebar;
background: white; background: root.wayland-titlebar ? transparent : white;
default-font-size: ImGuiFontStyle.base; default-font-size: ImGuiFontStyle.base;
in property <bool> custom-titlebar: false;
in property <bool> wayland-titlebar: false;
in property <bool> window-active: true;
private property <length> titlebar-height:
root.custom-titlebar ? (root.wayland-titlebar ? 32px : 31px) : 0px;
private property <length> wayland-corner-radius: 12px;
in property <string> local-id: ""; in property <string> local-id: "";
in property <string> local-password: ""; in property <string> local-password: "";
in-out property <string> remote-id-input: ""; in-out property <string> remote-id-input: "";
@@ -199,7 +210,7 @@ export component MainWindow inherits Window {
in property <string> current-version: ""; in property <string> current-version: "";
in property <string> latest-version: ""; in property <string> latest-version: "";
in property <string> release-name: ""; in property <string> release-name: "";
in property <string> release-notes: ""; in property <[ReleaseNoteBlock]> release-note-blocks;
in property <string> release-date: ""; in property <string> release-date: "";
in-out property <string> offline-warning: ""; in-out property <string> offline-warning: "";
in property <bool> connection-dialog-open: false; in property <bool> connection-dialog-open: false;
@@ -237,7 +248,9 @@ export component MainWindow inherits Window {
in-out property <string> server-port: ""; in-out property <string> server-port: "";
in-out property <string> coturn-port: ""; in-out property <string> coturn-port: "";
in property <bool> settings-session-active: false; in property <bool> settings-session-active: false;
in property <bool> hardware-codec-visible: true; // Keep the setting visible on every platform. Unsupported builds expose it
// as disabled so the fixed settings layout never turns into a blank row.
in property <bool> hardware-codec-available: true;
callback copy-local-id; callback copy-local-id;
callback toggle-password-visibility; callback toggle-password-visibility;
@@ -263,9 +276,13 @@ export component MainWindow inherits Window {
callback cancel-portable-service; callback cancel-portable-service;
callback acknowledge-portable-service; callback acknowledge-portable-service;
callback acknowledge-portable-service-suppressed; callback acknowledge-portable-service-suppressed;
callback main-title-drag(int, length, length);
callback minimize-main-window;
callback close-main-window;
private property <bool> menu-open: false; private property <bool> menu-open: false;
in-out property <bool> settings-open: false; in-out property <bool> settings-open: false;
private property <length> settings-scroll-y: 0px;
in-out property <bool> self-host-settings-open: false; in-out property <bool> self-host-settings-open: false;
in-out property <bool> about-open: false; in-out property <bool> about-open: false;
in-out property <bool> update-open: root.update-available; in-out property <bool> update-open: root.update-available;
@@ -299,17 +316,216 @@ export component MainWindow inherits Window {
? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px ? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px
: root.recent-tooltip-pointer-x + 12px)); : root.recent-tooltip-pointer-x + 12px));
private property <length> recent-tooltip-y: min( private property <length> recent-tooltip-y: min(
root.height - root.recent-tooltip-height - 8px, root.height - root.titlebar-height - root.recent-tooltip-height - 8px,
max(8px, root.recent-tooltip-pointer-y - root.recent-tooltip-height - 8px)); max(8px, root.recent-tooltip-pointer-y - root.recent-tooltip-height - 8px));
changed settings-open => {
if root.settings-open {
root.settings-scroll-y = 0px;
focus-sink.focus();
}
}
reset-remote-id => { reset-remote-id => {
root.remote-id-input = ""; root.remote-id-input = "";
remote-id-editor.focus(); remote-id-editor.focus();
} }
Rectangle { window-surface := Rectangle {
width: root.width; width: root.width;
height: root.height; height: root.height;
background: root.wayland-titlebar ? #f6f5f4 : transparent;
border-radius:
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
clip: root.wayland-titlebar;
if root.custom-titlebar && root.wayland-titlebar: Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: root.titlebar-height;
background: #f6f5f4;
z: 100;
Text {
x: 80px;
width: parent.width - 160px;
height: parent.height;
text: "CrossDesk";
color: root.window-active ? #3d3d3d : #747474;
font-size: 12px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
overflow: elide;
}
wayland-title-drag := TouchArea {
width: parent.width - 80px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => {
root.main-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => {
if (self.pressed) {
root.main-title-drag(2, self.mouse-x, self.mouse-y);
}
}
}
wayland-minimize-button := Rectangle {
x: parent.width - 74px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: wayland-minimize-touch.pressed ? #c0bfbc
: wayland-minimize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { wayland-minimize-touch.clicked(); }
Rectangle {
x: 8px;
y: 13px;
width: 8px;
height: 0.75px;
background: root.window-active
|| wayland-minimize-touch.has-hover
|| wayland-minimize-touch.pressed ? #454545 : #898989;
}
wayland-minimize-touch := TouchArea {
clicked => { root.minimize-main-window(); }
}
}
wayland-close-button := Rectangle {
x: parent.width - 34px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: wayland-close-touch.pressed ? #b2161d
: wayland-close-touch.has-hover ? #e01b24
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { wayland-close-touch.clicked(); }
Path {
width: parent.width;
height: parent.height;
viewbox-width: 24;
viewbox-height: 24;
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
stroke: wayland-close-touch.has-hover
|| wayland-close-touch.pressed ? white
: root.window-active ? #454545 : #898989;
stroke-width: 1px;
}
wayland-close-touch := TouchArea {
clicked => { root.close-main-window(); }
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d5d3d0;
}
}
if root.custom-titlebar && !root.wayland-titlebar: Rectangle {
x: 0px;
y: 0px;
width: root.width;
height: 31px;
background: #f9f9f9;
z: 100;
Image {
x: 8px;
y: 7px;
width: 16px;
height: 16px;
source: @image-url("../../../icons/linux/crossdesk_16x16.png");
image-fit: contain;
}
Text {
x: 30px;
width: parent.width - 122px;
height: parent.height;
text: "CrossDesk";
color: #202124;
font-size: 12px;
vertical-alignment: center;
overflow: elide;
}
title-drag := TouchArea {
x: 0px;
width: parent.width - 92px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => { root.main-title-drag(phase, self.mouse-x, self.mouse-y); }
moved => { if (self.pressed) { root.main-title-drag(2, self.mouse-x, self.mouse-y); } }
}
minimize-button := Rectangle {
x: parent.width - 92px;
width: 46px;
height: parent.height;
background: minimize-touch.pressed ? #c7c7c7
: minimize-touch.has-hover ? #e5e5e5
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { minimize-touch.clicked(); }
Text {
text: FontAwesomeIcons.minus;
color: #202124;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 10px;
horizontal-alignment: center;
vertical-alignment: center;
accessible-role: none;
}
minimize-touch := TouchArea { clicked => { root.minimize-main-window(); } }
}
close-button := Rectangle {
x: parent.width - 46px;
width: 46px;
height: parent.height;
background: close-touch.pressed ? #b3251b
: close-touch.has-hover ? #c42b1c
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { close-touch.clicked(); }
Text {
text: FontAwesomeIcons.xmark;
color: close-touch.has-hover || close-touch.pressed ? white : #202124;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 12px;
horizontal-alignment: center;
vertical-alignment: center;
accessible-role: none;
}
close-touch := TouchArea { clicked => { root.close-main-window(); } }
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d4d4d4;
}
}
content-layer := Rectangle {
x: 0px;
y: root.titlebar-height;
width: root.width;
height: root.height - self.y;
clip: true;
Rectangle {
width: parent.width;
height: parent.height;
background: white; background: white;
border-width: 0px; border-width: 0px;
border-radius: 0px; border-radius: 0px;
@@ -329,8 +545,8 @@ export component MainWindow inherits Window {
z: 7; z: 7;
} }
// App actions live in the content header; native window controls and // App actions live in the content header; window controls and dragging
// dragging are provided by the system title bar. // remain in the native or Windows-specific title bar above it.
menu-button := IconButton { menu-button := IconButton {
x: parent.width - 40px; x: parent.width - 40px;
y: 4px; y: 4px;
@@ -775,8 +991,8 @@ export component MainWindow inherits Window {
if root.menu-open: TouchArea { if root.menu-open: TouchArea {
x: 0px; x: 0px;
y: 0px; y: 0px;
width: root.width; width: parent.width;
height: root.height; height: parent.height;
z: 9; z: 9;
clicked => { clicked => {
root.menu-open = false; root.menu-open = false;
@@ -1180,9 +1396,9 @@ export component MainWindow inherits Window {
TouchArea { clicked => { focus-sink.focus(); } } TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface { DialogSurface {
x: root.compact-language ? 219px : 190px; x: root.compact-language ? 219px : 190px;
y: 9px; y: 60px;
width: root.compact-language ? 202px : 269px; width: root.compact-language ? 202px : 269px;
height: 432px; height: 330px;
border-radius: 7px; border-radius: 7px;
Rectangle { Rectangle {
@@ -1197,55 +1413,86 @@ export component MainWindow inherits Window {
Text { x: 6px; text: UiStrings.settings; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; } Text { x: 6px; text: UiStrings.settings; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
} }
Text { x: 8px; y: 31px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } settings-scroll := ScrollView {
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 30px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; } x: 0px;
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; } y: 21px;
width: parent.width;
height: 265px;
viewport-y <=> root.settings-scroll-y;
Text { x: 8px; y: 61px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } settings-content := VerticalLayout {
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 60px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; } // Reserve space for the overlaid vertical scrollbar.
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; } width: settings-scroll.width;
padding-top: 8px;
padding-bottom: 8px;
spacing: 0px;
Text { x: 8px; y: 91px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } Rectangle {
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 90px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; } height: 30px;
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; } 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 {
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.codec; 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.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.hardware-codec; color: root.hardware-codec-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
}
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; }
}
Rectangle {
height: 30px;
CompactButton { x: 8px; y: 3px; width: root.compact-language ? 70px : 206px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.autostart; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.minimize-to-tray; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.minimize-to-tray-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 154px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactButton { x: root.compact-language ? 106px : 166px; y: 3px; width: root.compact-language ? 73px : 80px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
}
}
}
Text { x: 8px; y: 121px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } Rectangle { x: 7px; y: 286px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 120px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; } CompactButton { x: root.compact-language ? 63px : 89px; y: 297px; primary: true; text: UiStrings.ok; clicked => { root.save-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
Rectangle { x: 7px; y: 147px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; } CompactButton { x: root.compact-language ? 104px : 130px; y: 297px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
if root.hardware-codec-visible: Text { x: 8px; y: 151px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.hardware-codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
if root.hardware-codec-visible: ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 151px; checked <=> root.hardware-codec-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 177px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 181px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 181px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 207px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 211px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 211px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 237px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactButton { x: 8px; y: 241px; width: root.compact-language ? 70px : 220px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 241px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 267px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 271px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.autostart; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 271px; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 297px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 301px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 301px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 327px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 331px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.minimize-to-tray; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 331px; checked <=> root.minimize-to-tray-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 357px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 361px; width: root.compact-language ? 108px : 168px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactButton { x: root.compact-language ? 120px : 180px; y: 361px; width: root.compact-language ? 73px : 80px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
CompactButton { x: root.compact-language ? 63px : 89px; y: 396px; primary: true; text: UiStrings.ok; clicked => { root.save-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 104px : 130px; y: 396px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
} }
} }
@@ -1284,7 +1531,6 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-host; font-size: ImGuiFontStyle.body; } LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-host; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; } LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
} }
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 61px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.server-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } Text { x: 8px; y: 61px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.server-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
Rectangle { Rectangle {
x: root.compact-language ? 100px : 173px; x: root.compact-language ? 100px : 173px;
@@ -1298,7 +1544,6 @@ 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; } 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; } LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
} }
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
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; } 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 { Rectangle {
x: root.compact-language ? 100px : 173px; x: root.compact-language ? 100px : 173px;
@@ -1312,7 +1557,6 @@ export component MainWindow inherits Window {
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; } 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; } LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
} }
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
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 ? 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 ? 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; } }
} }
@@ -1341,19 +1585,19 @@ export component MainWindow inherits Window {
Rectangle { y: parent.height - 7px; height: 7px; background: #d3d3d3; } 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: 6px; text: UiStrings.about; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
} }
Text { x: 30px; y: 29px; width: 240px; height: 20px; text: UiStrings.version + ": CrossDesk " + root.current-version; color: #202124; font-size: ImGuiFontStyle.body; 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: 51px; width: 240px; height: 20px; text: UiStrings.update-available + ":"; 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 { if root.update-available: Rectangle {
x: 30px; x: 30px;
y: 70px; y: 60px;
width: 240px; width: 240px;
height: 20px; 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-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(); } } version-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
} }
Text { x: 30px; y: 96px; width: 240px; height: 20px; text: UiStrings.copyright; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; } 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: 118px; width: 240px; height: 20px; text: UiStrings.license; 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: 157px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } } CompactButton { x: 133px; y: 145px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
} }
} }
@@ -1364,24 +1608,43 @@ export component MainWindow inherits Window {
TouchArea { clicked => { focus-sink.focus(); } } TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface { DialogSurface {
x: 120px; x: 120px;
y: 65px; y: 52px;
width: 400px; width: 400px;
height: 320px; height: 346px;
border-radius: 7px; border-radius: 7px;
Text { x: 40px; y: 22px; width: 320px; height: 24px; text: UiStrings.update-available + ": " + root.latest-version; color: #202124; font-size: ImGuiFontStyle.prominent; vertical-alignment: center; } Text { x: 30px; y: 22px; width: 320px; height: 24px; text: UiStrings.update-available + ": " + root.latest-version; color: #202124; font-size: ImGuiFontStyle.prominent; vertical-alignment: center; }
Rectangle { Rectangle {
x: 40px; x: 30px;
y: 47px; y: 47px;
width: 320px; width: 320px;
height: 22px; height: 22px;
Text { text: UiStrings.access-website + "https://crossdesk.cn"; color: website-touch.has-hover ? #174ea6 : #334fd1; font-size: ImGuiFontStyle.body; vertical-alignment: center; } Text {
x: 0px;
width: parent.width;
height: parent.height;
text: UiStrings.access-website + "https://crossdesk.cn";
color: website-touch.has-hover ? #174ea6 : #334fd1;
font-size: ImGuiFontStyle.body;
horizontal-alignment: left;
vertical-alignment: center;
}
website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } } website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
} }
Text {
x: 30px;
y: 75px;
width: 320px;
height: 20px;
text: UiStrings.release-notes;
color: #202124;
font-size: 12px;
vertical-alignment: center;
}
Rectangle { Rectangle {
x: 20px; x: 30px;
y: 74px; y: 105px;
width: 360px; width: 360px;
height: 200px; height: 190px;
background: white; background: white;
border-width: 1px; border-width: 1px;
border-color: #b9bdc3; border-color: #b9bdc3;
@@ -1392,19 +1655,24 @@ export component MainWindow inherits Window {
height: parent.height - 2px; height: parent.height - 2px;
VerticalLayout { VerticalLayout {
width: 340px; width: 340px;
padding-left: 19px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
spacing: 7px; spacing: 7px;
if root.release-name != "": Text { width: 312px; text: root.release-name; color: #202124; font-size: ImGuiFontStyle.body; wrap: word-wrap; } VerticalLayout {
Text { width: 312px; text: root.release-notes; color: #202124; font-size: ImGuiFontStyle.body; wrap: word-wrap; } width: 312px;
if root.release-date != "": Text { width: 312px; text: UiStrings.release-date-label + root.release-date; color: #4f555e; font-size: ImGuiFontStyle.small; wrap: word-wrap; } spacing: 10px;
for block in root.release-note-blocks: VerticalLayout {
padding-top: block.section-gap ? 10px : 0px;
StyledText { width: 312px; text: block.content; default-color: #202124; default-font-size: ImGuiFontStyle.body; }
} }
} }
} }
CompactButton { x: root.compact-language ? 163px : 130px; y: 283px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; clicked => { root.open-download(); root.update-open = false; } } }
CompactButton { x: root.compact-language ? 204px : 207px; y: 283px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.update-open = false; } } }
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; clicked => { root.open-download(); root.update-open = false; } }
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.update-open = false; } }
} }
} }
@@ -1478,4 +1746,19 @@ export component MainWindow inherits Window {
} }
} }
} }
}
if root.custom-titlebar: Rectangle {
x: 0px;
y: 0px;
width: root.width;
height: root.height;
background: transparent;
border-width: 1px;
border-color: root.wayland-titlebar ? #aaa8a6 : #8b8b8b;
border-radius:
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
z: 200;
}
}
} }
+194 -6
View File
@@ -112,13 +112,16 @@ component ControlBarButton inherits Rectangle {
export component StreamWindow inherits Window { export component StreamWindow inherits Window {
title: "CrossDesk"; title: "CrossDesk";
preferred-width: 1280px; preferred-width: 1280px;
preferred-height: 720px; preferred-height: 720px + (root.custom-titlebar ? 32px : 0px);
min-width: 640px; min-width: 640px;
min-height: 360px; min-height: 360px + (root.custom-titlebar ? 32px : 0px);
no-frame: false; no-frame: root.custom-titlebar;
background: black; background: root.custom-titlebar ? transparent : black;
default-font-size: ImGuiFontStyle.base; default-font-size: ImGuiFontStyle.base;
in property <bool> custom-titlebar: false;
in property <bool> window-active: true;
in property <bool> window-maximized: false;
in property <[StreamTab]> tabs; in property <[StreamTab]> tabs;
in-out property <int> selected-tab: 0; in-out property <int> selected-tab: 0;
in property <image> frame; in property <image> frame;
@@ -153,9 +156,14 @@ export component StreamWindow inherits Window {
callback toggle-stats; callback toggle-stats;
callback toggle-fullscreen; callback toggle-fullscreen;
callback disconnect; callback disconnect;
callback stream-title-drag(int, length, length);
callback minimize-stream-window;
callback toggle-maximize-stream-window;
callback close-stream-window;
callback pointer-input(PointerEventButton, PointerEventKind, length, length); callback pointer-input(PointerEventButton, PointerEventKind, length, length);
callback scroll-input(length, length, length, length); callback scroll-input(length, length, length, length);
callback key-input(string, bool, bool, bool, bool, bool); callback key-input(string, bool, bool, bool, bool, bool);
callback keyboard-focus-changed(bool);
private property <bool> control-expanded: true; private property <bool> control-expanded: true;
private property <bool> display-menu-open: false; private property <bool> display-menu-open: false;
@@ -169,6 +177,9 @@ export component StreamWindow inherits Window {
private property <length> control-press-x: 0px; private property <length> control-press-x: 0px;
private property <length> control-press-y: 0px; private property <length> control-press-y: 0px;
private property <int> dragging-tab: -1; private property <int> dragging-tab: -1;
private property <length> titlebar-height:
root.custom-titlebar && !root.fullscreen-enabled ? 32px : 0px;
private property <length> window-corner-radius: 12px;
callback begin-control-drag(length, length); callback begin-control-drag(length, length);
callback move-control-drag(length, length); callback move-control-drag(length, length);
callback end-control-drag; callback end-control-drag;
@@ -202,10 +213,167 @@ export component StreamWindow inherits Window {
} }
} }
Rectangle { window-surface := Rectangle {
width: root.width; width: root.width;
height: root.height; height: root.height;
background: black; background: black;
border-radius:
root.custom-titlebar && !root.fullscreen-enabled && !root.window-maximized
? root.window-corner-radius : 0px;
clip: root.custom-titlebar && !root.fullscreen-enabled;
if root.custom-titlebar && !root.fullscreen-enabled: Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: root.titlebar-height;
background: #f6f5f4;
z: 100;
Text {
x: 120px;
width: parent.width - 240px;
height: parent.height;
text: "CrossDesk";
color: root.window-active ? #3d3d3d : #747474;
font-size: 12px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
overflow: elide;
}
stream-title-drag-area := TouchArea {
width: parent.width - 120px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => {
root.stream-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => {
if (self.pressed) {
root.stream-title-drag(2, self.mouse-x, self.mouse-y);
}
}
double-clicked => {
root.toggle-maximize-stream-window();
}
}
stream-minimize-button := Rectangle {
x: parent.width - 114px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-minimize-touch.pressed ? #c0bfbc
: stream-minimize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { stream-minimize-touch.clicked(); }
Rectangle {
x: 8px;
y: 13px;
width: 8px;
height: 0.75px;
background: root.window-active
|| stream-minimize-touch.has-hover
|| stream-minimize-touch.pressed ? #454545 : #898989;
}
stream-minimize-touch := TouchArea {
clicked => { root.minimize-stream-window(); }
}
}
stream-maximize-button := Rectangle {
x: parent.width - 74px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-maximize-touch.pressed ? #c0bfbc
: stream-maximize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: root.window-maximized ? "Restore" : "Maximize";
accessible-action-default => { stream-maximize-touch.clicked(); }
if !root.window-maximized: Rectangle {
x: 8px;
y: 8px;
width: 8px;
height: 8px;
background: transparent;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
if root.window-maximized: Rectangle {
x: 9px;
y: 7px;
width: 8px;
height: 8px;
background: transparent;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
if root.window-maximized: Rectangle {
x: 7px;
y: 9px;
width: 8px;
height: 8px;
background: stream-maximize-button.background;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
stream-maximize-touch := TouchArea {
clicked => { root.toggle-maximize-stream-window(); }
}
}
stream-close-button := Rectangle {
x: parent.width - 34px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-close-touch.pressed ? #b2161d
: stream-close-touch.has-hover ? #e01b24
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { stream-close-touch.clicked(); }
Path {
width: parent.width;
height: parent.height;
viewbox-width: 24;
viewbox-height: 24;
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
stroke: stream-close-touch.has-hover
|| stream-close-touch.pressed ? white
: root.window-active ? #454545 : #898989;
stroke-width: 1px;
}
stream-close-touch := TouchArea {
clicked => { root.close-stream-window(); }
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d5d3d0;
}
}
content-layer := Rectangle {
y: root.titlebar-height;
width: parent.width;
height: parent.height - self.y;
background: black;
clip: true;
// Keep the ImGui tab strip behavior, including pointer-drag reordering. // Keep the ImGui tab strip behavior, including pointer-drag reordering.
Rectangle { Rectangle {
@@ -319,6 +487,8 @@ export component StreamWindow inherits Window {
input-focus := FocusScope { input-focus := FocusScope {
focus-on-click: true; focus-on-click: true;
focus-gained => { root.keyboard-focus-changed(true); }
focus-lost => { root.keyboard-focus-changed(false); }
key-pressed(event) => { key-pressed(event) => {
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
accept accept
@@ -333,6 +503,9 @@ export component StreamWindow inherits Window {
&& self.mouse-x <= control.x + control.width && self.mouse-x <= control.x + control.width
&& self.mouse-y >= control.y && self.mouse-y >= control.y
&& self.mouse-y <= control.y + control.height; && self.mouse-y <= control.y + control.height;
if event.kind == PointerEventKind.down && !over-control {
input-focus.focus();
}
if event.kind == PointerEventKind.down if event.kind == PointerEventKind.down
&& event.button == PointerEventButton.left && event.button == PointerEventButton.left
&& over-control { && over-control {
@@ -341,7 +514,10 @@ export component StreamWindow inherits Window {
&& (event.kind == PointerEventKind.up && (event.kind == PointerEventKind.up
|| event.kind == PointerEventKind.cancel) { || event.kind == PointerEventKind.cancel) {
root.end-control-drag(); root.end-control-drag();
} else if !root.control-dragging { // The control bar overlays the remote video. Events in
// its full rectangle (including gaps between buttons)
// are local UI input and must never reach the peer.
} else if !root.control-dragging && !over-control {
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y); root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
} }
} }
@@ -646,4 +822,16 @@ export component StreamWindow inherits Window {
} }
} }
} }
if root.custom-titlebar && !root.fullscreen-enabled: Rectangle {
width: parent.width;
height: parent.height;
background: transparent;
border-width: 1px;
border-color: #aaa8a6;
border-radius:
root.window-maximized ? 0px : root.window-corner-radius;
z: 200;
}
}
} }
@@ -1,4 +1,3 @@
#include <algorithm>
#include <string> #include <string>
#include "application/gui_application.h" #include "application/gui_application.h"
@@ -8,51 +7,9 @@
namespace crossdesk { namespace crossdesk {
std::string CleanMarkdown(const std::string &markdown) {
std::string result = markdown;
// remove # title mark
size_t pos = 0;
while (pos < result.length()) {
if (result[pos] == '\n' || pos == 0) {
size_t line_start = (result[pos] == '\n') ? pos + 1 : pos;
if (line_start < result.length() && result[line_start] == '#') {
size_t hash_end = line_start;
while (hash_end < result.length() &&
(result[hash_end] == '#' || result[hash_end] == ' ')) {
hash_end++;
}
result.erase(line_start, hash_end - line_start);
pos = line_start;
continue;
}
}
pos++;
}
// remove ** bold mark
pos = 0;
while ((pos = result.find("**", pos)) != std::string::npos) {
result.erase(pos, 2);
}
// remove all spaces
result.erase(std::remove(result.begin(), result.end(), ' '), result.end());
// replace . with 、
pos = 0;
while ((pos = result.find('.', pos)) != std::string::npos) {
result.replace(pos, 1, "");
pos += 1; // Move to next position after the replacement
}
return result;
}
int GuiApplication::UpdateNotificationWindow() { int GuiApplication::UpdateNotificationWindow() {
if (show_update_notification_window_ && update_available_) { if (show_update_notification_window_ && update_available_) {
const ImGuiViewport *viewport = ImGui::GetMainViewport(); const ImGuiViewport* viewport = ImGui::GetMainViewport();
float update_notification_window_width = title_bar_button_width_ * 10.0f; float update_notification_window_width = title_bar_button_width_ * 10.0f;
float update_notification_window_height = title_bar_button_width_ * 8.0f; float update_notification_window_height = title_bar_button_width_ * 8.0f;
@@ -142,8 +99,7 @@ int GuiApplication::UpdateNotificationWindow() {
// release notes // release notes
if (!release_notes_.empty()) { if (!release_notes_.empty()) {
ImGui::SetCursorPosX(update_notification_window_width * 0.05f); ImGui::SetCursorPosX(update_notification_window_width * 0.05f);
std::string cleaned_notes = CleanMarkdown(release_notes_); ImGui::TextWrapped("%s", release_notes_.c_str());
ImGui::TextWrapped("%s", cleaned_notes.c_str());
ImGui::Spacing(); ImGui::Spacing();
} }
+6 -5
View File
@@ -57,7 +57,7 @@ void ScaleNv12ToABGR(char* src, int src_w, int src_h, int dst_w, int dst_h,
v_fit.data(), (fit_w + 1) / 2, abgr.data(), fit_w * 4, v_fit.data(), (fit_w + 1) / 2, abgr.data(), fit_w * 4,
fit_w, fit_h); fit_w, fit_h);
memset(dst_rgba, 0, dst_w * dst_h * 4); std::memset(dst_rgba, 0, dst_w * dst_h * 4);
for (int i = 0; i < dst_w * dst_h; ++i) { for (int i = 0; i < dst_w * dst_h; ++i) {
dst_rgba[i * 4 + 3] = static_cast<char>(0xFF); dst_rgba[i * 4 + 3] = static_cast<char>(0xFF);
} }
@@ -65,7 +65,8 @@ void ScaleNv12ToABGR(char* src, int src_w, int src_h, int dst_w, int dst_h,
for (int row = 0; row < fit_h; ++row) { for (int row = 0; row < fit_h; ++row) {
int dst_offset = int dst_offset =
((row + (dst_h - fit_h) / 2) * dst_w + (dst_w - fit_w) / 2) * 4; ((row + (dst_h - fit_h) / 2) * dst_w + (dst_w - fit_w) / 2) * 4;
memcpy(dst_rgba + dst_offset, abgr.data() + row * fit_w * 4, fit_w * 4); std::memcpy(dst_rgba + dst_offset, abgr.data() + row * fit_w * 4,
fit_w * 4);
} }
} }
@@ -85,8 +86,8 @@ Thumbnail::Thumbnail(std::string save_path, unsigned char* aes128_key,
save_path_ = save_path; save_path_ = save_path;
} }
memcpy(aes128_key_, aes128_key, sizeof(aes128_key_)); std::memcpy(aes128_key_, aes128_key, sizeof(aes128_key_));
memcpy(aes128_iv_, aes128_iv, sizeof(aes128_iv_)); std::memcpy(aes128_iv_, aes128_iv, sizeof(aes128_iv_));
std::filesystem::create_directories(save_path_); std::filesystem::create_directories(save_path_);
} }
@@ -116,7 +117,7 @@ int Thumbnail::SaveToThumbnail(const char* yuv420p, int width, int height,
thumbnail_height_, rgba_buffer_); thumbnail_height_, rgba_buffer_);
} else { } else {
// If yuv420p is null, fill the buffer with black pixels // If yuv420p is null, fill the buffer with black pixels
memset(rgba_buffer_, 0x00, thumbnail_width_ * thumbnail_height_ * 4); std::memset(rgba_buffer_, 0x00, thumbnail_width_ * thumbnail_height_ * 4);
for (int i = 0; i < thumbnail_width_ * thumbnail_height_; ++i) { for (int i = 0; i < thumbnail_width_ * thumbnail_height_; ++i) {
// Set alpha channel to opaque // Set alpha channel to opaque
rgba_buffer_[i * 4 + 3] = static_cast<char>(0xFF); rgba_buffer_[i * 4 + 3] = static_cast<char>(0xFF);
+5 -4
View File
@@ -7,6 +7,7 @@
#ifndef _THUMBNAIL_H_ #ifndef _THUMBNAIL_H_
#define _THUMBNAIL_H_ #define _THUMBNAIL_H_
#include <cstring>
#include <filesystem> #include <filesystem>
#include <map> #include <map>
#include <unordered_map> #include <unordered_map>
@@ -52,18 +53,18 @@ class Thumbnail {
int DeleteAllFilesInDirectory(); int DeleteAllFilesInDirectory();
int GetKey(unsigned char* aes128_key) { int GetKey(unsigned char* aes128_key) {
memcpy(aes128_key, aes128_key_, sizeof(aes128_key_)); std::memcpy(aes128_key, aes128_key_, sizeof(aes128_key_));
return sizeof(aes128_key_); return sizeof(aes128_key_);
} }
int GetIv(unsigned char* aes128_iv) { int GetIv(unsigned char* aes128_iv) {
memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_)); std::memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_));
return sizeof(aes128_iv_); return sizeof(aes128_iv_);
} }
int GetKeyAndIv(unsigned char* aes128_key, unsigned char* aes128_iv) { int GetKeyAndIv(unsigned char* aes128_key, unsigned char* aes128_iv) {
memcpy(aes128_key, aes128_key_, sizeof(aes128_key_)); std::memcpy(aes128_key, aes128_key_, sizeof(aes128_key_));
memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_)); std::memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_));
return 0; return 0;
} }
+34 -11
View File
@@ -19,7 +19,7 @@ std::filesystem::path FindRepoRoot() {
return {}; return {};
} }
std::string ReadFile(const std::filesystem::path &path) { std::string ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary); std::ifstream file(path, std::ios::binary);
if (!file) { if (!file) {
return {}; return {};
@@ -30,8 +30,8 @@ std::string ReadFile(const std::filesystem::path &path) {
return stream.str(); return stream.str();
} }
bool ExpectContains(const char *name, const std::string &value, bool ExpectContains(const char* name, const std::string& value,
const std::string &expected) { const std::string& expected) {
if (value.find(expected) != std::string::npos) { if (value.find(expected) != std::string::npos) {
return true; return true;
} }
@@ -40,8 +40,8 @@ bool ExpectContains(const char *name, const std::string &value,
return false; return false;
} }
bool ExpectNotContains(const char *name, const std::string &value, bool ExpectNotContains(const char* name, const std::string& value,
const std::string &unexpected) { const std::string& unexpected) {
if (value.find(unexpected) == std::string::npos) { if (value.find(unexpected) == std::string::npos) {
return true; return true;
} }
@@ -73,6 +73,10 @@ int main() {
ReadFile(repo_root / "src/gui/runtime/remote_session.h"); ReadFile(repo_root / "src/gui/runtime/remote_session.h");
const std::string connection_runtime_cpp = const std::string connection_runtime_cpp =
ReadFile(repo_root / "src/gui/runtime/connection_runtime.cpp"); ReadFile(repo_root / "src/gui/runtime/connection_runtime.cpp");
const std::string peer_event_handler_cpp =
ReadFile(repo_root / "src/gui/runtime/peer_event_handler.cpp");
const std::string application_state_h =
ReadFile(repo_root / "src/gui/application/application_state.h");
bool ok = true; bool ok = true;
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable"); ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
@@ -81,6 +85,11 @@ int main() {
"\"Remote unavailable\""); "\"Remote unavailable\"");
ok &= ExpectContains("peer_connection.cpp", peer_connection, ok &= ExpectContains("peer_connection.cpp", peer_connection,
"ConnectionStatus::RemoteUnavailable"); "ConnectionStatus::RemoteUnavailable");
ok &= ExpectContains("peer_connection.cpp", peer_connection,
"IsCurrentPeerConnection");
ok &= ExpectContains(
"peer_connection.cpp", peer_connection,
"on_connection_status_(ConnectionStatus::Closed, remote_user_id.data()");
ok &= ExpectNotContains("peer_connection.cpp", peer_connection, ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
"\"Device offline\""); "\"Device offline\"");
ok &= ExpectNotContains("peer_connection.cpp", peer_connection, ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
@@ -95,16 +104,30 @@ int main() {
"return localization::device_offline[language];"); "return localization::device_offline[language];");
ok &= ExpectContains("runtime_state.h", runtime_state_h, ok &= ExpectContains("runtime_state.h", runtime_state_h,
"pending_presence_probe_started_at_"); "pending_presence_probe_started_at_");
ok &= ExpectContains("remote_session.h", remote_session_h,
"connection_attempt_started_at_");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"HandleConnectionTimeouts"); "HandlePresenceProbeTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"kPresenceProbeTimeout"); "kPresenceProbeTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectNotContains("remote_session.h", remote_session_h,
"kConnectionAttemptTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"connection_attempt_started_at_"); "connection_attempt_started_at_");
ok &= ExpectNotContains("remote_session.h", remote_session_h,
"connection_attempt_active_");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"kConnectionAttemptTimeout");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"ConnectionStatus::Failed");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"props->connection_status_.store(status);");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"case ConnectionStatus::Failed:");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"HandleServerControllerDisconnected");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"HandleServerControllerDisconnected(remote_id");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"ControllerHeartbeat");
ok &= ExpectContains("application_state.h", application_state_h,
"std::atomic<bool> need_to_destroy_server_window_");
return ok ? 0 : 1; return ok ? 0 : 1;
} }
+33 -25
View File
@@ -18,7 +18,7 @@ std::filesystem::path FindRepoRoot() {
return {}; return {};
} }
std::string ReadFile(const std::filesystem::path &path) { std::string ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary); std::ifstream file(path, std::ios::binary);
if (!file) { if (!file) {
return {}; return {};
@@ -28,8 +28,8 @@ std::string ReadFile(const std::filesystem::path &path) {
return stream.str(); return stream.str();
} }
bool ExpectContains(const char *name, const std::string &value, bool ExpectContains(const char* name, const std::string& value,
const std::string &expected) { const std::string& expected) {
if (value.find(expected) != std::string::npos) { if (value.find(expected) != std::string::npos) {
return true; return true;
} }
@@ -37,8 +37,8 @@ bool ExpectContains(const char *name, const std::string &value,
return false; return false;
} }
bool ExpectNotContains(const char *name, const std::string &value, bool ExpectNotContains(const char* name, const std::string& value,
const std::string &unexpected) { const std::string& unexpected) {
if (value.find(unexpected) == std::string::npos) { if (value.find(unexpected) == std::string::npos) {
return true; return true;
} }
@@ -46,8 +46,8 @@ bool ExpectNotContains(const char *name, const std::string &value,
return false; return false;
} }
bool ExpectContainsAtLeast(const char *name, const std::string &value, bool ExpectContainsAtLeast(const char* name, const std::string& value,
const std::string &expected, size_t min_count) { const std::string& expected, size_t min_count) {
size_t count = 0; size_t count = 0;
size_t pos = 0; size_t pos = 0;
while ((pos = value.find(expected, pos)) != std::string::npos) { while ((pos = value.find(expected, pos)) != std::string::npos) {
@@ -84,14 +84,16 @@ int main() {
"root.shortcut-menu-open = false;"); "root.shortcut-menu-open = false;");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"if root.display-menu-open: Rectangle"); "if root.display-menu-open: Rectangle");
ok &= ExpectContains("stream_window.slint", stream, ok &=
"display-touch.has-hover"); ExpectContains("stream_window.slint", stream, "display-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"root.switch-display(index)"); "root.switch-display(index)");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"if root.shortcut-menu-open: Rectangle"); "if root.shortcut-menu-open: Rectangle");
ok &=
ExpectContains("stream_window.slint", stream, "shortcut-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"shortcut-touch.has-hover"); "} else if !root.control-dragging && !over-control {");
ok &= ExpectContains("common.slint", common, ok &= ExpectContains("common.slint", common,
"if touch.has-hover && root.tooltip != \"\": Rectangle"); "if touch.has-hover && root.tooltip != \"\": Rectangle");
@@ -100,32 +102,38 @@ int main() {
"tooltip: StreamStrings.select-display"); "tooltip: StreamStrings.select-display");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.send-shortcut"); "tooltip: StreamStrings.send-shortcut");
ok &= ExpectContains("stream_window.slint", stream, ok &=
"root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse"); ExpectContains("stream_window.slint", stream,
ok &= ExpectContains("stream_window.slint", stream, "root.mouse-control-enabled ? StreamStrings.release-mouse "
": StreamStrings.control-mouse");
ok &= ExpectContains(
"stream_window.slint", stream,
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio"); "root.audio-enabled ? StreamStrings.mute : StreamStrings.audio");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.select-file"); "tooltip: StreamStrings.select-file");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats"); "root.stats-visible ? StreamStrings.hide-stats : "
ok &= ExpectContains("stream_window.slint", stream, "StreamStrings.show-stats");
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen"); ok &=
ExpectContains("stream_window.slint", stream,
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen "
": StreamStrings.fullscreen");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.disconnect"); "tooltip: StreamStrings.disconnect");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"root.control-expanded ? StreamStrings.collapse-control : StreamStrings.expand-control"); "root.control-expanded ? StreamStrings.collapse-control "
": StreamStrings.expand-control");
ok &= ExpectContains("gui_application.cpp", application, ok &=
"set_select_display("); ExpectContains("gui_application.cpp", application, "set_select_display(");
ok &= ExpectContains("gui_application.cpp", application, ok &= ExpectContains("gui_application.cpp", application, "set_show_stats(");
"set_show_stats("); ok &=
ok &= ExpectContains("gui_application.cpp", application, ExpectContains("gui_application.cpp", application, "set_expand_control(");
"set_expand_control(");
ok &= ExpectContains("gui_application.cpp", application, ok &= ExpectContains("gui_application.cpp", application,
"set_collapse_control("); "set_collapse_control(");
ok &= ExpectContains("gui_application.cpp", application, ok &= ExpectContains("gui_application.cpp", application,
"(*ui_->stream)->global<ui::StreamStrings>()"); "(*ui_->stream)->global<ui::StreamStrings>()");
ok &= ExpectNotContains("gui_application.cpp", application, ok &=
"#include <imgui"); ExpectNotContains("gui_application.cpp", application, "#include <imgui");
return ok ? 0 : 1; return ok ? 0 : 1;
} }
+136
View File
@@ -1,29 +1,109 @@
#include "crossdesk_ui.h" #include "crossdesk_ui.h"
#include "fa_solid_900.h"
#include <cassert> #include <cassert>
#include <cstdlib>
#include <fstream>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
namespace {
// Set the CROSSDESK_*_UI_SNAPSHOT variables while using Slint's software
// renderer to export deterministic active and inactive visual-check images.
bool WriteWindowSnapshot(const slint::Window &window, const char *path) {
const auto snapshot = window.take_snapshot();
if (!snapshot) {
return false;
}
std::ofstream output(path, std::ios::binary);
output << "P6\n"
<< snapshot->width() << ' ' << snapshot->height() << "\n255\n";
for (const auto &pixel : *snapshot) {
output.put(static_cast<char>(pixel.r));
output.put(static_cast<char>(pixel.g));
output.put(static_cast<char>(pixel.b));
}
return output.good();
}
bool RegisterFontAwesome(slint::Window &window) {
return !window.window_handle()
.register_font_from_data(fa_solid_900_ttf,
fa_solid_900_ttf_len)
.has_value();
}
} // namespace
int main() { int main() {
auto window = crossdesk::ui::MainWindow::create(); auto window = crossdesk::ui::MainWindow::create();
auto stream = crossdesk::ui::StreamWindow::create(); auto stream = crossdesk::ui::StreamWindow::create();
auto server = crossdesk::ui::ServerWindow::create(); auto server = crossdesk::ui::ServerWindow::create();
if (!RegisterFontAwesome(window->window()) ||
!RegisterFontAwesome(stream->window()) ||
!RegisterFontAwesome(server->window())) {
return 1;
}
window->set_local_id("123 456 789"); window->set_local_id("123 456 789");
window->set_local_password("123456"); window->set_local_password("123456");
window->set_connection_dialog_open(true); window->set_connection_dialog_open(true);
window->set_connection_password_required(true); window->set_connection_password_required(true);
window->set_custom_titlebar(true);
window->set_wayland_titlebar(true);
window->set_settings_session_active(true); window->set_settings_session_active(true);
window->set_hardware_codec_available(false);
window->set_portable_service_settings_visible(true); window->set_portable_service_settings_visible(true);
assert(std::string(window->get_local_id()) == "123 456 789"); assert(std::string(window->get_local_id()) == "123 456 789");
assert(window->get_connection_dialog_open()); assert(window->get_connection_dialog_open());
assert(window->get_connection_password_required()); assert(window->get_connection_password_required());
assert(window->get_custom_titlebar());
assert(window->get_wayland_titlebar());
assert(window->get_settings_session_active()); assert(window->get_settings_session_active());
assert(!window->get_hardware_codec_available());
assert(window->get_portable_service_settings_visible()); assert(window->get_portable_service_settings_visible());
if (const char *snapshot_path =
std::getenv("CROSSDESK_MAIN_UI_SNAPSHOT")) {
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
if (!snapshot_written) {
return 2;
}
}
if (const char *snapshot_path =
std::getenv("CROSSDESK_MAIN_INACTIVE_UI_SNAPSHOT")) {
window->set_window_active(false);
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
window->set_window_active(true);
if (!snapshot_written) {
return 3;
}
}
window->set_settings_open(true); window->set_settings_open(true);
window->set_about_open(true); window->set_about_open(true);
assert(window->get_settings_open()); assert(window->get_settings_open());
assert(window->get_about_open()); assert(window->get_about_open());
if (const char *snapshot_path =
std::getenv("CROSSDESK_SETTINGS_UI_SNAPSHOT")) {
window->set_about_open(false);
window->set_connection_dialog_open(false);
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
if (!snapshot_written) {
return 6;
}
window->set_connection_dialog_open(true);
window->set_about_open(true);
}
window->set_remote_id_input("987654321"); window->set_remote_id_input("987654321");
window->invoke_reset_remote_id(); window->invoke_reset_remote_id();
assert(std::string(window->get_remote_id_input()).empty()); assert(std::string(window->get_remote_id_input()).empty());
@@ -62,7 +142,37 @@ int main() {
stream->set_tabs( stream->set_tabs(
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>( std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
std::vector{tab})); std::vector{tab}));
stream->set_custom_titlebar(true);
stream->set_window_maximized(true);
assert(stream->get_tabs()->row_count() == 1); assert(stream->get_tabs()->row_count() == 1);
assert(stream->get_custom_titlebar());
assert(stream->get_window_maximized());
if (const char *snapshot_path =
std::getenv("CROSSDESK_STREAM_UI_SNAPSHOT")) {
stream->set_window_maximized(false);
stream->show();
const bool snapshot_written =
WriteWindowSnapshot(stream->window(), snapshot_path);
stream->hide();
stream->set_window_maximized(true);
if (!snapshot_written) {
return 4;
}
}
if (const char *snapshot_path =
std::getenv("CROSSDESK_STREAM_INACTIVE_UI_SNAPSHOT")) {
stream->set_window_maximized(false);
stream->set_window_active(false);
stream->show();
const bool snapshot_written =
WriteWindowSnapshot(stream->window(), snapshot_path);
stream->hide();
stream->set_window_active(true);
stream->set_window_maximized(true);
if (!snapshot_written) {
return 5;
}
}
bool tab_reordered = false; bool tab_reordered = false;
stream->on_reorder_tab([&](int index, float x, float width) { stream->on_reorder_tab([&](int index, float x, float width) {
@@ -71,6 +181,32 @@ int main() {
stream->invoke_reorder_tab(0, 120.0f, 110.0f); stream->invoke_reorder_tab(0, 120.0f, 110.0f);
assert(tab_reordered); assert(tab_reordered);
bool maximize_requested = false;
stream->on_toggle_maximize_stream_window(
[&] { maximize_requested = true; });
stream->invoke_toggle_maximize_stream_window();
assert(maximize_requested);
bool keyboard_focus_changed = false;
bool keyboard_input_received = false;
stream->on_keyboard_focus_changed(
[&](bool focused) { keyboard_focus_changed = focused; });
stream->on_key_input(
[&](slint::SharedString text, bool pressed, bool, bool, bool, bool) {
keyboard_input_received = std::string(text) == "a" && pressed;
});
stream->window().set_size(
slint::LogicalSize(slint::Size<float>{1280.0f, 720.0f}));
stream->window().dispatch_pointer_press_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_pointer_release_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_key_press_event("a");
assert(keyboard_focus_changed);
assert(keyboard_input_received);
crossdesk::ui::FileTransferEntry transfer; crossdesk::ui::FileTransferEntry transfer;
transfer.name = "archive.zip"; transfer.name = "archive.zip";
transfer.status = "Sending"; transfer.status = "Sending";
+120
View File
@@ -0,0 +1,120 @@
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
namespace {
std::filesystem::path FindRepoRoot() {
std::filesystem::path current = std::filesystem::current_path();
while (!current.empty()) {
if (std::filesystem::exists(current / "xmake.lua") &&
std::filesystem::exists(
current / "submodules/minirtc/src/transport/ice_transport_controller.cpp")) {
return current;
}
current = current.parent_path();
}
return {};
}
std::string ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return {};
}
std::ostringstream stream;
stream << file.rdbuf();
return stream.str();
}
std::string ExtractSection(const std::string& source, const std::string& begin,
const std::string& end) {
const size_t begin_pos = source.find(begin);
if (begin_pos == std::string::npos) {
return {};
}
const size_t end_pos = source.find(end, begin_pos + begin.size());
if (end_pos == std::string::npos) {
return {};
}
return source.substr(begin_pos, end_pos - begin_pos);
}
bool ExpectContains(const char* name, const std::string& value,
const std::string& expected) {
if (value.find(expected) != std::string::npos) {
return true;
}
std::cerr << name << " missing expected text: " << expected << "\n";
return false;
}
bool ExpectNotContains(const char* name, const std::string& value,
const std::string& unexpected) {
if (value.find(unexpected) == std::string::npos) {
return true;
}
std::cerr << name << " contains unsafe text: " << unexpected << "\n";
return false;
}
} // namespace
int main() {
const std::filesystem::path repo_root = FindRepoRoot();
if (repo_root.empty()) {
std::cerr << "failed to locate repository root\n";
return 1;
}
const std::string controller = ReadFile(
repo_root /
"submodules/minirtc/src/transport/ice_transport_controller.cpp");
const std::string destroy = ExtractSection(
controller, "void IceTransportController::Destroy()",
"uint32_t IceTransportController::AddVideoSendChannel");
const std::string send_video = ExtractSection(
controller, "int IceTransportController::SendVideo(",
"void IceTransportController::MaybeDegradeResolutionOnEncodeTime");
const std::string on_video_encoded = ExtractSection(
controller, "int IceTransportController::OnVideoEncoded(",
"int IceTransportController::SendAudio(");
bool ok = true;
ok &= ExpectContains("Destroy", destroy,
"std::unique_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("Destroy", destroy,
"senders.swap(stream_senders_);");
ok &= ExpectContains("Destroy", destroy,
"codecs.push_back(std::move(context->codec));");
ok &= ExpectContains("Destroy", destroy, "senders.clear();");
ok &= ExpectContains("Destroy", destroy, "codecs.clear();");
ok &= ExpectNotContains("Destroy", destroy,
"std::shared_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("SendVideo", send_video,
"std::weak_ptr<IceTransportController> weak_self");
ok &= ExpectContains("SendVideo", send_video,
"std::weak_ptr<StreamContext> weak_context");
ok &= ExpectContains(
"SendVideo", send_video,
"[weak_self, weak_context, channel_name, queue_delay_ms");
ok &= ExpectContains("SendVideo", send_video,
"return self->OnVideoEncoded(");
ok &= ExpectNotContains("SendVideo", send_video,
"[this, channel_name, context");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"if (!is_running_.load())");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"std::shared_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"it->second != context");
return ok ? 0 : 1;
}
+35
View File
@@ -0,0 +1,35 @@
package("rust")
set_kind("toolchain")
set_homepage("https://rust-lang.org")
set_description("Rust is a general-purpose programming language emphasizing performance, type safety, and concurrency.")
add_versions("1.92.0", "")
add_deps("ca-certificates", {host = true, private = true})
add_deps("rustup", {host = true, private = true, system = false})
on_install(function(package)
import("core.tools.rustc.target_triple")
local host_target = assert(target_triple(package:plat(), package:arch()),
"failed to determine the Rust host target triple")
local version = package:version():shortstr()
local toolchain_name = version .. "-" .. host_target
local rustup_home = assert(os.getenv("RUSTUP_HOME"), "cannot find rustup home")
os.vrunv("rustup", {"install", "--no-self-update", toolchain_name})
os.vmv(path.join(rustup_home, "toolchains", toolchain_name, "*"), package:installdir())
-- Keep the Xmake-managed toolchain isolated from rustup after installation.
os.vrm(path.join(rustup_home, "toolchains", toolchain_name))
os.vrm(path.join(rustup_home, "update-hashes", toolchain_name))
package:addenv("RC", "bin/rustc" .. (is_host("windows") and ".exe" or ""))
package:mark_as_pathenv("RC")
end)
on_test(function(package)
os.vrun("cargo --version")
os.vrun("rustc --version")
end)
package_end()
+1
View File
@@ -10,6 +10,7 @@ package("slint")
add_configs("shared", {description = "Build the Slint runtime as a shared library", default = true, type = "boolean", readonly = true}) add_configs("shared", {description = "Build the Slint runtime as a shared library", default = true, type = "boolean", readonly = true})
add_deps("cmake") add_deps("cmake")
add_deps("rust 1.92.0", {host = true, private = true, system = false})
on_load(function(package) on_load(function(package)
package:add("includedirs", "include/slint") package:add("includedirs", "include/slint")
+1 -1
View File
@@ -1 +1 @@
includes("cpp-httplib", "slint") includes("cpp-httplib", "rust", "slint")
+3 -1
View File
@@ -64,10 +64,12 @@ function setup_platform_settings()
"/usr/local/include/pipewire-0.3", "/usr/local/include/pipewire-0.3",
"/usr/include/pipewire", "/usr/include/pipewire",
"/usr/local/include/pipewire", "/usr/local/include/pipewire",
"/opt/crossdesk-pipewire-sdk/include/pipewire-0.3",
"/usr/include/spa-0.2", "/usr/include/spa-0.2",
"/usr/local/include/spa-0.2", "/usr/local/include/spa-0.2",
"/usr/include/spa", "/usr/include/spa",
"/usr/local/include/spa" "/usr/local/include/spa",
"/opt/crossdesk-pipewire-sdk/include/spa-0.2"
}, {system = true}) }, {system = true})
for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do
add_includedirs(include_dir, {system = true}) add_includedirs(include_dir, {system = true})
+28
View File
@@ -3,6 +3,22 @@ function setup_targets()
includes("submodules", "thirdparty") includes("submodules", "thirdparty")
local function copy_slint_runtime(target)
if not target:is_plat("windows") then
return
end
local slint = target:pkg("slint")
if not slint and target:dep("gui") then
slint = target:dep("gui"):pkg("slint")
end
assert(slint, "the Slint package is required to copy its Windows runtime")
local runtime_dir = path.join(slint:installdir(), "lib")
local runtime_dll = path.join(runtime_dir, "slint_cpp.dll")
assert(os.isfile(runtime_dll), "Slint runtime not found: " .. runtime_dll)
os.cp(runtime_dll, target:targetdir())
end
local crossdesk_windows_resource = "scripts/windows/crossdesk.rc" local crossdesk_windows_resource = "scripts/windows/crossdesk.rc"
if is_config("CROSSDESK_PORTABLE", true) then if is_config("CROSSDESK_PORTABLE", true) then
crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc" crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc"
@@ -57,6 +73,11 @@ function setup_targets()
set_default(false) set_default(false)
add_files("tests/connection_status_protocol_test.cpp") add_files("tests/connection_status_protocol_test.cpp")
target("video_callback_lifetime_test")
set_kind("binary")
set_default(false)
add_files("tests/video_callback_lifetime_test.cpp")
target("windows_manifest_resource_test") target("windows_manifest_resource_test")
set_kind("binary") set_kind("binary")
set_default(false) set_default(false)
@@ -87,10 +108,15 @@ function setup_targets()
set_kind("binary") set_kind("binary")
set_languages("c++20") set_languages("c++20")
set_default(false) set_default(false)
if is_os("windows") then
add_cxxflags("/bigobj")
end
add_packages("slint") add_packages("slint")
add_includedirs("src/gui/assets/fonts")
add_rules("slint") add_rules("slint")
add_files("src/gui/ui/crossdesk_ui.slint") add_files("src/gui/ui/crossdesk_ui.slint")
add_files("tests/slint_ui_smoke_test.cpp") add_files("tests/slint_ui_smoke_test.cpp")
after_build(copy_slint_runtime)
target("version_checker_test") target("version_checker_test")
set_kind("binary") set_kind("binary")
@@ -240,6 +266,7 @@ function setup_targets()
"src/gui/features/settings/*.cpp", "src/gui/ui/crossdesk_ui.slint") "src/gui/features/settings/*.cpp", "src/gui/ui/crossdesk_ui.slint")
add_includedirs("src/gui", {public = true}) add_includedirs("src/gui", {public = true})
if is_os("windows") then if is_os("windows") then
add_cxxflags("/bigobj")
add_files("src/gui/platform/tray/win_tray.cpp") add_files("src/gui/platform/tray/win_tray.cpp")
add_includedirs("src/service/windows", {public = true}) add_includedirs("src/service/windows", {public = true})
elseif is_os("macosx") then elseif is_os("macosx") then
@@ -295,4 +322,5 @@ function setup_targets()
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper") add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
add_files(crossdesk_windows_resource) add_files(crossdesk_windows_resource)
end end
after_build(copy_slint_runtime)
end end