Compare commits

...

9 Commits

Author SHA1 Message Date
dijunkun 6f5faa7111 fix: refine Wayland window behavior and titlebars 2026-07-23 20:40:13 +08:00
dijunkun 3a72e4849e Merge branch 'slint-client' of https://github.com/kunkundi/crossdesk into slint-client 2026-07-23 16:29:21 +08:00
dijunkun 43e44af730 fix: copy Slint runtime beside Windows binaries 2026-07-23 16:27:19 +08:00
dijunkun 97e592861c fix: remove main window maximize button 2026-07-23 16:27:11 +08:00
dijunkun 31b8983328 fix: gray out unavailable hardware codec setting 2026-07-23 11:11:08 +08:00
dijunkun 522f519d38 fix: include X11 headers for Wayland keyboard 2026-07-22 21:56:41 +08:00
dijunkun 37c4fe2e8a build: install Rust toolchain for Slint 2026-07-22 17:23:35 +08:00
dijunkun 2c20599800 fix: improve update notes display 2026-07-22 03:08:22 +08:00
dijunkun f5c4e2ba4a ci: add Linux build image workflow 2026-07-22 00:06:18 +08:00
34 changed files with 2296 additions and 629 deletions
+20
View File
@@ -0,0 +1,20 @@
**
!.dockerignore
!docker/
!docker/linux-build/
!docker/linux-build/**
# Dependency manifests and local xmake package recipes are the only project
# files needed while pre-warming the build image.
!xmake.lua
!xmake/
!xmake/**
!thirdparty/
!thirdparty/**
!submodules/
!submodules/xmake.lua
!submodules/minirtc/
!submodules/minirtc/xmake.lua
!submodules/minirtc/thirdparty/
!submodules/minirtc/thirdparty/**
+155
View File
@@ -0,0 +1,155 @@
name: Build Linux Build Image
on:
push:
branches:
- ci/linux-build-image
paths:
- .dockerignore
- .gitmodules
- .github/workflows/build-linux-image.yml
- docker/linux-build/**
- xmake.lua
- xmake/**
- thirdparty/**
- submodules/xmake.lua
- submodules/minirtc
- submodules/minirtc/xmake.lua
- submodules/minirtc/thirdparty/**
workflow_dispatch:
permissions:
contents: read
concurrency:
group: linux-build-image-${{ github.ref }}
cancel-in-progress: true
env:
IMAGE_NAME: crossdesk/ubuntu22.04
STACK_TAG: buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
XMAKE_VERSION: 3.0.9
RUST_VERSION: 1.92.0
RUSTUP_VERSION: 1.28.2
jobs:
build:
name: Build candidate (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
base_image: nvidia/cuda:12.6.3-devel-ubuntu22.04
- arch: arm64
platform: linux/arm64
runner: ubuntu-24.04-arm
base_image: ubuntu:22.04
steps:
- name: Set candidate tag
id: image
run: |
CANDIDATE_TAG="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-${{ matrix.arch }}"
echo "CANDIDATE_TAG=${CANDIDATE_TAG}" >> "${GITHUB_ENV}"
echo "candidate=${CANDIDATE_TAG}" >> "${GITHUB_OUTPUT}"
- name: Checkout source
uses: actions/checkout@v5
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push candidate
uses: docker/build-push-action@v7
with:
context: .
file: docker/linux-build/Dockerfile
platforms: ${{ matrix.platform }}
push: true
pull: true
tags: ${{ steps.image.outputs.candidate }}
cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:${{ env.STACK_TAG }}
cache-to: type=inline
provenance: false
sbom: false
build-args: |
BASE_IMAGE=${{ matrix.base_image }}
XMAKE_VERSION=${{ env.XMAKE_VERSION }}
RUST_VERSION=${{ env.RUST_VERSION }}
RUSTUP_VERSION=${{ env.RUSTUP_VERSION }}
verify:
name: Verify candidate (${{ matrix.arch }})
needs: build
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
steps:
- name: Set candidate tag
run: |
CANDIDATE_TAG="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-${{ matrix.arch }}"
echo "CANDIDATE_TAG=${CANDIDATE_TAG}" >> "${GITHUB_ENV}"
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Verify tools, architecture, and Ubuntu baseline
run: |
docker pull "${CANDIDATE_TAG}"
docker run --rm \
--env EXPECTED_ARCH="${{ matrix.arch }}" \
"${CANDIDATE_TAG}" \
verify-crossdesk-build-image
publish:
name: Publish multi-arch image
needs: verify
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Promote verified candidates
run: |
AMD64_IMAGE="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-amd64"
ARM64_IMAGE="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-arm64"
docker buildx imagetools create \
--tag "${IMAGE_NAME}:${STACK_TAG}" \
--tag "${IMAGE_NAME}:sha-${GITHUB_SHA}" \
--tag "${IMAGE_NAME}:latest" \
"${AMD64_IMAGE}" \
"${ARM64_IMAGE}"
- name: Inspect published manifest
run: |
docker buildx imagetools inspect "${IMAGE_NAME}:${STACK_TAG}"
+43 -8
View File
@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- "**" - "**"
- "!ci/linux-build-image"
tags: tags:
- "*" - "*"
workflow_dispatch: workflow_dispatch:
@@ -27,13 +28,13 @@ jobs:
matrix: matrix:
include: include:
- arch: amd64 - arch: amd64
runner: ubuntu-22.04 runner: ubuntu-24.04
image: crossdesk/ubuntu20.04:latest image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-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-22.04-arm runner: ubuntu-24.04-arm
image: crossdesk/ubuntu20.04-arm64v8:latest image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
package_script: ./scripts/linux/pkg_arm64.sh package_script: ./scripts/linux/pkg_arm64.sh
container: container:
@@ -99,12 +100,14 @@ jobs:
with: with:
submodules: recursive submodules: recursive
- name: Verify Linux build image
run: verify-crossdesk-build-image
- 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: |
apt install -y libxft-dev
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk xmake b -vy --root crossdesk
@@ -113,6 +116,14 @@ 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
run: |
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb"
VERIFY_DIR="${RUNNER_TEMP}/crossdesk-glibc-${{ matrix.arch }}"
install -d "${VERIFY_DIR}"
dpkg-deb --extract "${PACKAGE_FILE}" "${VERIFY_DIR}"
verify-crossdesk-glibc-baseline "${VERIFY_DIR}" 2.35
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v6
with: with:
@@ -123,17 +134,21 @@ 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"
strategy: strategy:
matrix: matrix:
include: include:
- arch: x64 - arch: x64
binary_arch: x86_64
runner: macos-15-intel runner: macos-15-intel
cache-key: intel cache-key: intel-macos14
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
binary_arch: arm64
runner: macos-14 runner: macos-14
cache-key: arm cache-key: arm-macos14
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 +204,9 @@ jobs:
- name: Install xmake - name: Install xmake
run: brew install xmake run: brew install xmake
- name: Update xmake repositories
run: xmake repo -u
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v5
@@ -197,7 +215,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
@@ -205,6 +223,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 }}"
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v6
with: with:
@@ -275,6 +309,7 @@ jobs:
run: | run: |
Invoke-Expression (Invoke-Webrequest 'https://raw.githubusercontent.com/tboox/xmake/master/scripts/get.ps1' -UseBasicParsing).Content Invoke-Expression (Invoke-Webrequest 'https://raw.githubusercontent.com/tboox/xmake/master/scripts/get.ps1' -UseBasicParsing).Content
echo "C:\Users\runneradmin\xmake" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append echo "C:\Users\runneradmin\xmake" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
xmake repo -u
xmake create cuda xmake create cuda
Set-Location cuda Set-Location cuda
xmake g --theme=plain xmake g --theme=plain
+153
View File
@@ -0,0 +1,153 @@
# syntax=docker/dockerfile:1.7
ARG BASE_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu22.04
FROM ${BASE_IMAGE}
ARG TARGETARCH
ARG XMAKE_VERSION=3.0.9
ARG RUST_VERSION=1.92.0
ARG RUSTUP_VERSION=1.28.2
LABEL org.opencontainers.image.title="CrossDesk Linux build environment" \
org.opencontainers.image.description="Ubuntu 22.04 build environment for CrossDesk" \
org.opencontainers.image.source="https://github.com/kunkundi/crossdesk" \
org.opencontainers.image.licenses="GPL-3.0-only"
ENV DEBIAN_FRONTEND=noninteractive \
TZ=Etc/UTC \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
RUSTUP_HOME=/opt/rustup \
CARGO_HOME=/opt/cargo \
XMAKE_GLOBALDIR=/data \
XMAKE_ROOT=y \
CUDA_PATH=/usr/local/cuda \
PATH=/root/.local/bin:/opt/cargo/bin:/usr/local/bin:${PATH}
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
autoconf \
automake \
binutils \
bison \
build-essential \
ca-certificates \
ccache \
cmake \
curl \
dbus \
dpkg-dev \
file \
flex \
gettext \
git \
libasound2-dev \
libdbus-1-dev \
libdrm-dev \
libegl1-mesa-dev \
libffi-dev \
libfontconfig1-dev \
libfreetype6-dev \
libgl1-mesa-dev \
libmount-dev \
libpulse-dev \
libreadline-dev \
libsndio-dev \
libssl-dev \
libtool \
libx11-dev \
libxcb-xfixes0-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-shm0-dev \
libxcb-xinerama0-dev \
libxcb-xkb-dev \
libxcb-xtest0-dev \
libxcursor-dev \
libxext-dev \
libxfixes-dev \
libxft-dev \
libxi-dev \
libxinerama-dev \
libxkbcommon-dev \
libxrandr-dev \
libxrender-dev \
libxtst-dev \
libxv-dev \
make \
nasm \
ninja-build \
patch \
patchelf \
perl \
pkg-config \
python3 \
python3-pip \
unzip \
xutils-dev \
xz-utils \
yasm \
zip \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
RUN export GITHUB_ACTIONS=true \
&& curl -fsSL https://xmake.io/shget.text \
| bash -s -- "v${XMAKE_VERSION}" \
&& source ~/.xmake/profile \
&& xmake --version | grep -F "v${XMAKE_VERSION}"
RUN case "${TARGETARCH}" in \
amd64) \
rustup_arch="x86_64-unknown-linux-gnu"; \
rustup_sha256="20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c" \
;; \
arm64) \
rustup_arch="aarch64-unknown-linux-gnu"; \
rustup_sha256="e3853c5a252fca15252d07cb23a1bdd9377a8c6f3efa01531109281ae47f841c" \
;; \
*) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \
esac \
&& curl --proto '=https' --tlsv1.2 --fail --show-error --location \
"https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustup_arch}/rustup-init" \
-o /tmp/rustup-init \
&& echo "${rustup_sha256} /tmp/rustup-init" | sha256sum --check - \
&& chmod 0755 /tmp/rustup-init \
&& /tmp/rustup-init -y --no-modify-path --profile minimal \
--default-toolchain "${RUST_VERSION}" \
&& rm -f /tmp/rustup-init \
&& rustc --version \
&& cargo --version
WORKDIR /tmp/crossdesk-dependencies
COPY xmake.lua ./xmake.lua
COPY xmake ./xmake
COPY thirdparty ./thirdparty
COPY submodules/xmake.lua ./submodules/xmake.lua
COPY submodules/minirtc/xmake.lua ./submodules/minirtc/xmake.lua
COPY submodules/minirtc/thirdparty ./submodules/minirtc/thirdparty
# Configure the real project manifests so the image cache stays aligned with
# CrossDesk's exact dependency graph. Source files are intentionally excluded
# from the Docker context; source-only changes therefore reuse this layer.
RUN xmake repo -u --root \
&& xmake f -m release --USE_CUDA=true --root -y \
&& rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \
&& mkdir -p /workspace
WORKDIR /workspace
COPY docker/linux-build/verify-build-image.sh \
/usr/local/bin/verify-crossdesk-build-image
COPY docker/linux-build/verify-glibc-baseline.sh \
/usr/local/bin/verify-crossdesk-glibc-baseline
RUN chmod 0755 \
/usr/local/bin/verify-crossdesk-build-image \
/usr/local/bin/verify-crossdesk-glibc-baseline \
&& verify-crossdesk-build-image
CMD ["bash"]
+41
View File
@@ -0,0 +1,41 @@
# CrossDesk Linux 构建镜像
这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake、
Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。
## 发布方式
推送影响镜像的文件到 `ci/linux-build-image` 分支时,
`build-linux-image.yml` 会在 GitHub 原生 amd64/arm64 runner 上分别构建和验证,
全部通过后再合并为同一个 multi-arch 镜像:
```text
crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
```
Docker 会根据运行机器自动选择正确架构。amd64 变体以
`nvidia/cuda:12.6.3-devel-ubuntu22.04` 为基础;arm64 变体不包含当前构建
用不到的 CUDA SDK。两个变体都使用 Ubuntu 22.04/glibc 2.35。
因此该镜像生成的 Linux 安装包最低支持 Ubuntu 22.04,不再保证兼容
Ubuntu 20.04。
每次成功发布还会生成 `sha-<完整提交 SHA>`,用于精确回滚;`latest` 和上面的
工具链版本 tag 只会在两个架构都验证成功后更新。
## GitHub 配置
在仓库的 Actions secrets 中配置:
- `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu22.04` 的 Docker Hub 用户名
- `DOCKERHUB_TOKEN`:该用户的 Docker Hub access token
工作流文件进入默认分支后,也可以从 Actions 页面手动运行。为了避免首次发布
新 tag 时应用构建先于镜像完成,常规 `build.yml` 不响应
`ci/linux-build-image` 分支的 push。第一次启用时,应先推送该分支并等待镜像
发布成功,再把应用构建工作流的修改合并到 `main`
## 更新依赖
修改 Dockerfile、xmake 清单、本地包配方或子模块中的包配方后,推送该分支即可。
升级工具链时应同时修改 Dockerfile 的版本参数、工作流中的 `STACK_TAG`,以及
常规构建工作流引用的镜像 tag。
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
EXPECTED_ARCH="${EXPECTED_ARCH:-}"
EXPECTED_XMAKE_VERSION="${EXPECTED_XMAKE_VERSION:-3.0.9}"
EXPECTED_RUST_VERSION="${EXPECTED_RUST_VERSION:-1.92.0}"
# shellcheck disable=SC1091
source /etc/os-release
if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "22.04" ]]; then
echo "Expected Ubuntu 22.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2
exit 1
fi
GLIBC_VERSION="$(getconf GNU_LIBC_VERSION | awk '{print $2}')"
if [[ "${GLIBC_VERSION}" != "2.35" ]]; then
echo "Expected glibc 2.35, found ${GLIBC_VERSION}" >&2
exit 1
fi
ACTUAL_ARCH="$(dpkg --print-architecture)"
if [[ -n "${EXPECTED_ARCH}" && "${ACTUAL_ARCH}" != "${EXPECTED_ARCH}" ]]; then
echo "Expected architecture ${EXPECTED_ARCH}, found ${ACTUAL_ARCH}" >&2
exit 1
fi
XMAKE_VERSION_OUTPUT="$(xmake --version 2>&1)"
grep -F "v${EXPECTED_XMAKE_VERSION}" <<<"${XMAKE_VERSION_OUTPUT}" >/dev/null
rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null
cargo --version >/dev/null
cmake --version >/dev/null
g++ --version >/dev/null
pkg-config --exists xft
for package_path in \
"s/slint" \
"l/libdatachannel" \
"o/openssl3" \
"s/spdlog"; do
if [[ ! -d "${XMAKE_GLOBALDIR}/.xmake/packages/${package_path}" ]]; then
echo "Prebuilt xmake package is missing: ${package_path}" >&2
exit 1
fi
done
if ! find "${XMAKE_GLOBALDIR}/.xmake/packages/s/slint" \
-type f -name 'slint-compiler*' -print -quit | grep -q .; then
echo "The prebuilt Slint compiler is missing" >&2
exit 1
fi
if [[ "${ACTUAL_ARCH}" == "amd64" && ! -f "${CUDA_PATH}/include/cuda.h" ]]; then
echo "The amd64 image is missing CUDA headers under ${CUDA_PATH}" >&2
exit 1
fi
echo "Verified CrossDesk build image: Ubuntu ${VERSION_ID}, ${ACTUAL_ARCH}, glibc ${GLIBC_VERSION}"
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "Usage: $0 <ELF-file-or-directory> [maximum-glibc-version]" >&2
exit 2
fi
INPUT_PATH="$1"
MAXIMUM_VERSION="${2:-2.35}"
verify_elf() {
local elf_file="$1"
local version=""
local highest_version="none"
local -a required_versions=()
mapfile -t required_versions < <(
readelf --version-info "${elf_file}" \
| grep -oE 'GLIBC_[0-9]+(\.[0-9]+)+' \
| sed 's/^GLIBC_//' \
| sort -Vu \
|| true
)
for version in "${required_versions[@]}"; do
if dpkg --compare-versions "${version}" gt "${MAXIMUM_VERSION}"; then
echo "${elf_file} requires GLIBC_${version}; maximum allowed is GLIBC_${MAXIMUM_VERSION}" >&2
return 1
fi
done
if [[ ${#required_versions[@]} -gt 0 ]]; then
highest_version="${required_versions[-1]}"
fi
echo "Verified ${elf_file}: highest required glibc version is ${highest_version}"
}
if [[ -f "${INPUT_PATH}" ]]; then
if ! readelf -h "${INPUT_PATH}" >/dev/null 2>&1; then
echo "Not an ELF file: ${INPUT_PATH}" >&2
exit 1
fi
verify_elf "${INPUT_PATH}"
exit 0
fi
if [[ ! -d "${INPUT_PATH}" ]]; then
echo "File or directory does not exist: ${INPUT_PATH}" >&2
exit 1
fi
ELF_COUNT=0
while IFS= read -r -d '' candidate; do
if readelf -h "${candidate}" >/dev/null 2>&1; then
verify_elf "${candidate}"
ELF_COUNT=$((ELF_COUNT + 1))
fi
done < <(find "${INPUT_PATH}" -type f -print0)
if [[ ${ELF_COUNT} -eq 0 ]]; then
echo "No ELF files found under ${INPUT_PATH}" >&2
exit 1
fi
+1 -1
View File
@@ -147,7 +147,7 @@ Version: $DEB_VERSION
Architecture: $DEBIAN_ARCH Architecture: $DEBIAN_ARCH
Maintainer: $MAINTAINER Maintainer: $MAINTAINER
Description: $DESCRIPTION Description: $DESCRIPTION
Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxext6, Depends: libc6 (>= 2.35), libstdc++6 (>= 9), 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
+99
View File
@@ -0,0 +1,99 @@
#!/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"
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
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,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;
+2 -2
View File
@@ -186,6 +186,6 @@ struct ApplicationState : MainWindowState,
ServerWindowState, ServerWindowState,
UiState {}; UiState {};
} // namespace crossdesk::gui_detail } // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_ #endif // CROSSDESK_GUI_APPLICATION_STATE_H_
File diff suppressed because it is too large Load Diff
+6
View File
@@ -36,6 +36,9 @@ private:
void SyncPlatformDialogs(); void SyncPlatformDialogs();
void SyncStreamWindow(); void SyncStreamWindow();
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 +62,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
@@ -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"访问官网: ", \
+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;
+284 -37
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,6 +276,9 @@ 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;
@@ -299,7 +315,7 @@ 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));
reset-remote-id => { reset-remote-id => {
@@ -307,9 +323,201 @@ export component MainWindow inherits Window {
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 +537,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 +983,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;
@@ -1213,8 +1421,8 @@ export component MainWindow inherits Window {
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; } 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; }
Rectangle { x: 7px; y: 147px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; } Rectangle { x: 7px; y: 147px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
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; } Text { x: 8px; y: 151px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.hardware-codec; color: root.hardware-codec-available ? #202124 : #8b8f95; 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; } ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 151px; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; }
Rectangle { x: 7px; y: 177px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; } 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; } 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; }
@@ -1341,19 +1549,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 +1572,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 +1619,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 ? 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: 283px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { 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 +1710,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;
}
}
} }
+184 -5
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,6 +156,10 @@ 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);
@@ -169,6 +176,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 +212,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 {
@@ -645,5 +812,17 @@ 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();
} }
@@ -204,4 +160,4 @@ int GuiApplication::UpdateNotificationWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
+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;
} }
+102
View File
@@ -1,25 +1,91 @@
#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());
@@ -62,7 +128,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 +167,12 @@ 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);
crossdesk::ui::FileTransferEntry transfer; crossdesk::ui::FileTransferEntry transfer;
transfer.name = "archive.zip"; transfer.name = "archive.zip";
transfer.status = "Sending"; transfer.status = "Sending";
+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()
@@ -10,17 +10,21 @@ 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")
package:add("links", "slint_cpp") if package:is_plat("windows") then
package:add("links", "slint_cpp.dll")
else
package:add("links", "slint_cpp")
end
end) end)
on_install("windows", "linux", "macosx", function(package) on_install("windows", "linux", "macosx", function(package)
local configs = { local configs = {
"-DSLINT_BUILD_TESTING=OFF", "-DSLINT_BUILD_TESTING=OFF",
"-DSLINT_BUILD_EXAMPLES=OFF", "-DSLINT_BUILD_EXAMPLES=OFF",
"-DSLINT_COMPILER=download",
"-DSLINT_FEATURE_INTERPRETER=OFF", "-DSLINT_FEATURE_INTERPRETER=OFF",
"-DSLINT_FEATURE_LIVE_PREVIEW=OFF", "-DSLINT_FEATURE_LIVE_PREVIEW=OFF",
"-DSLINT_FEATURE_TESTING=OFF", "-DSLINT_FEATURE_TESTING=OFF",
@@ -36,23 +40,6 @@ package("slint")
"-DBUILD_SHARED_LIBS=ON" "-DBUILD_SHARED_LIBS=ON"
} }
import("package.tools.cmake").install(package, configs) import("package.tools.cmake").install(package, configs)
-- SLINT_COMPILER=download only configures downstream CMake projects.
-- xmake's native Slint rule needs the matching host executable here.
import("net.http")
import("utils.archive")
local host_system = is_host("windows") and "windows" or (is_host("macosx") and "Darwin" or "Linux")
local host_arch = os.arch()
if host_arch == "x64" then
host_arch = "x86_64"
end
local suffix = host_system .. "-" .. host_arch
local archive_name = "slint-compiler-" .. suffix .. ".tar.gz"
local archive_file = path.join(package:builddir(), archive_name)
local download_url = "https://github.com/slint-ui/slint/releases/download/v" .. package:version_str() .. "/" .. archive_name
http.download(download_url, archive_file)
os.mkdir(path.join(package:installdir(), "bin"))
archive.extract(archive_file, package:installdir())
end) end)
on_test(function(package) on_test(function(package)
+1 -1
View File
@@ -1 +1 @@
includes("cpp-httplib") includes("cpp-httplib", "rust", "slint")
-2
View File
@@ -1,8 +1,6 @@
set_project("crossdesk") set_project("crossdesk")
set_license("GPL-3.0-only") set_license("GPL-3.0-only")
add_repositories("crossdesk-packages " .. path.join(os.scriptdir(), "xmake", "repository"))
includes("xmake/options.lua") includes("xmake/options.lua")
includes("xmake/platform.lua") includes("xmake/platform.lua")
includes("xmake/rules/slint.lua") includes("xmake/rules/slint.lua")
+1
View File
@@ -26,6 +26,7 @@ end
function setup_platform_settings() function setup_platform_settings()
if is_os("windows") then if is_os("windows") then
add_requires("libyuv", "miniaudio 0.11.21") add_requires("libyuv", "miniaudio 0.11.21")
add_defines("NOMINMAX")
add_links("Shell32", "dwmapi", "User32", "kernel32", add_links("Shell32", "dwmapi", "User32", "kernel32",
"gdi32", "winmm", "setupapi", "version", "gdi32", "winmm", "setupapi", "version",
"Imm32", "iphlpapi", "d3d11", "dxgi") "Imm32", "iphlpapi", "d3d11", "dxgi")
+23
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"
@@ -87,10 +103,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 +261,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 +317,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