Compare commits

..

1 Commits

Author SHA1 Message Date
dijunkun 7ff6c8a4ad ci: add Linux build image workflow 2026-07-21 22:35:02 +08:00
32 changed files with 991 additions and 2263 deletions
+3 -23
View File
@@ -134,21 +134,17 @@ jobs:
build-macos:
name: Build on macOS
runs-on: ${{ matrix.runner }}
env:
MACOSX_DEPLOYMENT_TARGET: "14.0"
strategy:
matrix:
include:
- arch: x64
binary_arch: x86_64
runner: macos-15-intel
cache-key: intel-macos14
cache-key: intel
out-dir: ./build/macosx/x86_64/release/crossdesk
package_script: ./scripts/macosx/pkg_x64.sh
- arch: arm64
binary_arch: arm64
runner: macos-14
cache-key: arm-macos14
cache-key: arm
out-dir: ./build/macosx/arm64/release/crossdesk
package_script: ./scripts/macosx/pkg_arm64.sh
@@ -215,7 +211,7 @@ jobs:
- name: Build CrossDesk
run: |
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Package CrossDesk app
@@ -223,22 +219,6 @@ jobs:
chmod +x ${{ matrix.package_script }}
${{ 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
uses: actions/upload-artifact@v6
with:
-131
View File
@@ -1,131 +0,0 @@
#!/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"
+3 -8
View File
@@ -1,16 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
#!/bin/bash
set -e
APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
PLATFORM="macos"
ARCH="arm64"
BINARY_ARCH="arm64"
IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="14.0"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MACOS_MIN_VERSION="10.12"
normalize_app_version() {
local input="$1"
@@ -97,9 +95,6 @@ EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
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."
mkdir -p build_pkg_scripts
+3 -8
View File
@@ -1,16 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
#!/bin/bash
set -e
APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
PLATFORM="macos"
ARCH="x64"
BINARY_ARCH="x86_64"
IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="14.0"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MACOS_MIN_VERSION="10.12"
normalize_app_version() {
local input="$1"
@@ -97,9 +95,6 @@ EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
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."
mkdir -p build_pkg_scripts
-99
View File
@@ -1,99 +0,0 @@
#!/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,9 +1,5 @@
#include "keyboard_capturer.h"
#include <X11/Xlib.h>
#include <X11/extensions/XTest.h>
#include <X11/keysym.h>
#include <errno.h>
#include <poll.h>
@@ -7,6 +7,10 @@
#ifndef _KEYBOARD_CAPTURER_H_
#define _KEYBOARD_CAPTURER_H_
#include <X11/Xlib.h>
#include <X11/extensions/XTest.h>
#include <X11/keysym.h>
#include <atomic>
#include <cstdint>
#include <functional>
@@ -17,7 +21,6 @@
struct DBusConnection;
struct DBusMessageIter;
struct _XDisplay;
namespace crossdesk {
@@ -45,8 +48,8 @@ class KeyboardCapturer : public DeviceController {
append_args);
private:
_XDisplay* display_;
unsigned long root_;
Display* display_;
Window root_;
std::atomic<bool> running_;
std::thread event_thread_;
bool use_wayland_portal_ = false;
@@ -5,9 +5,6 @@
#include <map>
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <dbus/dbus.h>
#endif
@@ -1,9 +1,6 @@
#include "mouse_controller.h"
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/XTest.h>
#include <unistd.h>
#include "platform.h"
#include "rd_log.h"
@@ -7,6 +7,10 @@
#ifndef _MOUSE_CONTROLLER_H_
#define _MOUSE_CONTROLLER_H_
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <unistd.h>
#include <cstdint>
#include <functional>
#include <string>
@@ -16,7 +20,6 @@
struct DBusConnection;
struct DBusMessageIter;
struct _XDisplay;
namespace crossdesk {
@@ -50,8 +53,8 @@ class MouseController : public DeviceController {
enum class WaylandAbsoluteMode { kUnknown, kPixels, kNormalized, kDisabled };
_XDisplay* display_ = nullptr;
unsigned long root_ = 0;
Display* display_ = nullptr;
Window root_ = 0;
std::vector<DisplayInfo> display_info_list_;
int screen_width_ = 0;
int screen_height_ = 0;
+2 -6
View File
@@ -1,7 +1,6 @@
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
#define CROSSDESK_GUI_APPLICATION_STATE_H_
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
@@ -120,11 +119,8 @@ struct StreamWindowState {
};
struct ServerWindowState {
// Transport callbacks run outside the UI thread. Keep the two lifecycle
// 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 need_to_create_server_window_ = false;
bool need_to_destroy_server_window_ = false;
bool server_window_created_ = false;
bool server_window_inited_ = false;
int server_window_width_default_ = 250;
File diff suppressed because it is too large Load Diff
-6
View File
@@ -36,9 +36,6 @@ private:
void SyncPlatformDialogs();
void SyncStreamWindow();
void SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
void SyncXWaylandWindowActivation();
#endif
void UpdateLocalization();
void ResetSettingsUi();
void SaveSettingsFromUi();
@@ -62,9 +59,6 @@ private:
bool OpenUrl(const std::string &url);
std::unique_ptr<SlintUi> ui_;
#if defined(__linux__) && !defined(__APPLE__)
bool use_xwayland_gui_ = false;
#endif
};
} // namespace crossdesk
+4 -2
View File
@@ -220,8 +220,10 @@ int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
return -1;
}
// SDL represents key auto-repeat as additional key-down events. Forward
// them so the fallback path has the same long-press behavior as native hooks.
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) {
return 0;
}
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
if (key_code < 0) {
return 0;
@@ -179,7 +179,6 @@ struct TranslationRow {
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
X(access_website, u8"访问官网: ", \
+9 -61
View File
@@ -1,3 +1,5 @@
#include "runtime/gui_runtime.h"
#include <algorithm>
#include <chrono>
#include <cstring>
@@ -10,7 +12,6 @@
#include "localization.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
@@ -160,63 +161,6 @@ void GuiRuntime::HandleConnectionTimeouts() {
}
}
void GuiRuntime::HandleServerControllerDisconnected(
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) {
@@ -356,7 +300,8 @@ void GuiRuntime::ResetRemoteSessionResources(
}
}
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
std::shared_ptr<GuiRuntime::RemoteSession>
GuiRuntime::FindRemoteSession(
const std::string &remote_id) {
if (remote_id.empty()) {
return nullptr;
@@ -370,6 +315,7 @@ std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
return it->second;
}
int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
bool remember_password, bool bypass_presence_check) {
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
@@ -390,7 +336,8 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
focused_remote_id_ = remote_id;
// std::shared_lock shared_lock(remote_sessions_mutex_);
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
bool exists =
(remote_sessions_.find(remote_id) != remote_sessions_.end());
// shared_lock.unlock();
if (!exists) {
@@ -400,7 +347,8 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
{
// std::unique_lock unique_lock(remote_sessions_mutex_);
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
remote_sessions_[remote_id] =
std::make_shared<RemoteSession>();
auto props = remote_sessions_[remote_id];
props->local_id_ = "C-" + std::string(client_id_);
props->remote_id_ = remote_id;
+7 -7
View File
@@ -8,9 +8,9 @@
#include "features/devices/session_device_manager.h"
#include "features/file_transfer/file_transfer_manager.h"
#include "features/input/keyboard_controller.h"
#include "features/settings/settings_manager.h"
#include "runtime/gui_state.h"
#include "runtime/peer_event_handler.h"
#include "features/settings/settings_manager.h"
namespace crossdesk {
@@ -46,21 +46,21 @@ class GuiRuntime : protected gui_detail::GuiState {
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandleConnectionTimeouts();
void HandleServerControllerDisconnected(const std::string& remote_id,
const char* reason);
void HandleWindowsServiceIntegration();
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions();
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
void ResetRemoteSessionResources(
std::shared_ptr<RemoteSession> props);
void WaitForThumbnailSaveTasks();
std::shared_ptr<RemoteSession> FindRemoteSession(
const std::string& remote_id);
std::shared_ptr<RemoteSession>
FindRemoteSession(const std::string &remote_id);
void ResetRemoteServiceStatus(RemoteSession &props);
void ApplyRemoteServiceStatus(RemoteSession &props,
const ServiceStatus &status);
RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
RemoteUnlockState
GetRemoteUnlockState(const RemoteSession &props) const;
#if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas);
#endif
+71 -24
View File
@@ -88,6 +88,7 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
}
}
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id,
size_t user_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
@@ -154,7 +155,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) return;
if (!runtime)
return;
std::string remote_id(user_id, user_id_size);
std::shared_ptr<GuiRuntime::RemoteSession> props;
@@ -200,13 +202,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.display_list
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
@@ -215,8 +215,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::string host_name = GetHostName();
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_size = host_name.size();
@@ -316,13 +315,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.display_list
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
@@ -331,22 +328,17 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::string host_name = GetHostName();
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_size = host_name.size();
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());
remote_action_codec::Free(remote_action);
}
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->need_to_create_server_window_ = true;
runtime->is_server_mode_ = true;
runtime->start_screen_capturer_ = true;
runtime->start_speaker_capturer_ = true;
@@ -355,8 +347,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
{
std::shared_lock lock(runtime->connection_status_mutex_);
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;
})) {
runtime->show_cursor_ = true;
@@ -368,8 +359,64 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->HandleServerControllerDisconnected(remote_id,
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
"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;
}
default:
+1 -1
View File
@@ -295,7 +295,7 @@ export component ImGuiCheckBox inherits Rectangle {
border-radius: 4px;
if root.checked: Text {
text: FontAwesomeIcons.check;
color: root.enabled ? #2e86de : #8b8f95;
color: #2e86de;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 16px;
+86 -369
View File
@@ -9,11 +9,6 @@ export struct RecentConnection {
thumbnail: image,
}
export struct ReleaseNoteBlock {
content: styled-text,
section-gap: bool,
}
export global UiStrings {
in-out property <string> local-desktop: "Local Desktop";
in-out property <string> remote-desktop: "Remote Desktop";
@@ -53,8 +48,8 @@ export global UiStrings {
in-out property <string> server-port: "Signal port";
in-out property <string> coturn-port: "TURN port";
in-out property <string> version: "Version";
in-out property <string> copyright: "© 2026 by JUNKUN DI. All right reserved.";
in-out property <string> license: "Licensed under GNU GPL v3.";
in-out property <string> copyright: "Copyright © CrossDesk contributors";
in-out property <string> license: "Licensed under GPL-3.0-only";
in-out property <string> signal-connected: "Signal server connected";
in-out property <string> signal-disconnected: "Signal server disconnected";
in-out property <string> tls-error: "Signal server TLS certificate error";
@@ -184,21 +179,15 @@ component ConnectionDialogButton inherits Rectangle {
export component MainWindow inherits Window {
title: "CrossDesk";
preferred-width: 640px;
preferred-height: 450px + root.titlebar-height;
preferred-height: 450px;
min-width: 640px;
max-width: 640px;
min-height: 450px + root.titlebar-height;
max-height: 450px + root.titlebar-height;
no-frame: root.custom-titlebar;
background: root.wayland-titlebar ? transparent : white;
min-height: 450px;
max-height: 450px;
no-frame: false;
background: white;
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-password: "";
in-out property <string> remote-id-input: "";
@@ -210,7 +199,7 @@ export component MainWindow inherits Window {
in property <string> current-version: "";
in property <string> latest-version: "";
in property <string> release-name: "";
in property <[ReleaseNoteBlock]> release-note-blocks;
in property <string> release-notes: "";
in property <string> release-date: "";
in-out property <string> offline-warning: "";
in property <bool> connection-dialog-open: false;
@@ -248,9 +237,7 @@ export component MainWindow inherits Window {
in-out property <string> server-port: "";
in-out property <string> coturn-port: "";
in property <bool> settings-session-active: false;
// Keep the setting visible on every platform. Unsupported builds expose it
// as disabled so the fixed settings layout never turns into a blank row.
in property <bool> hardware-codec-available: true;
in property <bool> hardware-codec-visible: true;
callback copy-local-id;
callback toggle-password-visibility;
@@ -276,13 +263,9 @@ export component MainWindow inherits Window {
callback cancel-portable-service;
callback acknowledge-portable-service;
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;
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> about-open: false;
in-out property <bool> update-open: root.update-available;
@@ -316,216 +299,17 @@ export component MainWindow inherits Window {
? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px
: root.recent-tooltip-pointer-x + 12px));
private property <length> recent-tooltip-y: min(
root.height - root.titlebar-height - root.recent-tooltip-height - 8px,
root.height - 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 => {
root.remote-id-input = "";
remote-id-editor.focus();
}
window-surface := Rectangle {
Rectangle {
width: root.width;
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;
border-width: 0px;
border-radius: 0px;
@@ -545,8 +329,8 @@ export component MainWindow inherits Window {
z: 7;
}
// App actions live in the content header; window controls and dragging
// remain in the native or Windows-specific title bar above it.
// App actions live in the content header; native window controls and
// dragging are provided by the system title bar.
menu-button := IconButton {
x: parent.width - 40px;
y: 4px;
@@ -991,8 +775,8 @@ export component MainWindow inherits Window {
if root.menu-open: TouchArea {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
width: root.width;
height: root.height;
z: 9;
clicked => {
root.menu-open = false;
@@ -1396,9 +1180,9 @@ export component MainWindow inherits Window {
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: root.compact-language ? 219px : 190px;
y: 60px;
y: 9px;
width: root.compact-language ? 202px : 269px;
height: 330px;
height: 432px;
border-radius: 7px;
Rectangle {
@@ -1413,86 +1197,55 @@ export component MainWindow inherits Window {
Text { x: 6px; text: UiStrings.settings; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
}
settings-scroll := ScrollView {
x: 0px;
y: 21px;
width: parent.width;
height: 265px;
viewport-y <=> root.settings-scroll-y;
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; }
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 30px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
settings-content := VerticalLayout {
// Reserve space for the overlaid vertical scrollbar.
width: settings-scroll.width;
padding-top: 8px;
padding-bottom: 8px;
spacing: 0px;
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; }
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; }
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Rectangle {
height: 30px;
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: 91px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
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; }
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Rectangle { x: 7px; y: 286px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
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; } }
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; } }
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; }
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; }
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; } }
}
}
@@ -1531,6 +1284,7 @@ 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; }
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; }
Rectangle {
x: root.compact-language ? 100px : 173px;
@@ -1544,6 +1298,7 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
}
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; }
Rectangle {
x: root.compact-language ? 100px : 173px;
@@ -1557,6 +1312,7 @@ 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; }
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 ? 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; } }
}
@@ -1585,19 +1341,19 @@ export component MainWindow inherits Window {
Rectangle { y: parent.height - 7px; height: 7px; background: #d3d3d3; }
Text { x: 6px; text: UiStrings.about; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
}
Text { x: 30px; y: 40px; width: 240px; height: 20px; text: UiStrings.version + ": CrossDesk " + root.current-version; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
if root.update-available: Text { x: 30px; y: 60px; width: 240px; height: 20px; text: UiStrings.update-available + ":"; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
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; }
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: Rectangle {
x: 30px;
y: 60px;
y: 70px;
width: 240px;
height: 20px;
version-link := Text { text: root.latest-version; color: version-touch.has-hover ? #174ea6 : #334fd1; font-size: ImGuiFontStyle.body; horizontal-alignment: center; vertical-alignment: center; }
version-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
}
Text { x: 30px; y: 85px; width: 240px; height: 20px; text: UiStrings.copyright; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
Text { x: 30px; y: 105px; width: 240px; height: 20px; text: UiStrings.license; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
CompactButton { x: 133px; y: 145px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
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: 118px; 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; } }
}
}
@@ -1608,43 +1364,24 @@ export component MainWindow inherits Window {
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: 120px;
y: 52px;
y: 65px;
width: 400px;
height: 346px;
height: 320px;
border-radius: 7px;
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; }
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; }
Rectangle {
x: 30px;
x: 40px;
y: 47px;
width: 320px;
height: 22px;
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;
}
Text { text: UiStrings.access-website + "https://crossdesk.cn"; color: website-touch.has-hover ? #174ea6 : #334fd1; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
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 {
x: 30px;
y: 105px;
x: 20px;
y: 74px;
width: 360px;
height: 190px;
height: 200px;
background: white;
border-width: 1px;
border-color: #b9bdc3;
@@ -1655,24 +1392,19 @@ export component MainWindow inherits Window {
height: parent.height - 2px;
VerticalLayout {
width: 340px;
padding-left: 8px;
padding-left: 19px;
padding-right: 8px;
padding-top: 8px;
padding-bottom: 8px;
spacing: 7px;
VerticalLayout {
width: 312px;
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; }
if root.release-name != "": Text { width: 312px; text: root.release-name; color: #202124; font-size: ImGuiFontStyle.body; wrap: word-wrap; }
Text { width: 312px; text: root.release-notes; color: #202124; font-size: ImGuiFontStyle.body; wrap: word-wrap; }
if root.release-date != "": Text { width: 312px; text: UiStrings.release-date-label + root.release-date; color: #4f555e; font-size: ImGuiFontStyle.small; wrap: word-wrap; }
}
}
}
}
}
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; } }
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; } }
}
}
@@ -1747,18 +1479,3 @@ 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;
}
}
}
+6 -188
View File
@@ -112,16 +112,13 @@ component ControlBarButton inherits Rectangle {
export component StreamWindow inherits Window {
title: "CrossDesk";
preferred-width: 1280px;
preferred-height: 720px + (root.custom-titlebar ? 32px : 0px);
preferred-height: 720px;
min-width: 640px;
min-height: 360px + (root.custom-titlebar ? 32px : 0px);
no-frame: root.custom-titlebar;
background: root.custom-titlebar ? transparent : black;
min-height: 360px;
no-frame: false;
background: black;
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-out property <int> selected-tab: 0;
in property <image> frame;
@@ -156,10 +153,6 @@ export component StreamWindow inherits Window {
callback toggle-stats;
callback toggle-fullscreen;
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 scroll-input(length, length, length, length);
callback key-input(string, bool, bool, bool, bool, bool);
@@ -176,9 +169,6 @@ export component StreamWindow inherits Window {
private property <length> control-press-x: 0px;
private property <length> control-press-y: 0px;
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 move-control-drag(length, length);
callback end-control-drag;
@@ -212,167 +202,10 @@ export component StreamWindow inherits Window {
}
}
window-surface := Rectangle {
Rectangle {
width: root.width;
height: root.height;
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.
Rectangle {
@@ -508,10 +341,7 @@ export component StreamWindow inherits Window {
&& (event.kind == PointerEventKind.up
|| event.kind == PointerEventKind.cancel) {
root.end-control-drag();
// 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 {
} else if !root.control-dragging {
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
}
}
@@ -816,16 +646,4 @@ 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,3 +1,4 @@
#include <algorithm>
#include <string>
#include "application/gui_application.h"
@@ -7,6 +8,48 @@
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() {
if (show_update_notification_window_ && update_available_) {
const ImGuiViewport *viewport = ImGui::GetMainViewport();
@@ -99,7 +142,8 @@ int GuiApplication::UpdateNotificationWindow() {
// release notes
if (!release_notes_.empty()) {
ImGui::SetCursorPosX(update_notification_window_width * 0.05f);
ImGui::TextWrapped("%s", release_notes_.c_str());
std::string cleaned_notes = CleanMarkdown(release_notes_);
ImGui::TextWrapped("%s", cleaned_notes.c_str());
ImGui::Spacing();
}
+5 -6
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,
fit_w, fit_h);
std::memset(dst_rgba, 0, dst_w * dst_h * 4);
memset(dst_rgba, 0, dst_w * dst_h * 4);
for (int i = 0; i < dst_w * dst_h; ++i) {
dst_rgba[i * 4 + 3] = static_cast<char>(0xFF);
}
@@ -65,8 +65,7 @@ void ScaleNv12ToABGR(char* src, int src_w, int src_h, int dst_w, int dst_h,
for (int row = 0; row < fit_h; ++row) {
int dst_offset =
((row + (dst_h - fit_h) / 2) * dst_w + (dst_w - fit_w) / 2) * 4;
std::memcpy(dst_rgba + dst_offset, abgr.data() + row * fit_w * 4,
fit_w * 4);
memcpy(dst_rgba + dst_offset, abgr.data() + row * fit_w * 4, fit_w * 4);
}
}
@@ -86,8 +85,8 @@ Thumbnail::Thumbnail(std::string save_path, unsigned char* aes128_key,
save_path_ = save_path;
}
std::memcpy(aes128_key_, aes128_key, sizeof(aes128_key_));
std::memcpy(aes128_iv_, aes128_iv, sizeof(aes128_iv_));
memcpy(aes128_key_, aes128_key, sizeof(aes128_key_));
memcpy(aes128_iv_, aes128_iv, sizeof(aes128_iv_));
std::filesystem::create_directories(save_path_);
}
@@ -117,7 +116,7 @@ int Thumbnail::SaveToThumbnail(const char* yuv420p, int width, int height,
thumbnail_height_, rgba_buffer_);
} else {
// If yuv420p is null, fill the buffer with black pixels
std::memset(rgba_buffer_, 0x00, thumbnail_width_ * thumbnail_height_ * 4);
memset(rgba_buffer_, 0x00, thumbnail_width_ * thumbnail_height_ * 4);
for (int i = 0; i < thumbnail_width_ * thumbnail_height_; ++i) {
// Set alpha channel to opaque
rgba_buffer_[i * 4 + 3] = static_cast<char>(0xFF);
+4 -5
View File
@@ -7,7 +7,6 @@
#ifndef _THUMBNAIL_H_
#define _THUMBNAIL_H_
#include <cstring>
#include <filesystem>
#include <map>
#include <unordered_map>
@@ -53,18 +52,18 @@ class Thumbnail {
int DeleteAllFilesInDirectory();
int GetKey(unsigned char* aes128_key) {
std::memcpy(aes128_key, aes128_key_, sizeof(aes128_key_));
memcpy(aes128_key, aes128_key_, sizeof(aes128_key_));
return sizeof(aes128_key_);
}
int GetIv(unsigned char* aes128_iv) {
std::memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_));
memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_));
return sizeof(aes128_iv_);
}
int GetKeyAndIv(unsigned char* aes128_key, unsigned char* aes128_iv) {
std::memcpy(aes128_key, aes128_key_, sizeof(aes128_key_));
std::memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_));
memcpy(aes128_key, aes128_key_, sizeof(aes128_key_));
memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_));
return 0;
}
-17
View File
@@ -73,10 +73,6 @@ int main() {
ReadFile(repo_root / "src/gui/runtime/remote_session.h");
const std::string 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;
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
@@ -85,11 +81,6 @@ int main() {
"\"Remote unavailable\"");
ok &= ExpectContains("peer_connection.cpp", peer_connection,
"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,
"\"Device offline\"");
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
@@ -114,14 +105,6 @@ int main() {
"kConnectionAttemptTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"connection_attempt_started_at_");
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;
}
+18 -26
View File
@@ -84,16 +84,14 @@ int main() {
"root.shortcut-menu-open = false;");
ok &= ExpectContains("stream_window.slint", stream,
"if root.display-menu-open: Rectangle");
ok &=
ExpectContains("stream_window.slint", stream, "display-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream,
"display-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream,
"root.switch-display(index)");
ok &= ExpectContains("stream_window.slint", stream,
"if root.shortcut-menu-open: Rectangle");
ok &=
ExpectContains("stream_window.slint", stream, "shortcut-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream,
"} else if !root.control-dragging && !over-control {");
"shortcut-touch.has-hover");
ok &= ExpectContains("common.slint", common,
"if touch.has-hover && root.tooltip != \"\": Rectangle");
@@ -102,38 +100,32 @@ int main() {
"tooltip: StreamStrings.select-display");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.send-shortcut");
ok &=
ExpectContains("stream_window.slint", stream,
"root.mouse-control-enabled ? StreamStrings.release-mouse "
": StreamStrings.control-mouse");
ok &= 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");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.select-file");
ok &= ExpectContains("stream_window.slint", stream,
"root.stats-visible ? StreamStrings.hide-stats : "
"StreamStrings.show-stats");
ok &=
ExpectContains("stream_window.slint", stream,
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen "
": StreamStrings.fullscreen");
"root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats");
ok &= ExpectContains("stream_window.slint", stream,
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.disconnect");
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, "set_select_display(");
ok &= ExpectContains("gui_application.cpp", application, "set_show_stats(");
ok &=
ExpectContains("gui_application.cpp", application, "set_expand_control(");
ok &= ExpectContains("gui_application.cpp", application,
"set_select_display(");
ok &= ExpectContains("gui_application.cpp", application,
"set_show_stats(");
ok &= ExpectContains("gui_application.cpp", application,
"set_expand_control(");
ok &= ExpectContains("gui_application.cpp", application,
"set_collapse_control(");
ok &= ExpectContains("gui_application.cpp", application,
"(*ui_->stream)->global<ui::StreamStrings>()");
ok &=
ExpectNotContains("gui_application.cpp", application, "#include <imgui");
ok &= ExpectNotContains("gui_application.cpp", application,
"#include <imgui");
return ok ? 0 : 1;
}
-116
View File
@@ -1,109 +1,29 @@
#include "crossdesk_ui.h"
#include "fa_solid_900.h"
#include <cassert>
#include <cstdlib>
#include <fstream>
#include <memory>
#include <string>
#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() {
auto window = crossdesk::ui::MainWindow::create();
auto stream = crossdesk::ui::StreamWindow::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_password("123456");
window->set_connection_dialog_open(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_hardware_codec_available(false);
window->set_portable_service_settings_visible(true);
assert(std::string(window->get_local_id()) == "123 456 789");
assert(window->get_connection_dialog_open());
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_hardware_codec_available());
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_about_open(true);
assert(window->get_settings_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->invoke_reset_remote_id();
assert(std::string(window->get_remote_id_input()).empty());
@@ -142,37 +62,7 @@ int main() {
stream->set_tabs(
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
std::vector{tab}));
stream->set_custom_titlebar(true);
stream->set_window_maximized(true);
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;
stream->on_reorder_tab([&](int index, float x, float width) {
@@ -181,12 +71,6 @@ int main() {
stream->invoke_reorder_tab(0, 120.0f, 110.0f);
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;
transfer.name = "archive.zip";
transfer.status = "Sending";
-35
View File
@@ -1,35 +0,0 @@
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,7 +10,6 @@ package("slint")
add_configs("shared", {description = "Build the Slint runtime as a shared library", default = true, type = "boolean", readonly = true})
add_deps("cmake")
add_deps("rust 1.92.0", {host = true, private = true, system = false})
on_load(function(package)
package:add("includedirs", "include/slint")
+1 -1
View File
@@ -1 +1 @@
includes("cpp-httplib", "rust", "slint")
includes("cpp-httplib", "slint")
-23
View File
@@ -3,22 +3,6 @@ function setup_targets()
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"
if is_config("CROSSDESK_PORTABLE", true) then
crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc"
@@ -103,15 +87,10 @@ function setup_targets()
set_kind("binary")
set_languages("c++20")
set_default(false)
if is_os("windows") then
add_cxxflags("/bigobj")
end
add_packages("slint")
add_includedirs("src/gui/assets/fonts")
add_rules("slint")
add_files("src/gui/ui/crossdesk_ui.slint")
add_files("tests/slint_ui_smoke_test.cpp")
after_build(copy_slint_runtime)
target("version_checker_test")
set_kind("binary")
@@ -261,7 +240,6 @@ function setup_targets()
"src/gui/features/settings/*.cpp", "src/gui/ui/crossdesk_ui.slint")
add_includedirs("src/gui", {public = true})
if is_os("windows") then
add_cxxflags("/bigobj")
add_files("src/gui/platform/tray/win_tray.cpp")
add_includedirs("src/service/windows", {public = true})
elseif is_os("macosx") then
@@ -317,5 +295,4 @@ function setup_targets()
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
add_files(crossdesk_windows_resource)
end
after_build(copy_slint_runtime)
end