Compare commits

...
22 Commits
Author SHA1 Message Date
dijunkun d32974dc18 ci: add Linux capture build dependencies 2026-07-28 17:58:39 +08:00
dijunkun efd8b7c0ff fix: let ICE drive connection failure state 2026-07-28 17:43:25 +08:00
dijunkun 383452ed3b ci: enable Wayland for Linux release builds 2026-07-28 17:25:49 +08:00
dijunkun 2570e57958 ci: use macOS 26 SDK for release artifacts 2026-07-28 16:29:32 +08:00
dijunkun eed90dad34 revert: keep macOS main window titlebar opaque 2026-07-28 16:29:32 +08:00
dijunkun c53ce2a581 fix: prevent video callback crash during transport teardown 2026-07-28 15:26:13 +08:00
dijunkun 708c39bb99 fix: keep macOS main window titlebar opaque 2026-07-28 14:42:06 +08:00
dijunkun cee249a25b fix: align DTLS handling with SRTP negotiation 2026-07-28 14:11:17 +08:00
dijunkun 7c3b13409c fix: restore remote keyboard control in Slint client, refs #91 2026-07-28 14:11:16 +08:00
dijunkun e3d15f8f9a fix: include platform header for Linux build 2026-07-27 10:18:48 +08:00
dijunkun a6e8038286 fix: block remote mouse input over control bar 2026-07-27 00:30:48 +08:00
dijunkun 765452e3c9 fix: close server window after controller disconnects 2026-07-27 00:22:33 +08:00
dijunkun 7cd0d6a351 fix: support SDL keyboard auto-repeat 2026-07-26 18:11:41 +08:00
dijunkun c01b52eac5 fix: refine settings dialog scrolling layout 2026-07-26 17:52:23 +08:00
dijunkun 737cf9b558 fix: prioritize online recent connections 2026-07-26 16:49:03 +08:00
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
32 changed files with 1737 additions and 516 deletions
+36 -8
View File
@@ -103,12 +103,29 @@ jobs:
- name: Verify Linux build image - name: Verify Linux build image
run: verify-crossdesk-build-image run: verify-crossdesk-build-image
- name: Ensure Wayland build dependencies
shell: bash
run: |
if pkg-config --exists dbus-1 libdrm libpipewire-0.3 libspa-0.2; then
exit 0
fi
apt-get update
apt-get install -y --no-install-recommends \
libdbus-1-dev \
libdrm-dev \
libpipewire-0.3-dev \
libspa-0.2-dev
rm -rf /var/lib/apt/lists/*
pkg-config --exists dbus-1 libdrm libpipewire-0.3 libspa-0.2
- name: Build CrossDesk - name: Build CrossDesk
env: env:
CUDA_PATH: /usr/local/cuda CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data XMAKE_GLOBALDIR: /data
run: | run: |
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --USE_WAYLAND=true --root -y
xmake b -vy --root crossdesk xmake b -vy --root crossdesk
- name: Package - name: Package
@@ -136,19 +153,20 @@ jobs:
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
env: env:
MACOSX_DEPLOYMENT_TARGET: "14.0" MACOSX_DEPLOYMENT_TARGET: "14.0"
EXPECTED_MACOS_SDK: "26.5"
strategy: strategy:
matrix: matrix:
include: include:
- arch: x64 - arch: x64
binary_arch: x86_64 binary_arch: x86_64
runner: macos-15-intel runner: macos-26-intel
cache-key: intel-macos14 cache-key: intel-macos26-xcode26.6-min14
out-dir: ./build/macosx/x86_64/release/crossdesk out-dir: ./build/macosx/x86_64/release/crossdesk
package_script: ./scripts/macosx/pkg_x64.sh package_script: ./scripts/macosx/pkg_x64.sh
- arch: arm64 - arch: arm64
binary_arch: arm64 binary_arch: arm64
runner: macos-14 runner: macos-26
cache-key: arm-macos14 cache-key: arm-macos26-xcode26.6-min14
out-dir: ./build/macosx/arm64/release/crossdesk out-dir: ./build/macosx/arm64/release/crossdesk
package_script: ./scripts/macosx/pkg_arm64.sh package_script: ./scripts/macosx/pkg_arm64.sh
@@ -193,6 +211,17 @@ jobs:
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
- name: Select Xcode 26.6
shell: bash
run: |
sudo xcode-select -s /Applications/Xcode_26.6.app/Contents/Developer
xcodebuild -version
SDK_VERSION="$(xcrun --sdk macosx --show-sdk-version)"
if [[ "${SDK_VERSION}" != "${EXPECTED_MACOS_SDK}" ]]; then
echo "Unexpected macOS SDK: expected ${EXPECTED_MACOS_SDK}, got ${SDK_VERSION}" >&2
exit 1
fi
- name: Cache xmake dependencies - name: Cache xmake dependencies
uses: actions/cache@v5 uses: actions/cache@v5
with: with:
@@ -237,7 +266,7 @@ jobs:
exit 1 exit 1
fi fi
./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" ./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" "${EXPECTED_MACOS_SDK}"
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v6
@@ -424,8 +453,7 @@ jobs:
release: release:
name: Publish Release name: Publish Release
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
needs: needs: [build-linux, build-macos, build-windows-x64]
[build-linux, build-macos, build-windows-x64]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
+3 -1
View File
@@ -52,9 +52,11 @@ RUN apt-get update \
libfreetype6-dev \ libfreetype6-dev \
libgl1-mesa-dev \ libgl1-mesa-dev \
libmount-dev \ libmount-dev \
libpipewire-0.3-dev \
libpulse-dev \ libpulse-dev \
libreadline-dev \ libreadline-dev \
libsndio-dev \ libsndio-dev \
libspa-0.2-dev \
libssl-dev \ libssl-dev \
libtool \ libtool \
libx11-dev \ libx11-dev \
@@ -134,7 +136,7 @@ COPY submodules/minirtc/thirdparty ./submodules/minirtc/thirdparty
# CrossDesk's exact dependency graph. Source files are intentionally excluded # CrossDesk's exact dependency graph. Source files are intentionally excluded
# from the Docker context; source-only changes therefore reuse this layer. # from the Docker context; source-only changes therefore reuse this layer.
RUN xmake repo -u --root \ RUN xmake repo -u --root \
&& xmake f -m release --USE_CUDA=true --root -y \ && xmake f -m release --USE_CUDA=true --USE_WAYLAND=true --root -y \
&& rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \ && rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \
&& mkdir -p /workspace && mkdir -p /workspace
+2 -1
View File
@@ -1,7 +1,8 @@
# CrossDesk Linux 构建镜像 # CrossDesk Linux 构建镜像
这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake、 这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake、
Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。 Rust、Linux 开发库(包括 D-Bus、DRM、PipeWire 和 SPA以及根据项目
`xmake.lua` 提前编译好的依赖。
## 发布方式 ## 发布方式
+11 -1
View File
@@ -30,7 +30,17 @@ rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null
cargo --version >/dev/null cargo --version >/dev/null
cmake --version >/dev/null cmake --version >/dev/null
g++ --version >/dev/null g++ --version >/dev/null
pkg-config --exists xft for pkg_config_package in \
dbus-1 \
libdrm \
libpipewire-0.3 \
libspa-0.2 \
xft; do
if ! pkg-config --exists "${pkg_config_package}"; then
echo "Required pkg-config package is missing: ${pkg_config_package}" >&2
exit 1
fi
done
for package_path in \ for package_path in \
"s/slint" \ "s/slint" \
+20 -2
View File
@@ -1,13 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
if [[ $# -ne 2 ]]; then if [[ $# -lt 2 || $# -gt 3 ]]; then
echo "Usage: $0 <app-bundle> <expected-architecture>" >&2 echo "Usage: $0 <app-bundle> <expected-architecture> [expected-sdk-version]" >&2
exit 2 exit 2
fi fi
APP_BUNDLE="$1" APP_BUNDLE="$1"
EXPECTED_ARCH="$2" EXPECTED_ARCH="$2"
EXPECTED_SDK_VERSION="${3:-}"
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk" APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks" FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
INFO_PLIST="$APP_BUNDLE/Contents/Info.plist" INFO_PLIST="$APP_BUNDLE/Contents/Info.plist"
@@ -94,6 +95,23 @@ if [[ -z "$BINARY_MIN_VERSION" || "$PLIST_MIN_VERSION" != "$BINARY_MIN_VERSION"
exit 1 exit 1
fi fi
if [[ -n "$EXPECTED_SDK_VERSION" ]]; then
verify_sdk_version() {
local macho="$1"
local actual_sdk=""
actual_sdk="$(otool -l "$macho" | awk '
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); next }
build_version && $1 == "sdk" { print $2; exit }')"
if [[ "$actual_sdk" != "$EXPECTED_SDK_VERSION" ]]; then
echo "Unexpected macOS SDK in $macho: expected $EXPECTED_SDK_VERSION, got ${actual_sdk:-unknown}" >&2
return 1
fi
}
verify_sdk_version "$APP_EXECUTABLE"
verify_sdk_version "$SLINT_LIBRARY"
fi
codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE" codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"
echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)" echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)"
@@ -5,6 +5,9 @@
#include <map> #include <map>
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER #if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <dbus/dbus.h> #include <dbus/dbus.h>
#endif #endif
@@ -1,5 +1,6 @@
#include "keyboard_capturer.h" #include "keyboard_capturer.h"
#include <cstdint>
#include <unordered_map> #include <unordered_map>
#include "keyboard_converter.h" #include "keyboard_converter.h"
@@ -10,6 +11,7 @@ namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr; static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr; static void* g_user_ptr = nullptr;
static std::unordered_map<int, int> g_unmapped_keycode_to_vk; static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
constexpr int64_t kCrossDeskInjectedKeyboardEvent = 0x43524f5353444553;
static int VkCodeFromUnicode(UniChar ch) { static int VkCodeFromUnicode(UniChar ch) {
if (ch >= 'a' && ch <= 'z') { if (ch >= 'a' && ch <= 'z') {
@@ -103,6 +105,10 @@ static int ResolveVkCodeFromMacEvent(CGEventRef event, CGKeyCode key_code,
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
CGEventRef event, void* userInfo) { CGEventRef event, void* userInfo) {
(void)proxy; (void)proxy;
if (CGEventGetIntegerValueField(event, kCGEventSourceUserData) ==
kCrossDeskInjectedKeyboardEvent) {
return event;
}
if (!g_on_key_action) { if (!g_on_key_action) {
return event; return event;
} }
@@ -302,6 +308,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
} }
CGEventSetFlags(event, ToCGEventFlags(injected_flags)); CGEventSetFlags(event, ToCGEventFlags(injected_flags));
CGEventSetIntegerValueField(event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, event); CGEventPost(kCGHIDEventTap, event);
CFRelease(event); CFRelease(event);
@@ -314,6 +322,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release"); LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
return -1; return -1;
} }
CGEventSetIntegerValueField(fn_release_event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, fn_release_event); CGEventPost(kCGHIDEventTap, fn_release_event);
CFRelease(fn_release_event); CFRelease(fn_release_event);
} }
@@ -49,6 +49,9 @@ static bool PreferSideSpecificVkInjection(int key_code) {
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION && g_on_key_action) { if (nCode == HC_ACTION && g_on_key_action) {
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam); KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
if ((kbData->flags & LLKHF_INJECTED) != 0) {
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
const int key_code = NormalizeModifierVkCode(kbData); const int key_code = NormalizeModifierVkCode(kbData);
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) { if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
+7 -3
View File
@@ -1,6 +1,7 @@
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_ #ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
#define CROSSDESK_GUI_APPLICATION_STATE_H_ #define CROSSDESK_GUI_APPLICATION_STATE_H_
#include <atomic>
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
@@ -64,7 +65,7 @@ struct InteractionState {
bool start_keyboard_capturer_ = false; bool start_keyboard_capturer_ = false;
bool show_cursor_ = false; bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false; bool keyboard_capturer_is_started_ = false;
bool keyboard_capturer_uses_sdl_events_ = false; bool keyboard_capturer_uses_window_events_ = false;
bool foucs_on_main_window_ = false; bool foucs_on_main_window_ = false;
bool focus_on_stream_window_ = false; bool focus_on_stream_window_ = false;
bool audio_capture_ = false; bool audio_capture_ = false;
@@ -119,8 +120,11 @@ struct StreamWindowState {
}; };
struct ServerWindowState { struct ServerWindowState {
bool need_to_create_server_window_ = false; // Transport callbacks run outside the UI thread. Keep the two lifecycle
bool need_to_destroy_server_window_ = false; // requests atomic so a disconnect cannot be lost while the Slint timer is
// deciding whether to create or destroy the controlled-side window.
std::atomic<bool> need_to_create_server_window_{false};
std::atomic<bool> need_to_destroy_server_window_{false};
bool server_window_created_ = false; bool server_window_created_ = false;
bool server_window_inited_ = false; bool server_window_inited_ = false;
int server_window_width_default_ = 250; int server_window_width_default_ = 250;
+413 -36
View File
@@ -28,12 +28,19 @@
#include "crossdesk_ui.h" #include "crossdesk_ui.h"
#include "fa_solid_900.h" #include "fa_solid_900.h"
#include "localization.h" #include "localization.h"
#include "platform.h"
#if _WIN32 #if _WIN32
#include <windows.h>
#include "platform/tray/win_tray.h" #include "platform/tray/win_tray.h"
#elif defined(__APPLE__) #elif defined(__APPLE__)
#include "platform/tray/mac_tray.h" #include "platform/tray/mac_tray.h"
#include "platform/window_drag.h" #include "platform/window_drag.h"
#elif defined(__linux__) #elif defined(__linux__)
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <unistd.h>
#include "platform/tray/linux_tray.h" #include "platform/tray/linux_tray.h"
#endif #endif
#include "rd_log.h" #include "rd_log.h"
@@ -44,6 +51,138 @@ namespace {
using namespace std::chrono_literals; using namespace std::chrono_literals;
#if defined(__linux__) && !defined(__APPLE__)
bool HasNonEmptyEnvironmentVariable(const char* name) {
const char* value = std::getenv(name);
return value != nullptr && value[0] != '\0';
}
class ScopedUnsetEnvironmentVariable {
public:
explicit ScopedUnsetEnvironmentVariable(const char* name) : name_(name) {
if (const char* value = std::getenv(name_)) {
original_value_ = value;
}
unsetenv(name_);
}
~ScopedUnsetEnvironmentVariable() {
if (original_value_) {
setenv(name_, original_value_->c_str(), 1);
} else {
unsetenv(name_);
}
}
ScopedUnsetEnvironmentVariable(const ScopedUnsetEnvironmentVariable&) =
delete;
ScopedUnsetEnvironmentVariable& operator=(
const ScopedUnsetEnvironmentVariable&) = delete;
private:
const char* name_;
std::optional<std::string> original_value_;
};
::Window X11GetActiveWindow(Display* display) {
if (!display) {
return 0;
}
const ::Window root = DefaultRootWindow(display);
const Atom active_window_atom =
XInternAtom(display, "_NET_ACTIVE_WINDOW", True);
if (active_window_atom != None) {
Atom actual_type = None;
int actual_format = 0;
unsigned long item_count = 0;
unsigned long bytes_after = 0;
unsigned char* data = nullptr;
if (XGetWindowProperty(display, root, active_window_atom, 0, 1, False,
XA_WINDOW, &actual_type, &actual_format, &item_count,
&bytes_after, &data) == Success &&
actual_type == XA_WINDOW && actual_format == 32 && item_count == 1 &&
data) {
const ::Window active_window = *reinterpret_cast<const ::Window*>(data);
XFree(data);
return active_window;
}
if (data) {
XFree(data);
}
}
::Window focused_window = 0;
int revert_to = RevertToNone;
XGetInputFocus(display, &focused_window, &revert_to);
return focused_window == None || focused_window == PointerRoot
? 0
: focused_window;
}
std::optional<unsigned long> X11GetWindowProcessId(Display* display,
::Window x11_window) {
if (!display || !x11_window) {
return std::nullopt;
}
const Atom process_id_atom = XInternAtom(display, "_NET_WM_PID", True);
if (process_id_atom == None) {
return std::nullopt;
}
Atom actual_type = None;
int actual_format = 0;
unsigned long item_count = 0;
unsigned long bytes_after = 0;
unsigned char* data = nullptr;
const int status = XGetWindowProperty(
display, x11_window, process_id_atom, 0, 1, False, XA_CARDINAL,
&actual_type, &actual_format, &item_count, &bytes_after, &data);
if (status != Success || actual_type != XA_CARDINAL || actual_format != 32 ||
item_count != 1 || !data) {
if (data) {
XFree(data);
}
return std::nullopt;
}
const auto process_id = *reinterpret_cast<const unsigned long*>(data);
XFree(data);
return process_id;
}
bool X11WindowMatches(Display* display, ::Window x11_window,
const slint::Window& slint_window) {
if (!display || !x11_window) {
return false;
}
XWindowAttributes attributes;
if (!XGetWindowAttributes(display, x11_window, &attributes)) {
return false;
}
int root_x = 0;
int root_y = 0;
::Window child = 0;
if (!XTranslateCoordinates(display, x11_window, DefaultRootWindow(display), 0,
0, &root_x, &root_y, &child)) {
return false;
}
const auto position = slint_window.position();
const auto size = slint_window.size();
constexpr int kGeometryTolerance = 4;
return std::abs(root_x - position.x) <= kGeometryTolerance &&
std::abs(root_y - position.y) <= kGeometryTolerance &&
std::abs(attributes.width - static_cast<int>(size.width)) <=
kGeometryTolerance &&
std::abs(attributes.height - static_cast<int>(size.height)) <=
kGeometryTolerance;
}
#endif
#if _WIN32 #if _WIN32
HICON LoadSlintTrayIcon() { HICON LoadSlintTrayIcon() {
HMODULE module = GetModuleHandleW(nullptr); HMODULE module = GetModuleHandleW(nullptr);
@@ -130,17 +269,15 @@ std::vector<ParsedReleaseNoteBlock> ParseReleaseNotesMarkdownForSlint(
size_t line_start = 0; size_t line_start = 0;
while (line_start < markdown.size()) { while (line_start < markdown.size()) {
const size_t newline = markdown.find('\n', line_start); const size_t newline = markdown.find('\n', line_start);
const size_t line_end = newline == std::string_view::npos const size_t line_end =
? markdown.size() newline == std::string_view::npos ? markdown.size() : newline;
: newline;
std::string_view line = markdown.substr(line_start, line_end - line_start); std::string_view line = markdown.substr(line_start, line_end - line_start);
if (!line.empty() && line.back() == '\r') { if (!line.empty() && line.back() == '\r') {
line.remove_suffix(1); line.remove_suffix(1);
} }
if (std::all_of(line.begin(), line.end(), [](unsigned char ch) { if (std::all_of(line.begin(), line.end(),
return std::isspace(ch) != 0; [](unsigned char ch) { return std::isspace(ch) != 0; })) {
})) {
section_gap = !blocks.empty(); section_gap = !blocks.empty();
if (newline == std::string_view::npos) { if (newline == std::string_view::npos) {
break; break;
@@ -167,11 +304,11 @@ std::vector<ParsedReleaseNoteBlock> ParseReleaseNotesMarkdownForSlint(
const std::string block_markdown(line); const std::string block_markdown(line);
ParsedReleaseNoteBlock block; ParsedReleaseNoteBlock block;
if (const auto parsed = if (const auto parsed = slint::StyledText::from_markdown(block_markdown)) {
slint::StyledText::from_markdown(block_markdown)) {
block.content = *parsed; block.content = *parsed;
} else { } else {
LOG_WARN("Failed to parse a release-note Markdown block; using plain text"); LOG_WARN(
"Failed to parse a release-note Markdown block; using plain text");
block.content = slint::StyledText::from_plain_text(block_markdown); block.content = slint::StyledText::from_plain_text(block_markdown);
} }
block.section_gap = section_gap; block.section_gap = section_gap;
@@ -496,6 +633,12 @@ float ServerWindowLogicalHeight(bool collapsed) {
constexpr float kStreamWindowLogicalWidth = 1280.0f; constexpr float kStreamWindowLogicalWidth = 1280.0f;
constexpr float kStreamWindowLogicalHeight = 720.0f; constexpr float kStreamWindowLogicalHeight = 720.0f;
constexpr float kWaylandTitlebarLogicalHeight = 32.0f;
float StreamWindowLogicalHeight(bool custom_titlebar) {
return kStreamWindowLogicalHeight +
(custom_titlebar ? kWaylandTitlebarLogicalHeight : 0.0f);
}
SDL_DisplayID DisplayForSlintWindow(const slint::Window& window) { SDL_DisplayID DisplayForSlintWindow(const slint::Window& window) {
const float scale = std::max(window.scale_factor(), 0.01f); const float scale = std::max(window.scale_factor(), 0.01f);
@@ -681,6 +824,8 @@ struct GuiApplication::SlintUi {
bool portable_service_dialog_initialized = false; bool portable_service_dialog_initialized = false;
std::string recent_model_signature; std::string recent_model_signature;
slint::Timer timer; slint::Timer timer;
WindowDragState main_drag;
WindowDragState stream_drag;
WindowDragState server_drag; WindowDragState server_drag;
int main_native_titlebar_attempts = 30; int main_native_titlebar_attempts = 30;
int stream_live_resize_configuration_attempts = 0; int stream_live_resize_configuration_attempts = 0;
@@ -696,7 +841,17 @@ struct GuiApplication::SlintUi {
std::unique_ptr<MacTray> tray; std::unique_ptr<MacTray> tray;
#elif defined(__linux__) #elif defined(__linux__)
std::unique_ptr<LinuxTray> tray; std::unique_ptr<LinuxTray> tray;
Display* x11_focus_display = nullptr;
std::chrono::steady_clock::time_point last_x11_activation_poll{};
#endif #endif
~SlintUi() {
#if defined(__linux__) && !defined(__APPLE__)
if (x11_focus_display) {
XCloseDisplay(x11_focus_display);
}
#endif
}
}; };
GuiApplication::GuiApplication() = default; GuiApplication::GuiApplication() = default;
@@ -791,6 +946,25 @@ bool GuiApplication::InitializeSDL() {
if (!getenv("SDL_AUDIODRIVER")) { if (!getenv("SDL_AUDIODRIVER")) {
setenv("SDL_AUDIODRIVER", "pulseaudio", 0); setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
} }
// Standard Wayland top-level windows cannot choose an absolute screen
// position. Use the session's XWayland compatibility server for the GUI so
// auxiliary windows such as the controlled-side status window can be
// placed deterministically. Capture and input still detect the surrounding
// Wayland session through XDG_SESSION_TYPE and continue to use the portal.
const char* requested_video_driver = std::getenv("SDL_VIDEODRIVER");
const bool video_driver_allows_x11 =
requested_video_driver == nullptr || requested_video_driver[0] == '\0' ||
std::strcmp(requested_video_driver, "x11") == 0;
use_xwayland_gui_ = IsWaylandSession() &&
HasNonEmptyEnvironmentVariable("DISPLAY") &&
video_driver_allows_x11;
if (use_xwayland_gui_) {
setenv("SDL_VIDEODRIVER", "x11", 1);
LOG_INFO(
"Wayland session detected; using XWayland for positioned GUI "
"windows while retaining Wayland portal capture and input");
}
#endif #endif
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
LOG_ERROR("SDL initialization failed: {}", SDL_GetError()); LOG_ERROR("SDL initialization failed: {}", SDL_GetError());
@@ -815,10 +989,28 @@ void GuiApplication::InitializeModules() {
} }
void GuiApplication::InitializeUi() { void GuiApplication::InitializeUi() {
#if defined(__linux__) && !defined(__APPLE__)
// Winit prefers Wayland whenever WAYLAND_DISPLAY/WAYLAND_SOCKET is present.
// Hide them only while Slint selects its process-wide window backend, then
// restore the environment before the rest of the application starts.
std::optional<ScopedUnsetEnvironmentVariable> wayland_display_guard;
std::optional<ScopedUnsetEnvironmentVariable> wayland_socket_guard;
if (use_xwayland_gui_) {
wayland_display_guard.emplace("WAYLAND_DISPLAY");
wayland_socket_guard.emplace("WAYLAND_SOCKET");
}
#endif
ui_ = std::make_unique<SlintUi>(); ui_ = std::make_unique<SlintUi>();
#if _WIN32
ui_->main->set_custom_titlebar(true);
#elif defined(__linux__)
if (use_xwayland_gui_) {
ui_->main->set_custom_titlebar(true);
ui_->main->set_wayland_titlebar(true);
}
#endif
std::vector<ui::ReleaseNoteBlock> release_note_blocks; std::vector<ui::ReleaseNoteBlock> release_note_blocks;
for (const auto& parsed : for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) {
ParseReleaseNotesMarkdownForSlint(release_notes_)) {
ui::ReleaseNoteBlock block; ui::ReleaseNoteBlock block;
block.content = parsed.content; block.content = parsed.content;
block.section_gap = parsed.section_gap; block.section_gap = parsed.section_gap;
@@ -920,6 +1112,9 @@ void GuiApplication::InitializeUi() {
if (ui_->capture_mode && ui_->capture_page == "stream") { if (ui_->capture_mode && ui_->capture_page == "stream") {
ui_->stream.emplace(ui::StreamWindow::create()); ui_->stream.emplace(ui::StreamWindow::create());
#if defined(__linux__)
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
#endif
RegisterFontAwesome((*ui_->stream)->window()); RegisterFontAwesome((*ui_->stream)->window());
(*ui_->stream)->set_tabs(ui_->tab_model); (*ui_->stream)->set_tabs(ui_->tab_model);
(*ui_->stream)->set_displays(ui_->display_model); (*ui_->stream)->set_displays(ui_->display_model);
@@ -962,9 +1157,10 @@ void GuiApplication::InitializeUi() {
#endif #endif
BindStreamCallbacks(); BindStreamCallbacks();
(*ui_->stream)->show(); (*ui_->stream)->show();
PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(), PositionWindowAtCenter(
kStreamWindowLogicalWidth, (*ui_->stream)->window(), ui_->main->window(),
kStreamWindowLogicalHeight); kStreamWindowLogicalWidth,
StreamWindowLogicalHeight((*ui_->stream)->get_custom_titlebar()));
ui_->main->hide(); ui_->main->hide();
} else if (ui_->capture_mode && ui_->capture_page == "server") { } else if (ui_->capture_mode && ui_->capture_page == "server") {
ui_->server.emplace(ui::ServerWindow::create()); ui_->server.emplace(ui::ServerWindow::create());
@@ -1109,6 +1305,27 @@ void GuiApplication::BindMainCallbacks() {
exit_ = true; exit_ = true;
return slint::CloseRequestResponse::HideWindow; return slint::CloseRequestResponse::HideWindow;
}); });
main->on_main_title_drag([this](int phase, float x, float y) {
if (ui_) {
DragWindow(ui_->main, phase, x, y, ui_->main_drag);
}
});
main->on_minimize_main_window([this] {
if (ui_) {
ui_->main->window().set_minimized(true);
}
});
main->on_close_main_window([this] {
if (!ui_) {
return;
}
if (MinimizeMainWindowToTray()) {
ui_->main->hide();
return;
}
exit_ = true;
ui_->main->hide();
});
main->on_copy_local_id([this] { main->on_copy_local_id([this] {
if (!SDL_SetClipboardText(client_id_)) { if (!SDL_SetClipboardText(client_id_)) {
LOG_WARN("Copy local id failed: {}", SDL_GetError()); LOG_WARN("Copy local id failed: {}", SDL_GetError());
@@ -1337,7 +1554,7 @@ void GuiApplication::BindStreamCallbacks() {
return; return;
} }
auto& stream = *ui_->stream; auto& stream = *ui_->stream;
stream->window().on_close_requested([this] { auto close_stream_sessions = [this] {
std::vector<std::string> ids; std::vector<std::string> ids;
{ {
std::shared_lock lock(remote_sessions_mutex_); std::shared_lock lock(remote_sessions_mutex_);
@@ -1348,8 +1565,35 @@ void GuiApplication::BindStreamCallbacks() {
for (const auto& id : ids) { for (const auto& id : ids) {
CloseStreamTab(id); CloseStreamTab(id);
} }
if (ui_ && ui_->stream) {
(*ui_->stream)->hide();
}
};
stream->window().on_close_requested([close_stream_sessions] {
close_stream_sessions();
return slint::CloseRequestResponse::HideWindow; return slint::CloseRequestResponse::HideWindow;
}); });
stream->on_stream_title_drag([this](int phase, float x, float y) {
if (ui_ && ui_->stream) {
DragWindow(*ui_->stream, phase, x, y, ui_->stream_drag);
}
});
stream->on_minimize_stream_window([this] {
if (ui_ && ui_->stream) {
(*ui_->stream)->window().set_minimized(true);
}
});
stream->on_toggle_maximize_stream_window([this] {
if (!ui_ || !ui_->stream) {
return;
}
auto& stream = *ui_->stream;
auto& window = stream->window();
const bool maximize = !window.is_maximized();
stream->set_window_maximized(maximize);
window.set_maximized(maximize);
});
stream->on_close_stream_window(close_stream_sessions);
stream->on_select_tab([this](int index) { SelectStreamTab(index); }); stream->on_select_tab([this](int index) { SelectStreamTab(index); });
stream->on_reorder_tab([this](int from, float drop_x, float tab_width) { stream->on_reorder_tab([this](int from, float drop_x, float tab_width) {
ReorderStreamTab(from, drop_x, tab_width); ReorderStreamTab(from, drop_x, tab_width);
@@ -1484,6 +1728,8 @@ void GuiApplication::BindStreamCallbacks() {
bool control, bool alt, bool shift, bool meta) { bool control, bool alt, bool shift, bool meta) {
SendKeyInput(std::string(text), pressed, control, alt, shift, meta); SendKeyInput(std::string(text), pressed, control, alt, shift, meta);
}); });
stream->on_keyboard_focus_changed(
[this](bool focused) { SetStreamKeyboardFocus(focused); });
} }
void GuiApplication::BindServerCallbacks() { void GuiApplication::BindServerCallbacks() {
@@ -1587,8 +1833,13 @@ void GuiApplication::Tick() {
} }
HandleConnectionStatusChange(); HandleConnectionStatusChange();
HandlePendingPresenceProbe(); HandlePendingPresenceProbe();
HandleConnectionTimeouts(); HandlePresenceProbeTimeout();
HandleWindowsServiceIntegration(); HandleWindowsServiceIntegration();
#if defined(__linux__) && !defined(__APPLE__)
SyncXWaylandWindowActivation();
#else
SyncStreamKeyboardFocus();
#endif
devices_.UpdateInteractions(); devices_.UpdateInteractions();
UpdateLocalization(); UpdateLocalization();
@@ -1782,9 +2033,9 @@ void GuiApplication::SyncMainWindow() {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ #if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \ !defined(__arm__) && USE_CUDA) || \
defined(__APPLE__)) defined(__APPLE__))
ui_->main->set_hardware_codec_visible(true); ui_->main->set_hardware_codec_available(true);
#else #else
ui_->main->set_hardware_codec_visible(false); ui_->main->set_hardware_codec_available(false);
#endif #endif
if (show_offline_warning_window_) { if (show_offline_warning_window_) {
@@ -1829,6 +2080,11 @@ void GuiApplication::SyncMainWindow() {
} }
model.push_back(std::move(item)); model.push_back(std::move(item));
} }
// LoadThumbnail already orders connections by last update time. Keep that
// order within each presence group while placing online devices first.
std::stable_partition(
model.begin(), model.end(),
[](const ui::RecentConnection& connection) { return connection.online; });
ui_->recent_model->set_vector(std::move(model)); ui_->recent_model->set_vector(std::move(model));
} }
@@ -1960,6 +2216,77 @@ void GuiApplication::SyncPlatformDialogs() {
#endif #endif
} }
#if defined(__linux__) && !defined(__APPLE__)
void GuiApplication::SyncXWaylandWindowActivation() {
if (!ui_ || !use_xwayland_gui_) {
return;
}
const auto now = std::chrono::steady_clock::now();
if (now - ui_->last_x11_activation_poll < 100ms) {
return;
}
ui_->last_x11_activation_poll = now;
if (!ui_->x11_focus_display) {
ui_->x11_focus_display = XOpenDisplay(nullptr);
}
Display* display = ui_->x11_focus_display;
const ::Window active_window = X11GetActiveWindow(display);
if (!active_window) {
SetStreamKeyboardFocus(false);
return;
}
if (const auto process_id = X11GetWindowProcessId(display, active_window);
process_id && *process_id != static_cast<unsigned long>(getpid())) {
ui_->main->set_window_active(false);
if (ui_->stream) {
(*ui_->stream)->set_window_active(false);
}
SetStreamKeyboardFocus(false);
return;
}
ui_->main->set_window_active(
X11WindowMatches(display, active_window, ui_->main->window()));
if (ui_->stream) {
const bool stream_active =
X11WindowMatches(display, active_window, (*ui_->stream)->window());
(*ui_->stream)->set_window_active(stream_active);
SetStreamKeyboardFocus(stream_active);
} else {
SetStreamKeyboardFocus(false);
}
}
#endif
void GuiApplication::SetStreamKeyboardFocus(bool focused) {
if (focus_on_stream_window_ == focused) {
return;
}
focus_on_stream_window_ = focused;
if (!focused) {
keyboard_.ForceReleasePressedKeys();
}
}
void GuiApplication::SyncStreamKeyboardFocus() {
if (!ui_ || !ui_->stream || !(*ui_->stream)->window().is_visible()) {
SetStreamKeyboardFocus(false);
return;
}
#if _WIN32
const HWND stream_hwnd = (*ui_->stream)->window().win32_hwnd();
SetStreamKeyboardFocus(stream_hwnd != nullptr &&
GetForegroundWindow() == stream_hwnd);
#elif defined(__APPLE__)
SetStreamKeyboardFocus(IsStreamWindowActive());
#endif
}
void GuiApplication::SyncStreamWindow() { void GuiApplication::SyncStreamWindow() {
bool has_sessions = false; bool has_sessions = false;
{ {
@@ -1968,6 +2295,9 @@ void GuiApplication::SyncStreamWindow() {
} }
if (need_to_create_stream_window_ && !ui_->stream) { if (need_to_create_stream_window_ && !ui_->stream) {
ui_->stream.emplace(ui::StreamWindow::create()); ui_->stream.emplace(ui::StreamWindow::create());
#if defined(__linux__)
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
#endif
RegisterFontAwesome((*ui_->stream)->window()); RegisterFontAwesome((*ui_->stream)->window());
// Slint globals belong to a component tree. Force the next localization // Slint globals belong to a component tree. Force the next localization
// pass to initialize the newly-created, independent stream window tree. // pass to initialize the newly-created, independent stream window tree.
@@ -1991,6 +2321,7 @@ void GuiApplication::SyncStreamWindow() {
if (!ui_->stream) { if (!ui_->stream) {
return; return;
} }
(*ui_->stream)->set_window_maximized((*ui_->stream)->window().is_maximized());
(*ui_->stream)->set_srtp_enabled(enable_srtp_); (*ui_->stream)->set_srtp_enabled(enable_srtp_);
#if defined(__APPLE__) #if defined(__APPLE__)
if (ui_->stream_live_resize_configuration_attempts > 0) { if (ui_->stream_live_resize_configuration_attempts > 0) {
@@ -2002,15 +2333,17 @@ void GuiApplication::SyncStreamWindow() {
} }
#endif #endif
if (ui_->stream_initial_position_attempts > 0) { if (ui_->stream_initial_position_attempts > 0) {
if (PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(), if (PositionWindowAtCenter(
kStreamWindowLogicalWidth, (*ui_->stream)->window(), ui_->main->window(),
kStreamWindowLogicalHeight)) { kStreamWindowLogicalWidth,
StreamWindowLogicalHeight((*ui_->stream)->get_custom_titlebar()))) {
--ui_->stream_initial_position_attempts; --ui_->stream_initial_position_attempts;
} else { } else {
ui_->stream_initial_position_attempts = 0; ui_->stream_initial_position_attempts = 0;
} }
} }
if (!has_sessions) { if (!has_sessions) {
SetStreamKeyboardFocus(false);
#if defined(__APPLE__) #if defined(__APPLE__)
SetStreamWindowFullscreen(false); SetStreamWindowFullscreen(false);
#endif #endif
@@ -2238,7 +2571,22 @@ void GuiApplication::SyncStreamWindow() {
} }
void GuiApplication::SyncServerWindow() { void GuiApplication::SyncServerWindow() {
if (need_to_create_server_window_ && !ui_->server) { // The connection map is authoritative. Lifecycle flags record callback
// intent, but callbacks for different controllers can cross each other, so
// the final decision must reflect the current connected-controller set.
need_to_create_server_window_.exchange(false, std::memory_order_acq_rel);
need_to_destroy_server_window_.exchange(false, std::memory_order_acq_rel);
bool has_connected_controller = false;
{
std::shared_lock lock(connection_status_mutex_);
has_connected_controller =
std::any_of(connection_status_.begin(), connection_status_.end(),
[](const auto& entry) {
return entry.second == ConnectionStatus::Connected;
});
}
if (has_connected_controller && !ui_->server) {
ui_->server.emplace(ui::ServerWindow::create()); ui_->server.emplace(ui::ServerWindow::create());
RegisterFontAwesome((*ui_->server)->window()); RegisterFontAwesome((*ui_->server)->window());
(*ui_->server)->set_controllers(ui_->controller_model); (*ui_->server)->set_controllers(ui_->controller_model);
@@ -2258,15 +2606,13 @@ void GuiApplication::SyncServerWindow() {
ui_->server_initial_position_attempts = 2; ui_->server_initial_position_attempts = 2;
server_window_created_ = true; server_window_created_ = true;
server_window_inited_ = true; server_window_inited_ = true;
need_to_create_server_window_ = false;
} }
if (need_to_destroy_server_window_ && ui_->server) { if (!has_connected_controller && ui_->server) {
(*ui_->server)->hide(); (*ui_->server)->hide();
ui_->server.reset(); ui_->server.reset();
server_window_created_ = false; server_window_created_ = false;
server_window_inited_ = false; server_window_inited_ = false;
ui_->server_initial_position_attempts = 0; ui_->server_initial_position_attempts = 0;
need_to_destroy_server_window_ = false;
} }
if (!ui_->server) { if (!ui_->server) {
return; return;
@@ -2479,14 +2825,25 @@ void GuiApplication::SelectStreamTab(int index) {
if (!ui_ || index < 0 || index >= static_cast<int>(ui_->tab_ids.size())) { if (!ui_ || index < 0 || index >= static_cast<int>(ui_->tab_ids.size())) {
return; return;
} }
focused_remote_id_ = ui_->tab_ids[index]; const std::string selected_remote_id = ui_->tab_ids[index];
controlled_remote_id_ = focused_remote_id_; if (!controlled_remote_id_.empty() &&
controlled_remote_id_ != selected_remote_id) {
keyboard_.ForceReleasePressedKeys();
}
focused_remote_id_ = selected_remote_id;
controlled_remote_id_ = selected_remote_id;
std::shared_lock lock(remote_sessions_mutex_); std::shared_lock lock(remote_sessions_mutex_);
for (auto& [id, props] : remote_sessions_) { for (auto& [id, props] : remote_sessions_) {
if (props) { if (props) {
props->tab_selected_ = id == focused_remote_id_; props->tab_selected_ = id == focused_remote_id_;
} }
} }
const auto selected = remote_sessions_.find(selected_remote_id);
start_keyboard_capturer_ = selected != remote_sessions_.end() &&
selected->second &&
selected->second->control_mouse_ &&
selected->second->connection_status_.load() ==
ConnectionStatus::Connected;
} }
void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) { void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) {
@@ -2561,10 +2918,14 @@ void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
const auto size = (*ui_->stream)->window().size(); const auto size = (*ui_->stream)->window().size();
const float scale = (*ui_->stream)->window().scale_factor(); const float scale = (*ui_->stream)->window().scale_factor();
const float available_width = size.width / scale; const float available_width = size.width / scale;
const float titlebar_height =
!fullscreen_button_pressed_ && (*ui_->stream)->get_custom_titlebar()
? kWaylandTitlebarLogicalHeight
: 0.0f;
const float available_height = const float available_height =
size.height / scale - (fullscreen_button_pressed_ size.height / scale - titlebar_height -
? 0.0f (fullscreen_button_pressed_ ? 0.0f
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f)); : (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
float render_width = available_width; float render_width = available_width;
float render_height = available_height; float render_height = available_height;
float offset_x = 0; float offset_x = 0;
@@ -2632,10 +2993,14 @@ void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
const auto size = (*ui_->stream)->window().size(); const auto size = (*ui_->stream)->window().size();
const float scale = (*ui_->stream)->window().scale_factor(); const float scale = (*ui_->stream)->window().scale_factor();
const float width = size.width / scale; const float width = size.width / scale;
const float titlebar_height =
!fullscreen_button_pressed_ && (*ui_->stream)->get_custom_titlebar()
? kWaylandTitlebarLogicalHeight
: 0.0f;
const float height = const float height =
size.height / scale - (fullscreen_button_pressed_ size.height / scale - titlebar_height -
? 0.0f (fullscreen_button_pressed_ ? 0.0f
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f)); : (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
if (width <= 0 || height <= 0) { if (width <= 0 || height <= 0) {
return; return;
} }
@@ -2666,10 +3031,22 @@ void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
(void)alt; (void)alt;
(void)shift; (void)shift;
(void)meta; (void)meta;
if (auto props = SelectedSession()) { auto props = SelectedSession();
controlled_remote_id_ = props->remote_id_; if (!props || !props->peer_ || !props->control_mouse_ ||
focused_remote_id_ = props->remote_id_; props->connection_status_.load() != ConnectionStatus::Connected) {
return;
} }
controlled_remote_id_ = props->remote_id_;
focused_remote_id_ = props->remote_id_;
// Native hooks see the same physical key before Slint does. When a native
// hook is active, forwarding the FocusScope callback as well would duplicate
// the event on platforms whose hook does not consume the local key.
if (keyboard_capturer_is_started_ && !keyboard_capturer_uses_window_events_) {
return;
}
const int virtual_key = SlintKeyToWindowsVk(text); const int virtual_key = SlintKeyToWindowsVk(text);
if (virtual_key >= 0) { if (virtual_key >= 0) {
keyboard_.SendKeyCommand(virtual_key, pressed); keyboard_.SendKeyCommand(virtual_key, pressed);
+8
View File
@@ -35,7 +35,12 @@ private:
void SyncConnectionDialog(); void SyncConnectionDialog();
void SyncPlatformDialogs(); void SyncPlatformDialogs();
void SyncStreamWindow(); void SyncStreamWindow();
void SyncStreamKeyboardFocus();
void SetStreamKeyboardFocus(bool focused);
void SyncServerWindow(); void SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
void SyncXWaylandWindowActivation();
#endif
void UpdateLocalization(); void UpdateLocalization();
void ResetSettingsUi(); void ResetSettingsUi();
void SaveSettingsFromUi(); void SaveSettingsFromUi();
@@ -59,6 +64,9 @@ private:
bool OpenUrl(const std::string &url); bool OpenUrl(const std::string &url);
std::unique_ptr<SlintUi> ui_; std::unique_ptr<SlintUi> ui_;
#if defined(__linux__) && !defined(__APPLE__)
bool use_xwayland_gui_ = false;
#endif
}; };
} // namespace crossdesk } // namespace crossdesk
+2 -1
View File
@@ -200,7 +200,8 @@ void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP: case SDL_EVENT_KEY_UP:
if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ && if (keyboard_capturer_is_started_ &&
keyboard_capturer_uses_window_events_ &&
focus_on_stream_window_ && stream_window_ && focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == event.key.windowID) { SDL_GetWindowID(stream_window_) == event.key.windowID) {
ProcessKeyboardEvent(event); ProcessKeyboardEvent(event);
+3 -5
View File
@@ -220,10 +220,8 @@ int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
return -1; return -1;
} }
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) { // SDL represents key auto-repeat as additional key-down events. Forward
return 0; // them so the fallback path has the same long-press behavior as native hooks.
}
const int key_code = TranslateSdlKeyboardEventToVk(event.key); const int key_code = TranslateSdlKeyboardEventToVk(event.key);
if (key_code < 0) { if (key_code < 0) {
return 0; return 0;
@@ -412,4 +410,4 @@ int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -228,27 +228,27 @@ int SessionDeviceManager::StopMouseController() {
} }
int SessionDeviceManager::StartKeyboardCapturer() { int SessionDeviceManager::StartKeyboardCapturer() {
owner_.keyboard_capturer_uses_sdl_events_ = false; owner_.keyboard_capturer_uses_window_events_ = false;
#ifdef __APPLE__ #ifdef __APPLE__
if (!owner_.EnsureMacAccessibilityPermission()) { if (!owner_.EnsureMacAccessibilityPermission()) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
return 0; return 0;
} }
#endif #endif
#if defined(__linux__) && !defined(__APPLE__) #if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) { if (IsWaylandSession()) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
LOG_INFO("Start keyboard capturer with SDL Wayland backend"); LOG_INFO("Start keyboard capturer with Slint Wayland backend");
return 0; return 0;
} }
#endif #endif
if (!keyboard_capturer_) { if (!keyboard_capturer_) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN( LOG_WARN(
"keyboard capturer is nullptr, falling back to SDL keyboard events"); "keyboard capturer is nullptr, falling back to Slint keyboard events");
return 0; return 0;
} }
@@ -263,9 +263,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
}, },
&owner_); &owner_);
if (hook_ret != 0) { if (hook_ret != 0) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN( LOG_WARN(
"Start keyboard capturer failed, falling back to SDL keyboard events"); "Start keyboard capturer failed, falling back to Slint keyboard events");
} else { } else {
LOG_INFO("Start keyboard capturer with native hook"); LOG_INFO("Start keyboard capturer with native hook");
} }
@@ -273,9 +273,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
} }
int SessionDeviceManager::StopKeyboardCapturer() { int SessionDeviceManager::StopKeyboardCapturer() {
if (owner_.keyboard_capturer_uses_sdl_events_) { if (owner_.keyboard_capturer_uses_window_events_) {
owner_.keyboard_capturer_uses_sdl_events_ = false; owner_.keyboard_capturer_uses_window_events_ = false;
LOG_INFO("Stop keyboard capturer with SDL keyboard backend"); LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
return 0; return 0;
} }
+4
View File
@@ -16,6 +16,10 @@ bool HideDisabledMainWindowZoomButton();
// fullscreen action with an immediate, single-window fullscreen transition. // fullscreen action with an immediate, single-window fullscreen transition.
bool ConfigureStreamWindowLiveResize(); bool ConfigureStreamWindowLiveResize();
// Returns whether the configured stream window is currently the active AppKit
// key window.
bool IsStreamWindowActive();
// Enters or leaves the stream window's animation-free fullscreen mode. // Enters or leaves the stream window's animation-free fullscreen mode.
bool SetStreamWindowFullscreen(bool fullscreen); bool SetStreamWindowFullscreen(bool fullscreen);
bool IsStreamWindowFullscreen(); bool IsStreamWindowFullscreen();
+7
View File
@@ -146,6 +146,13 @@ bool ConfigureStreamWindowLiveResize() {
} }
} }
bool IsStreamWindowActive() {
@autoreleasepool {
return stream_window != nil && [NSApp isActive] &&
[stream_window isKeyWindow];
}
}
bool SetStreamWindowFullscreen(bool fullscreen) { bool SetStreamWindowFullscreen(bool fullscreen) {
@autoreleasepool { @autoreleasepool {
if (stream_window == nil || stream_fullscreen == fullscreen) { if (stream_window == nil || stream_fullscreen == fullscreen) {
+70 -62
View File
@@ -1,5 +1,3 @@
#include "runtime/gui_runtime.h"
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <cstring> #include <cstring>
@@ -11,18 +9,15 @@
#include <vector> #include <vector>
#include "localization.h" #include "localization.h"
#include "platform.h"
#include "rd_log.h" #include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk { namespace crossdesk {
namespace { namespace {
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5); constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
constexpr auto kConnectionAttemptTimeout = std::chrono::seconds(20); } // namespace
bool IsConnectionAttemptPending(ConnectionStatus status) {
return status == ConnectionStatus::Connecting ||
status == ConnectionStatus::Gathering;
}
} // namespace
void GuiRuntime::HandleConnectionStatusChange() { void GuiRuntime::HandleConnectionStatusChange() {
if (signal_connected_ && peer_ && need_to_send_recent_connections_) { if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
@@ -31,7 +26,7 @@ void GuiRuntime::HandleConnectionStatusChange() {
j["type"] = "recent_connections_presence"; j["type"] = "recent_connections_presence";
j["user_id"] = client_id_; j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array(); j["devices"] = nlohmann::json::array();
for (const auto &id : recent_connection_ids_) { for (const auto& id : recent_connection_ids_) {
std::string pure_id = id; std::string pure_id = id;
size_t pos_y = pure_id.find('Y'); size_t pos_y = pure_id.find('Y');
size_t pos_n = pure_id.find('N'); size_t pos_n = pure_id.find('N');
@@ -95,7 +90,7 @@ void GuiRuntime::HandlePendingPresenceProbe() {
show_offline_warning_window_ = true; show_offline_warning_window_ = true;
} }
void GuiRuntime::HandleConnectionTimeouts() { void GuiRuntime::HandlePresenceProbeTimeout() {
const auto now = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now();
bool presence_probe_timed_out = false; bool presence_probe_timed_out = false;
@@ -121,48 +116,67 @@ void GuiRuntime::HandleConnectionTimeouts() {
show_offline_warning_window_ = true; show_offline_warning_window_ = true;
LOG_WARN("Presence probe timed out for [{}]", presence_remote_id); LOG_WARN("Presence probe timed out for [{}]", presence_remote_id);
} }
}
bool rejoin_state_changed = false; void GuiRuntime::HandleServerControllerDisconnected(
for (auto &[_, props] : remote_sessions_) { const std::string& remote_id, const char* reason) {
if (!props || !props->connection_attempt_active_.load()) { keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
continue;
}
const ConnectionStatus status = props->connection_status_.load(); bool has_connected_controller = false;
if (!IsConnectionAttemptPending(status)) { bool has_web_controller = false;
props->connection_attempt_active_.store(false); std::string remaining_controller_id;
continue; {
} std::unique_lock lock(connection_status_mutex_);
connection_status_.erase(remote_id);
if (now - props->connection_attempt_started_at_ < connection_host_names_.erase(remote_id);
kConnectionAttemptTimeout) { for (const auto& [id, status] : connection_status_) {
continue; if (status != ConnectionStatus::Connected) {
} continue;
}
LOG_WARN("Connection to [{}] timed out, status={}", props->remote_id_, has_connected_controller = true;
static_cast<int>(status)); has_web_controller =
props->connection_attempt_active_.store(false); has_web_controller || id.find("web") != std::string::npos;
props->connection_established_ = false; if (remaining_controller_id.empty()) {
props->rejoin_ = false; remaining_controller_id = id;
props->connection_status_.store(ConnectionStatus::Failed);
focused_remote_id_ = props->remote_id_;
show_connection_status_window_ = true;
rejoin_state_changed = true;
}
if (rejoin_state_changed) {
need_to_rejoin_ = false;
for (const auto &[_, props] : remote_sessions_) {
if (props && props->rejoin_) {
need_to_rejoin_ = true;
break;
} }
} }
} }
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, int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
const char *password, const char* password,
bool remember_password) { bool remember_password) {
if (!signal_connected_ || !peer_) { if (!signal_connected_ || !peer_) {
return -1; return -1;
@@ -221,7 +235,7 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
std::thread save_thread([buffer_copy, video_width, video_height, remote_id, std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
remote_host_name, password, remote_host_name, password,
thumbnail = thumbnail_]() { thumbnail = thumbnail_]() {
thumbnail->SaveToThumbnail((char *)buffer_copy.data(), video_width, thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width,
video_height, remote_id, remote_host_name, video_height, remote_id, remote_host_name,
password); password);
}); });
@@ -255,7 +269,7 @@ void GuiRuntime::CloseAllRemoteSessions() {
{ {
// std::shared_lock lock(remote_sessions_mutex_); // std::shared_lock lock(remote_sessions_mutex_);
for (auto &it : remote_sessions_) { for (auto& it : remote_sessions_) {
auto props = it.second; auto props = it.second;
CloseRemoteSession(props); CloseRemoteSession(props);
} }
@@ -279,7 +293,7 @@ void GuiRuntime::WaitForThumbnailSaveTasks() {
return; return;
} }
for (auto &thread : threads_to_join) { for (auto& thread : threads_to_join) {
if (thread.joinable()) { if (thread.joinable()) {
thread.join(); thread.join();
} }
@@ -300,9 +314,8 @@ void GuiRuntime::ResetRemoteSessionResources(
} }
} }
std::shared_ptr<GuiRuntime::RemoteSession> std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
GuiRuntime::FindRemoteSession( const std::string& remote_id) {
const std::string &remote_id) {
if (remote_id.empty()) { if (remote_id.empty()) {
return nullptr; return nullptr;
} }
@@ -315,8 +328,7 @@ GuiRuntime::FindRemoteSession(
return it->second; return it->second;
} }
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
bool remember_password, bool bypass_presence_check) { bool remember_password, bool bypass_presence_check) {
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) { if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
int ret = int ret =
@@ -336,19 +348,17 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
focused_remote_id_ = remote_id; focused_remote_id_ = remote_id;
// std::shared_lock shared_lock(remote_sessions_mutex_); // std::shared_lock shared_lock(remote_sessions_mutex_);
bool exists = bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
(remote_sessions_.find(remote_id) != remote_sessions_.end());
// shared_lock.unlock(); // shared_lock.unlock();
if (!exists) { if (!exists) {
PeerPtr *peer_to_init = nullptr; PeerPtr* peer_to_init = nullptr;
std::string local_id; std::string local_id;
{ {
// std::unique_lock unique_lock(remote_sessions_mutex_); // std::unique_lock unique_lock(remote_sessions_mutex_);
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) { if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
remote_sessions_[remote_id] = remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
std::make_shared<RemoteSession>();
auto props = remote_sessions_[remote_id]; auto props = remote_sessions_[remote_id];
props->local_id_ = "C-" + std::string(client_id_); props->local_id_ = "C-" + std::string(client_id_);
props->remote_id_ = remote_id; props->remote_id_ = remote_id;
@@ -371,7 +381,7 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
return -1; return -1;
} }
for (const auto &display_info : devices_.display_info_list()) { for (const auto& display_info : devices_.display_info_list()) {
AddVideoStream(props->peer_, display_info.name.c_str()); AddVideoStream(props->peer_, display_info.name.c_str());
} }
AddAudioStream(props->peer_, props->audio_label_.c_str()); AddAudioStream(props->peer_, props->audio_label_.c_str());
@@ -402,8 +412,6 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
auto props = remote_sessions_[remote_id]; auto props = remote_sessions_[remote_id];
if (!props->connection_established_) { if (!props->connection_established_) {
props->connection_status_.store(ConnectionStatus::Connecting); props->connection_status_.store(ConnectionStatus::Connecting);
props->connection_attempt_active_.store(true);
props->connection_attempt_started_at_ = std::chrono::steady_clock::now();
show_connection_status_window_ = true; show_connection_status_window_ = true;
props->remember_password_ = remember_password; props->remember_password_ = remember_password;
@@ -429,4 +437,4 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
+20 -20
View File
@@ -8,16 +8,16 @@
#include "features/devices/session_device_manager.h" #include "features/devices/session_device_manager.h"
#include "features/file_transfer/file_transfer_manager.h" #include "features/file_transfer/file_transfer_manager.h"
#include "features/input/keyboard_controller.h" #include "features/input/keyboard_controller.h"
#include "features/settings/settings_manager.h"
#include "runtime/gui_state.h" #include "runtime/gui_state.h"
#include "runtime/peer_event_handler.h" #include "runtime/peer_event_handler.h"
#include "features/settings/settings_manager.h"
namespace crossdesk { namespace crossdesk {
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session // Shared GUI runtime. It owns subsystem controllers and cross-cutting session
// state, but no window lifecycle, ImGui view, or transport callback methods. // state, but no window lifecycle, ImGui view, or transport callback methods.
class GuiRuntime : protected gui_detail::GuiState { class GuiRuntime : protected gui_detail::GuiState {
protected: protected:
using FileTransferState = gui_detail::FileTransferState; using FileTransferState = gui_detail::FileTransferState;
using RemoteSession = gui_detail::RemoteSession; using RemoteSession = gui_detail::RemoteSession;
@@ -32,35 +32,35 @@ protected:
GuiRuntime(); GuiRuntime();
~GuiRuntime(); ~GuiRuntime();
static void SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len); static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
static void SdlCaptureAudioOut(void *userdata, Uint8 *stream, int len); static void SdlCaptureAudioOut(void* userdata, Uint8* stream, int len);
int CreateConnectionPeer(); int CreateConnectionPeer();
int ConnectTo(const std::string &remote_id, const char *password, int ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check = false); bool remember_password, bool bypass_presence_check = false);
int RequestSingleDevicePresence(const std::string &remote_id, int RequestSingleDevicePresence(const std::string& remote_id,
const char *password, bool remember_password); const char* password, bool remember_password);
void UpdateLabels(); void UpdateLabels();
void HandleRecentConnections(); void HandleRecentConnections();
void HandleConnectionStatusChange(); void HandleConnectionStatusChange();
void HandlePendingPresenceProbe(); void HandlePendingPresenceProbe();
void HandleConnectionTimeouts(); void HandlePresenceProbeTimeout();
void HandleServerControllerDisconnected(const std::string& remote_id,
const char* reason);
void HandleWindowsServiceIntegration(); void HandleWindowsServiceIntegration();
void CloseRemoteSession(std::shared_ptr<RemoteSession> props); void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions(); void CloseAllRemoteSessions();
void ResetRemoteSessionResources( void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
std::shared_ptr<RemoteSession> props);
void WaitForThumbnailSaveTasks(); void WaitForThumbnailSaveTasks();
std::shared_ptr<RemoteSession> std::shared_ptr<RemoteSession> FindRemoteSession(
FindRemoteSession(const std::string &remote_id); const std::string& remote_id);
void ResetRemoteServiceStatus(RemoteSession &props); void ResetRemoteServiceStatus(RemoteSession& props);
void ApplyRemoteServiceStatus(RemoteSession &props, void ApplyRemoteServiceStatus(RemoteSession& props,
const ServiceStatus &status); const ServiceStatus& status);
RemoteUnlockState RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
GetRemoteUnlockState(const RemoteSession &props) const;
#if _WIN32 #if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas); void ResetLocalWindowsServiceState(bool clear_pending_sas);
#endif #endif
@@ -82,7 +82,7 @@ protected:
KeyboardController keyboard_; KeyboardController keyboard_;
PeerEventHandler peer_events_; PeerEventHandler peer_events_;
private: private:
friend class ClipboardController; friend class ClipboardController;
friend class SessionDeviceManager; friend class SessionDeviceManager;
friend class FileTransferManager; friend class FileTransferManager;
@@ -91,6 +91,6 @@ private:
friend class PeerEventHandler; friend class PeerEventHandler;
}; };
} // namespace crossdesk } // namespace crossdesk
#endif // CROSSDESK_GUI_RUNTIME_H_ #endif // CROSSDESK_GUI_RUNTIME_H_
+199 -250
View File
@@ -26,12 +26,12 @@
namespace crossdesk { namespace crossdesk {
PeerEventHandler::PeerEventHandler(GuiRuntime &owner) : owner_(owner) {} PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
void PeerEventHandler::OnSignalMessage(const char *message, size_t size, void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
void *user_data) { void* user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data); auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime || !message || size == 0) { if (!runtime || !message || size == 0) {
return; return;
} }
@@ -43,7 +43,7 @@ void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
std::string type = j["type"].get<std::string>(); std::string type = j["type"].get<std::string>();
if (type == "presence") { if (type == "presence") {
if (j.contains("devices") && j["devices"].is_array()) { if (j.contains("devices") && j["devices"].is_array()) {
for (auto &dev : j["devices"]) { for (auto& dev : j["devices"]) {
if (!dev.is_object()) { if (!dev.is_object()) {
continue; continue;
} }
@@ -88,11 +88,10 @@ void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
} }
} }
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id, size_t user_id_size, void* user_data) {
size_t user_id_size, void *user_data) { auto* handler = static_cast<PeerEventHandler*>(user_data);
auto *handler = static_cast<PeerEventHandler *>(user_data); GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) { if (!runtime) {
return; return;
} }
@@ -150,13 +149,12 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id,
} }
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
const char *user_id, const char* user_id,
const size_t user_id_size, const size_t user_id_size,
void *user_data) { void* user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data); auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) if (!runtime) return;
return;
std::string remote_id(user_id, user_id_size); std::string remote_id(user_id, user_id_size);
std::shared_ptr<GuiRuntime::RemoteSession> props; std::shared_ptr<GuiRuntime::RemoteSession> props;
@@ -172,113 +170,112 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->is_client_mode_ = true; runtime->is_client_mode_ = true;
runtime->show_connection_status_window_ = true; runtime->show_connection_status_window_ = true;
props->connection_status_.store(status); props->connection_status_.store(status);
if (status != ConnectionStatus::Connecting &&
status != ConnectionStatus::Gathering) {
props->connection_attempt_active_.store(false);
}
switch (status) { switch (status) {
case ConnectionStatus::Connected: { case ConnectionStatus::Connected: {
runtime->ResetRemoteServiceStatus(*props); runtime->ResetRemoteServiceStatus(*props);
{ {
RemoteAction remote_action; RemoteAction remote_action;
remote_action.i.display_num = remote_action.i.display_num =
runtime->devices_.display_info_list().size(); runtime->devices_.display_info_list().size();
remote_action.i.display_list = remote_action.i.display_list =
(char **)malloc(remote_action.i.display_num * sizeof(char *)); (char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left = remote_action.i.left =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top = remote_action.i.top =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right = remote_action.i.right =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom = remote_action.i.bottom =
(int *)malloc(remote_action.i.display_num * sizeof(int)); (int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) { for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1, LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name); runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char *)malloc( remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1); runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i], strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(), runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length()); runtime->devices_.display_info_list()[i].name.length());
remote_action.i.display_list remote_action.i
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0'; .display_list[i][runtime->devices_.display_info_list()[i]
remote_action.i.left[i] = .name.length()] = '\0';
runtime->devices_.display_info_list()[i].left; remote_action.i.left[i] =
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top; runtime->devices_.display_info_list()[i].left;
remote_action.i.right[i] = remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].right; runtime->devices_.display_info_list()[i].top;
remote_action.i.bottom[i] = remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].bottom; runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
} }
std::string host_name = GetHostName(); if (!runtime->need_to_create_stream_window_ &&
remote_action.type = ControlType::host_infomation; !runtime->remote_sessions_.empty()) {
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size()); runtime->need_to_create_stream_window_ = true;
remote_action.i.host_name[host_name.size()] = '\0'; }
remote_action.i.host_name_size = host_name.size(); props->connection_established_ = true;
runtime->start_keyboard_capturer_ = true;
std::string msg = remote_action.to_json(); break;
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
} }
case ConnectionStatus::Disconnected:
if (!runtime->need_to_create_stream_window_ && case ConnectionStatus::Failed:
!runtime->remote_sessions_.empty()) { case ConnectionStatus::Closed: {
runtime->need_to_create_stream_window_ = true; runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
} "connection_closed");
props->connection_established_ = true;
runtime->start_keyboard_capturer_ = true;
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
"connection_closed");
props->connection_established_ = false;
props->enable_mouse_control_ = false;
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = true;
}
runtime->focus_on_stream_window_ = false;
break;
}
case ConnectionStatus::IncorrectPassword: {
runtime->password_validating_ = false;
runtime->password_validating_time_++;
if (runtime->connect_button_pressed_) {
runtime->connect_button_pressed_ = false;
props->connection_established_ = false; props->connection_established_ = false;
runtime->connect_button_label_ = props->enable_mouse_control_ = false;
localization::connect[runtime->localization_language_index_]; runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = true;
}
runtime->focus_on_stream_window_ = false;
break;
} }
break; case ConnectionStatus::IncorrectPassword: {
} runtime->password_validating_ = false;
case ConnectionStatus::NoSuchTransmissionId: runtime->password_validating_time_++;
case ConnectionStatus::RemoteUnavailable: { if (runtime->connect_button_pressed_) {
if (runtime->connect_button_pressed_) { runtime->connect_button_pressed_ = false;
props->connection_established_ = false; props->connection_established_ = false;
runtime->connect_button_label_ = runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_]; localization::connect[runtime->localization_language_index_];
}
break;
} }
break; case ConnectionStatus::NoSuchTransmissionId:
} case ConnectionStatus::RemoteUnavailable: {
default: if (runtime->connect_button_pressed_) {
break; props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
}
break;
}
default:
break;
} }
} else { } else {
runtime->is_client_mode_ = false; runtime->is_client_mode_ = false;
@@ -289,155 +286,107 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
} }
switch (status) { switch (status) {
case ConnectionStatus::Connected: { case ConnectionStatus::Connected: {
#if _WIN32 #if _WIN32
runtime->last_windows_service_status_tick_ = 0; runtime->last_windows_service_status_tick_ = 0;
#endif #endif
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.display_info_list().size();
remote_action.i.display_list =
(char **)malloc(remote_action.i.display_num * sizeof(char *));
remote_action.i.left =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int *)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char *)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i.display_list
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
}
runtime->need_to_create_server_window_ = true;
runtime->is_server_mode_ = true;
runtime->start_screen_capturer_ = true;
runtime->start_speaker_capturer_ = true;
runtime->remote_client_id_ = remote_id;
runtime->start_mouse_controller_ = true;
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) {
return kv.first.find("web") != std::string::npos;
})) {
runtime->show_cursor_ = true;
}
}
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
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_); RemoteAction remote_action;
runtime->connection_status_.erase(remote_id); remote_action.i.display_num =
runtime->connection_host_names_.erase(remote_id); runtime->devices_.display_info_list().size();
} remote_action.i.display_list =
runtime->devices_.ResetToInitialDisplay(); (char**)malloc(remote_action.i.display_num * sizeof(char*));
} remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
}
{ std::string host_name = GetHostName();
std::shared_lock lock(runtime->connection_status_mutex_); remote_action.type = ControlType::host_infomation;
if (std::all_of(runtime->connection_status_.begin(), memcpy(&remote_action.i.host_name, host_name.data(),
runtime->connection_status_.end(), [](const auto &kv) { host_name.size());
return kv.first.find("web") == std::string::npos; remote_action.i.host_name[host_name.size()] = '\0';
})) { remote_action.i.host_name_size = host_name.size();
runtime->show_cursor_ = false;
}
}
break; std::string msg = remote_action.to_json();
} int ret =
default: SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
break; 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->is_server_mode_ = true;
runtime->start_screen_capturer_ = true;
runtime->start_speaker_capturer_ = true;
runtime->remote_client_id_ = remote_id;
runtime->start_mouse_controller_ = true;
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(),
[](const auto& kv) {
return kv.first.find("web") != std::string::npos;
})) {
runtime->show_cursor_ = true;
}
}
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->HandleServerControllerDisconnected(remote_id,
"connection_closed");
break;
}
default:
break;
} }
} }
} }
void PeerEventHandler::OnNetStatusReport( void PeerEventHandler::OnNetStatusReport(
const char *client_id, size_t client_id_size, TraversalMode mode, const char* client_id, size_t client_id_size, TraversalMode mode,
const XNetTrafficStats *net_traffic_stats, const char *user_id, const XNetTrafficStats* net_traffic_stats, const char* user_id,
const size_t user_id_size, void *user_data) { const size_t user_id_size, void* user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data); auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) { if (!runtime) {
return; return;
} }
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) { if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
std::string id, password; std::string id, password;
const char *at_pos = strchr(client_id, '@'); const char* at_pos = strchr(client_id, '@');
if (at_pos == nullptr) { if (at_pos == nullptr) {
id = client_id; id = client_id;
password.clear(); password.clear();
@@ -510,4 +459,4 @@ void PeerEventHandler::OnNetStatusReport(
props->net_traffic_stats_ = *net_traffic_stats; props->net_traffic_stats_ = *net_traffic_stats;
} }
} }
} // namespace crossdesk } // namespace crossdesk
+3 -5
View File
@@ -58,7 +58,7 @@ struct FileTransferState {
// connection, media, input, window and transfer data that share one lifetime. // connection, media, input, window and transfer data that share one lifetime.
struct RemoteSession { struct RemoteSession {
Params params_; Params params_;
PeerPtr *peer_ = nullptr; PeerPtr* peer_ = nullptr;
std::string audio_label_ = "control_audio"; std::string audio_label_ = "control_audio";
std::string data_label_ = "data"; std::string data_label_ = "data";
std::string mouse_label_ = "mouse"; std::string mouse_label_ = "mouse";
@@ -74,8 +74,6 @@ struct RemoteSession {
SignalStatus signal_status_ = SignalStatus::SignalClosed; SignalStatus signal_status_ = SignalStatus::SignalClosed;
bool connection_established_ = false; bool connection_established_ = false;
bool rejoin_ = false; bool rejoin_ = false;
std::atomic<bool> connection_attempt_active_ = false;
std::chrono::steady_clock::time_point connection_attempt_started_at_;
bool net_traffic_stats_button_pressed_ = false; bool net_traffic_stats_button_pressed_ = false;
bool enable_mouse_control_ = true; bool enable_mouse_control_ = true;
bool mouse_controller_is_started_ = false; bool mouse_controller_is_started_ = false;
@@ -160,6 +158,6 @@ struct RemoteSession {
using RemoteSessionPtr = std::shared_ptr<RemoteSession>; using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
} // namespace crossdesk::gui_detail } // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_REMOTE_SESSION_H_ #endif // CROSSDESK_GUI_REMOTE_SESSION_H_
+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;
+317 -63
View File
@@ -184,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: "";
@@ -242,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;
@@ -268,9 +276,13 @@ export component MainWindow inherits Window {
callback cancel-portable-service; callback cancel-portable-service;
callback acknowledge-portable-service; callback acknowledge-portable-service;
callback acknowledge-portable-service-suppressed; callback acknowledge-portable-service-suppressed;
callback main-title-drag(int, length, length);
callback minimize-main-window;
callback close-main-window;
private property <bool> menu-open: false; private property <bool> menu-open: false;
in-out property <bool> settings-open: false; in-out property <bool> settings-open: false;
private property <length> settings-scroll-y: 0px;
in-out property <bool> self-host-settings-open: false; in-out property <bool> self-host-settings-open: false;
in-out property <bool> about-open: false; in-out property <bool> about-open: false;
in-out property <bool> update-open: root.update-available; in-out property <bool> update-open: root.update-available;
@@ -304,17 +316,216 @@ export component MainWindow inherits Window {
? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px ? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px
: root.recent-tooltip-pointer-x + 12px)); : root.recent-tooltip-pointer-x + 12px));
private property <length> recent-tooltip-y: min( private property <length> recent-tooltip-y: min(
root.height - root.recent-tooltip-height - 8px, root.height - root.titlebar-height - root.recent-tooltip-height - 8px,
max(8px, root.recent-tooltip-pointer-y - root.recent-tooltip-height - 8px)); max(8px, root.recent-tooltip-pointer-y - root.recent-tooltip-height - 8px));
changed settings-open => {
if root.settings-open {
root.settings-scroll-y = 0px;
focus-sink.focus();
}
}
reset-remote-id => { reset-remote-id => {
root.remote-id-input = ""; root.remote-id-input = "";
remote-id-editor.focus(); remote-id-editor.focus();
} }
Rectangle { window-surface := Rectangle {
width: root.width; width: root.width;
height: root.height; height: root.height;
background: root.wayland-titlebar ? #f6f5f4 : transparent;
border-radius:
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
clip: root.wayland-titlebar;
if root.custom-titlebar && root.wayland-titlebar: Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: root.titlebar-height;
background: #f6f5f4;
z: 100;
Text {
x: 80px;
width: parent.width - 160px;
height: parent.height;
text: "CrossDesk";
color: root.window-active ? #3d3d3d : #747474;
font-size: 12px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
overflow: elide;
}
wayland-title-drag := TouchArea {
width: parent.width - 80px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => {
root.main-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => {
if (self.pressed) {
root.main-title-drag(2, self.mouse-x, self.mouse-y);
}
}
}
wayland-minimize-button := Rectangle {
x: parent.width - 74px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: wayland-minimize-touch.pressed ? #c0bfbc
: wayland-minimize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { wayland-minimize-touch.clicked(); }
Rectangle {
x: 8px;
y: 13px;
width: 8px;
height: 0.75px;
background: root.window-active
|| wayland-minimize-touch.has-hover
|| wayland-minimize-touch.pressed ? #454545 : #898989;
}
wayland-minimize-touch := TouchArea {
clicked => { root.minimize-main-window(); }
}
}
wayland-close-button := Rectangle {
x: parent.width - 34px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: wayland-close-touch.pressed ? #b2161d
: wayland-close-touch.has-hover ? #e01b24
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { wayland-close-touch.clicked(); }
Path {
width: parent.width;
height: parent.height;
viewbox-width: 24;
viewbox-height: 24;
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
stroke: wayland-close-touch.has-hover
|| wayland-close-touch.pressed ? white
: root.window-active ? #454545 : #898989;
stroke-width: 1px;
}
wayland-close-touch := TouchArea {
clicked => { root.close-main-window(); }
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d5d3d0;
}
}
if root.custom-titlebar && !root.wayland-titlebar: Rectangle {
x: 0px;
y: 0px;
width: root.width;
height: 31px;
background: #f9f9f9;
z: 100;
Image {
x: 8px;
y: 7px;
width: 16px;
height: 16px;
source: @image-url("../../../icons/linux/crossdesk_16x16.png");
image-fit: contain;
}
Text {
x: 30px;
width: parent.width - 122px;
height: parent.height;
text: "CrossDesk";
color: #202124;
font-size: 12px;
vertical-alignment: center;
overflow: elide;
}
title-drag := TouchArea {
x: 0px;
width: parent.width - 92px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => { root.main-title-drag(phase, self.mouse-x, self.mouse-y); }
moved => { if (self.pressed) { root.main-title-drag(2, self.mouse-x, self.mouse-y); } }
}
minimize-button := Rectangle {
x: parent.width - 92px;
width: 46px;
height: parent.height;
background: minimize-touch.pressed ? #c7c7c7
: minimize-touch.has-hover ? #e5e5e5
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { minimize-touch.clicked(); }
Text {
text: FontAwesomeIcons.minus;
color: #202124;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 10px;
horizontal-alignment: center;
vertical-alignment: center;
accessible-role: none;
}
minimize-touch := TouchArea { clicked => { root.minimize-main-window(); } }
}
close-button := Rectangle {
x: parent.width - 46px;
width: 46px;
height: parent.height;
background: close-touch.pressed ? #b3251b
: close-touch.has-hover ? #c42b1c
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { close-touch.clicked(); }
Text {
text: FontAwesomeIcons.xmark;
color: close-touch.has-hover || close-touch.pressed ? white : #202124;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 12px;
horizontal-alignment: center;
vertical-alignment: center;
accessible-role: none;
}
close-touch := TouchArea { clicked => { root.close-main-window(); } }
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d4d4d4;
}
}
content-layer := Rectangle {
x: 0px;
y: root.titlebar-height;
width: root.width;
height: root.height - self.y;
clip: true;
Rectangle {
width: parent.width;
height: parent.height;
background: white; background: white;
border-width: 0px; border-width: 0px;
border-radius: 0px; border-radius: 0px;
@@ -334,8 +545,8 @@ export component MainWindow inherits Window {
z: 7; z: 7;
} }
// App actions live in the content header; native window controls and // App actions live in the content header; window controls and dragging
// dragging are provided by the system title bar. // remain in the native or Windows-specific title bar above it.
menu-button := IconButton { menu-button := IconButton {
x: parent.width - 40px; x: parent.width - 40px;
y: 4px; y: 4px;
@@ -780,8 +991,8 @@ export component MainWindow inherits Window {
if root.menu-open: TouchArea { if root.menu-open: TouchArea {
x: 0px; x: 0px;
y: 0px; y: 0px;
width: root.width; width: parent.width;
height: root.height; height: parent.height;
z: 9; z: 9;
clicked => { clicked => {
root.menu-open = false; root.menu-open = false;
@@ -1185,9 +1396,9 @@ export component MainWindow inherits Window {
TouchArea { clicked => { focus-sink.focus(); } } TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface { DialogSurface {
x: root.compact-language ? 219px : 190px; x: root.compact-language ? 219px : 190px;
y: 9px; y: 60px;
width: root.compact-language ? 202px : 269px; width: root.compact-language ? 202px : 269px;
height: 432px; height: 330px;
border-radius: 7px; border-radius: 7px;
Rectangle { Rectangle {
@@ -1202,55 +1413,86 @@ export component MainWindow inherits Window {
Text { x: 6px; text: UiStrings.settings; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; } Text { x: 6px; text: UiStrings.settings; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
} }
Text { x: 8px; y: 31px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } settings-scroll := ScrollView {
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 30px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; } x: 0px;
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; } y: 21px;
width: parent.width;
height: 265px;
viewport-y <=> root.settings-scroll-y;
Text { x: 8px; y: 61px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } settings-content := VerticalLayout {
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 60px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; } // Reserve space for the overlaid vertical scrollbar.
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; } width: settings-scroll.width;
padding-top: 8px;
padding-bottom: 8px;
spacing: 0px;
Text { x: 8px; y: 91px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } Rectangle {
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 90px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; } height: 30px;
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; } Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.hardware-codec; color: root.hardware-codec-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
CompactButton { x: 8px; y: 3px; width: root.compact-language ? 70px : 206px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.autostart; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.minimize-to-tray; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.minimize-to-tray-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 154px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactButton { x: root.compact-language ? 106px : 166px; y: 3px; width: root.compact-language ? 73px : 80px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
}
}
}
Text { x: 8px; y: 121px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } Rectangle { x: 7px; y: 286px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 120px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; } CompactButton { x: root.compact-language ? 63px : 89px; y: 297px; primary: true; text: UiStrings.ok; clicked => { root.save-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
Rectangle { x: 7px; y: 147px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; } CompactButton { x: root.compact-language ? 104px : 130px; y: 297px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
if root.hardware-codec-visible: Text { x: 8px; y: 151px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.hardware-codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
if root.hardware-codec-visible: ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 151px; checked <=> root.hardware-codec-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 177px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 181px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 181px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 207px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 211px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 211px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 237px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactButton { x: 8px; y: 241px; width: root.compact-language ? 70px : 220px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 241px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 267px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 271px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.autostart; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 271px; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 297px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 301px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 301px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 327px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 331px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.minimize-to-tray; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 331px; checked <=> root.minimize-to-tray-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 357px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 361px; width: root.compact-language ? 108px : 168px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactButton { x: root.compact-language ? 120px : 180px; y: 361px; width: root.compact-language ? 73px : 80px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
CompactButton { x: root.compact-language ? 63px : 89px; y: 396px; primary: true; text: UiStrings.ok; clicked => { root.save-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 104px : 130px; y: 396px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
} }
} }
@@ -1289,7 +1531,6 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-host; font-size: ImGuiFontStyle.body; } LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-host; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; } LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
} }
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 61px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.server-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } Text { x: 8px; y: 61px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.server-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
Rectangle { Rectangle {
x: root.compact-language ? 100px : 173px; x: root.compact-language ? 100px : 173px;
@@ -1303,7 +1544,6 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; } LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; } LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
} }
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 91px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.coturn-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } Text { x: 8px; y: 91px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.coturn-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
Rectangle { Rectangle {
x: root.compact-language ? 100px : 173px; x: root.compact-language ? 100px : 173px;
@@ -1317,7 +1557,6 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.coturn-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; } LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.coturn-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; } LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
} }
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactButton { x: root.compact-language ? 93px : 135px; y: 130px; primary: true; text: UiStrings.ok; clicked => { root.save-self-hosted-settings(); root.self-host-settings-open = false; } } CompactButton { x: root.compact-language ? 93px : 135px; y: 130px; primary: true; text: UiStrings.ok; clicked => { root.save-self-hosted-settings(); root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 134px : 176px; y: 130px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } } CompactButton { x: root.compact-language ? 134px : 176px; y: 130px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
} }
@@ -1507,4 +1746,19 @@ export component MainWindow inherits Window {
} }
} }
} }
}
if root.custom-titlebar: Rectangle {
x: 0px;
y: 0px;
width: root.width;
height: root.height;
background: transparent;
border-width: 1px;
border-color: root.wayland-titlebar ? #aaa8a6 : #8b8b8b;
border-radius:
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
z: 200;
}
}
} }
+194 -6
View File
@@ -112,13 +112,16 @@ component ControlBarButton inherits Rectangle {
export component StreamWindow inherits Window { export component StreamWindow inherits Window {
title: "CrossDesk"; title: "CrossDesk";
preferred-width: 1280px; preferred-width: 1280px;
preferred-height: 720px; preferred-height: 720px + (root.custom-titlebar ? 32px : 0px);
min-width: 640px; min-width: 640px;
min-height: 360px; min-height: 360px + (root.custom-titlebar ? 32px : 0px);
no-frame: false; no-frame: root.custom-titlebar;
background: black; background: root.custom-titlebar ? transparent : black;
default-font-size: ImGuiFontStyle.base; default-font-size: ImGuiFontStyle.base;
in property <bool> custom-titlebar: false;
in property <bool> window-active: true;
in property <bool> window-maximized: false;
in property <[StreamTab]> tabs; in property <[StreamTab]> tabs;
in-out property <int> selected-tab: 0; in-out property <int> selected-tab: 0;
in property <image> frame; in property <image> frame;
@@ -153,9 +156,14 @@ export component StreamWindow inherits Window {
callback toggle-stats; callback toggle-stats;
callback toggle-fullscreen; callback toggle-fullscreen;
callback disconnect; callback disconnect;
callback stream-title-drag(int, length, length);
callback minimize-stream-window;
callback toggle-maximize-stream-window;
callback close-stream-window;
callback pointer-input(PointerEventButton, PointerEventKind, length, length); callback pointer-input(PointerEventButton, PointerEventKind, length, length);
callback scroll-input(length, length, length, length); callback scroll-input(length, length, length, length);
callback key-input(string, bool, bool, bool, bool, bool); callback key-input(string, bool, bool, bool, bool, bool);
callback keyboard-focus-changed(bool);
private property <bool> control-expanded: true; private property <bool> control-expanded: true;
private property <bool> display-menu-open: false; private property <bool> display-menu-open: false;
@@ -169,6 +177,9 @@ export component StreamWindow inherits Window {
private property <length> control-press-x: 0px; private property <length> control-press-x: 0px;
private property <length> control-press-y: 0px; private property <length> control-press-y: 0px;
private property <int> dragging-tab: -1; private property <int> dragging-tab: -1;
private property <length> titlebar-height:
root.custom-titlebar && !root.fullscreen-enabled ? 32px : 0px;
private property <length> window-corner-radius: 12px;
callback begin-control-drag(length, length); callback begin-control-drag(length, length);
callback move-control-drag(length, length); callback move-control-drag(length, length);
callback end-control-drag; callback end-control-drag;
@@ -202,10 +213,167 @@ export component StreamWindow inherits Window {
} }
} }
Rectangle { window-surface := Rectangle {
width: root.width; width: root.width;
height: root.height; height: root.height;
background: black; background: black;
border-radius:
root.custom-titlebar && !root.fullscreen-enabled && !root.window-maximized
? root.window-corner-radius : 0px;
clip: root.custom-titlebar && !root.fullscreen-enabled;
if root.custom-titlebar && !root.fullscreen-enabled: Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: root.titlebar-height;
background: #f6f5f4;
z: 100;
Text {
x: 120px;
width: parent.width - 240px;
height: parent.height;
text: "CrossDesk";
color: root.window-active ? #3d3d3d : #747474;
font-size: 12px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
overflow: elide;
}
stream-title-drag-area := TouchArea {
width: parent.width - 120px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => {
root.stream-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => {
if (self.pressed) {
root.stream-title-drag(2, self.mouse-x, self.mouse-y);
}
}
double-clicked => {
root.toggle-maximize-stream-window();
}
}
stream-minimize-button := Rectangle {
x: parent.width - 114px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-minimize-touch.pressed ? #c0bfbc
: stream-minimize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { stream-minimize-touch.clicked(); }
Rectangle {
x: 8px;
y: 13px;
width: 8px;
height: 0.75px;
background: root.window-active
|| stream-minimize-touch.has-hover
|| stream-minimize-touch.pressed ? #454545 : #898989;
}
stream-minimize-touch := TouchArea {
clicked => { root.minimize-stream-window(); }
}
}
stream-maximize-button := Rectangle {
x: parent.width - 74px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-maximize-touch.pressed ? #c0bfbc
: stream-maximize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: root.window-maximized ? "Restore" : "Maximize";
accessible-action-default => { stream-maximize-touch.clicked(); }
if !root.window-maximized: Rectangle {
x: 8px;
y: 8px;
width: 8px;
height: 8px;
background: transparent;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
if root.window-maximized: Rectangle {
x: 9px;
y: 7px;
width: 8px;
height: 8px;
background: transparent;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
if root.window-maximized: Rectangle {
x: 7px;
y: 9px;
width: 8px;
height: 8px;
background: stream-maximize-button.background;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
stream-maximize-touch := TouchArea {
clicked => { root.toggle-maximize-stream-window(); }
}
}
stream-close-button := Rectangle {
x: parent.width - 34px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-close-touch.pressed ? #b2161d
: stream-close-touch.has-hover ? #e01b24
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { stream-close-touch.clicked(); }
Path {
width: parent.width;
height: parent.height;
viewbox-width: 24;
viewbox-height: 24;
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
stroke: stream-close-touch.has-hover
|| stream-close-touch.pressed ? white
: root.window-active ? #454545 : #898989;
stroke-width: 1px;
}
stream-close-touch := TouchArea {
clicked => { root.close-stream-window(); }
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d5d3d0;
}
}
content-layer := Rectangle {
y: root.titlebar-height;
width: parent.width;
height: parent.height - self.y;
background: black;
clip: true;
// Keep the ImGui tab strip behavior, including pointer-drag reordering. // Keep the ImGui tab strip behavior, including pointer-drag reordering.
Rectangle { Rectangle {
@@ -319,6 +487,8 @@ export component StreamWindow inherits Window {
input-focus := FocusScope { input-focus := FocusScope {
focus-on-click: true; focus-on-click: true;
focus-gained => { root.keyboard-focus-changed(true); }
focus-lost => { root.keyboard-focus-changed(false); }
key-pressed(event) => { key-pressed(event) => {
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
accept accept
@@ -333,6 +503,9 @@ export component StreamWindow inherits Window {
&& self.mouse-x <= control.x + control.width && self.mouse-x <= control.x + control.width
&& self.mouse-y >= control.y && self.mouse-y >= control.y
&& self.mouse-y <= control.y + control.height; && self.mouse-y <= control.y + control.height;
if event.kind == PointerEventKind.down && !over-control {
input-focus.focus();
}
if event.kind == PointerEventKind.down if event.kind == PointerEventKind.down
&& event.button == PointerEventButton.left && event.button == PointerEventButton.left
&& over-control { && over-control {
@@ -341,7 +514,10 @@ export component StreamWindow inherits Window {
&& (event.kind == PointerEventKind.up && (event.kind == PointerEventKind.up
|| event.kind == PointerEventKind.cancel) { || event.kind == PointerEventKind.cancel) {
root.end-control-drag(); root.end-control-drag();
} else if !root.control-dragging { // The control bar overlays the remote video. Events in
// its full rectangle (including gaps between buttons)
// are local UI input and must never reach the peer.
} else if !root.control-dragging && !over-control {
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y); root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
} }
} }
@@ -645,5 +821,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;
}
} }
} }
+34 -11
View File
@@ -19,7 +19,7 @@ std::filesystem::path FindRepoRoot() {
return {}; return {};
} }
std::string ReadFile(const std::filesystem::path &path) { std::string ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary); std::ifstream file(path, std::ios::binary);
if (!file) { if (!file) {
return {}; return {};
@@ -30,8 +30,8 @@ std::string ReadFile(const std::filesystem::path &path) {
return stream.str(); return stream.str();
} }
bool ExpectContains(const char *name, const std::string &value, bool ExpectContains(const char* name, const std::string& value,
const std::string &expected) { const std::string& expected) {
if (value.find(expected) != std::string::npos) { if (value.find(expected) != std::string::npos) {
return true; return true;
} }
@@ -40,8 +40,8 @@ bool ExpectContains(const char *name, const std::string &value,
return false; return false;
} }
bool ExpectNotContains(const char *name, const std::string &value, bool ExpectNotContains(const char* name, const std::string& value,
const std::string &unexpected) { const std::string& unexpected) {
if (value.find(unexpected) == std::string::npos) { if (value.find(unexpected) == std::string::npos) {
return true; return true;
} }
@@ -73,6 +73,10 @@ int main() {
ReadFile(repo_root / "src/gui/runtime/remote_session.h"); ReadFile(repo_root / "src/gui/runtime/remote_session.h");
const std::string connection_runtime_cpp = const std::string connection_runtime_cpp =
ReadFile(repo_root / "src/gui/runtime/connection_runtime.cpp"); ReadFile(repo_root / "src/gui/runtime/connection_runtime.cpp");
const std::string peer_event_handler_cpp =
ReadFile(repo_root / "src/gui/runtime/peer_event_handler.cpp");
const std::string application_state_h =
ReadFile(repo_root / "src/gui/application/application_state.h");
bool ok = true; bool ok = true;
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable"); ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
@@ -81,6 +85,11 @@ int main() {
"\"Remote unavailable\""); "\"Remote unavailable\"");
ok &= ExpectContains("peer_connection.cpp", peer_connection, ok &= ExpectContains("peer_connection.cpp", peer_connection,
"ConnectionStatus::RemoteUnavailable"); "ConnectionStatus::RemoteUnavailable");
ok &= ExpectContains("peer_connection.cpp", peer_connection,
"IsCurrentPeerConnection");
ok &= ExpectContains(
"peer_connection.cpp", peer_connection,
"on_connection_status_(ConnectionStatus::Closed, remote_user_id.data()");
ok &= ExpectNotContains("peer_connection.cpp", peer_connection, ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
"\"Device offline\""); "\"Device offline\"");
ok &= ExpectNotContains("peer_connection.cpp", peer_connection, ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
@@ -95,16 +104,30 @@ int main() {
"return localization::device_offline[language];"); "return localization::device_offline[language];");
ok &= ExpectContains("runtime_state.h", runtime_state_h, ok &= ExpectContains("runtime_state.h", runtime_state_h,
"pending_presence_probe_started_at_"); "pending_presence_probe_started_at_");
ok &= ExpectContains("remote_session.h", remote_session_h,
"connection_attempt_started_at_");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"HandleConnectionTimeouts"); "HandlePresenceProbeTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"kPresenceProbeTimeout"); "kPresenceProbeTimeout");
ok &= ExpectNotContains("remote_session.h", remote_session_h,
"connection_attempt_started_at_");
ok &= ExpectNotContains("remote_session.h", remote_session_h,
"connection_attempt_active_");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"kConnectionAttemptTimeout");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"ConnectionStatus::Failed");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"props->connection_status_.store(status);");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"case ConnectionStatus::Failed:");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"kConnectionAttemptTimeout"); "HandleServerControllerDisconnected");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"connection_attempt_started_at_"); "HandleServerControllerDisconnected(remote_id");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"ControllerHeartbeat");
ok &= ExpectContains("application_state.h", application_state_h,
"std::atomic<bool> need_to_destroy_server_window_");
return ok ? 0 : 1; return ok ? 0 : 1;
} }
+35 -27
View File
@@ -18,7 +18,7 @@ std::filesystem::path FindRepoRoot() {
return {}; return {};
} }
std::string ReadFile(const std::filesystem::path &path) { std::string ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary); std::ifstream file(path, std::ios::binary);
if (!file) { if (!file) {
return {}; return {};
@@ -28,8 +28,8 @@ std::string ReadFile(const std::filesystem::path &path) {
return stream.str(); return stream.str();
} }
bool ExpectContains(const char *name, const std::string &value, bool ExpectContains(const char* name, const std::string& value,
const std::string &expected) { const std::string& expected) {
if (value.find(expected) != std::string::npos) { if (value.find(expected) != std::string::npos) {
return true; return true;
} }
@@ -37,8 +37,8 @@ bool ExpectContains(const char *name, const std::string &value,
return false; return false;
} }
bool ExpectNotContains(const char *name, const std::string &value, bool ExpectNotContains(const char* name, const std::string& value,
const std::string &unexpected) { const std::string& unexpected) {
if (value.find(unexpected) == std::string::npos) { if (value.find(unexpected) == std::string::npos) {
return true; return true;
} }
@@ -46,8 +46,8 @@ bool ExpectNotContains(const char *name, const std::string &value,
return false; return false;
} }
bool ExpectContainsAtLeast(const char *name, const std::string &value, bool ExpectContainsAtLeast(const char* name, const std::string& value,
const std::string &expected, size_t min_count) { const std::string& expected, size_t min_count) {
size_t count = 0; size_t count = 0;
size_t pos = 0; size_t pos = 0;
while ((pos = value.find(expected, pos)) != std::string::npos) { while ((pos = value.find(expected, pos)) != std::string::npos) {
@@ -62,7 +62,7 @@ bool ExpectContainsAtLeast(const char *name, const std::string &value,
return false; return false;
} }
} // namespace } // namespace
int main() { int main() {
const std::filesystem::path repo_root = FindRepoRoot(); const std::filesystem::path repo_root = FindRepoRoot();
@@ -84,14 +84,16 @@ int main() {
"root.shortcut-menu-open = false;"); "root.shortcut-menu-open = false;");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"if root.display-menu-open: Rectangle"); "if root.display-menu-open: Rectangle");
ok &= ExpectContains("stream_window.slint", stream, ok &=
"display-touch.has-hover"); ExpectContains("stream_window.slint", stream, "display-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"root.switch-display(index)"); "root.switch-display(index)");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"if root.shortcut-menu-open: Rectangle"); "if root.shortcut-menu-open: Rectangle");
ok &=
ExpectContains("stream_window.slint", stream, "shortcut-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"shortcut-touch.has-hover"); "} else if !root.control-dragging && !over-control {");
ok &= ExpectContains("common.slint", common, ok &= ExpectContains("common.slint", common,
"if touch.has-hover && root.tooltip != \"\": Rectangle"); "if touch.has-hover && root.tooltip != \"\": Rectangle");
@@ -100,32 +102,38 @@ int main() {
"tooltip: StreamStrings.select-display"); "tooltip: StreamStrings.select-display");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.send-shortcut"); "tooltip: StreamStrings.send-shortcut");
ok &= ExpectContains("stream_window.slint", stream, ok &=
"root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse"); ExpectContains("stream_window.slint", stream,
ok &= ExpectContains("stream_window.slint", stream, "root.mouse-control-enabled ? StreamStrings.release-mouse "
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio"); ": StreamStrings.control-mouse");
ok &= ExpectContains(
"stream_window.slint", stream,
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.select-file"); "tooltip: StreamStrings.select-file");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats"); "root.stats-visible ? StreamStrings.hide-stats : "
ok &= ExpectContains("stream_window.slint", stream, "StreamStrings.show-stats");
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen"); ok &=
ExpectContains("stream_window.slint", stream,
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen "
": StreamStrings.fullscreen");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.disconnect"); "tooltip: StreamStrings.disconnect");
ok &= ExpectContains("stream_window.slint", stream, ok &= ExpectContains("stream_window.slint", stream,
"root.control-expanded ? StreamStrings.collapse-control : StreamStrings.expand-control"); "root.control-expanded ? StreamStrings.collapse-control "
": StreamStrings.expand-control");
ok &= ExpectContains("gui_application.cpp", application, ok &=
"set_select_display("); ExpectContains("gui_application.cpp", application, "set_select_display(");
ok &= ExpectContains("gui_application.cpp", application, ok &= ExpectContains("gui_application.cpp", application, "set_show_stats(");
"set_show_stats("); ok &=
ok &= ExpectContains("gui_application.cpp", application, ExpectContains("gui_application.cpp", application, "set_expand_control(");
"set_expand_control(");
ok &= ExpectContains("gui_application.cpp", application, ok &= ExpectContains("gui_application.cpp", application,
"set_collapse_control("); "set_collapse_control(");
ok &= ExpectContains("gui_application.cpp", application, ok &= ExpectContains("gui_application.cpp", application,
"(*ui_->stream)->global<ui::StreamStrings>()"); "(*ui_->stream)->global<ui::StreamStrings>()");
ok &= ExpectNotContains("gui_application.cpp", application, ok &=
"#include <imgui"); ExpectNotContains("gui_application.cpp", application, "#include <imgui");
return ok ? 0 : 1; return ok ? 0 : 1;
} }
+136
View File
@@ -1,29 +1,109 @@
#include "crossdesk_ui.h" #include "crossdesk_ui.h"
#include "fa_solid_900.h"
#include <cassert> #include <cassert>
#include <cstdlib>
#include <fstream>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
namespace {
// Set the CROSSDESK_*_UI_SNAPSHOT variables while using Slint's software
// renderer to export deterministic active and inactive visual-check images.
bool WriteWindowSnapshot(const slint::Window &window, const char *path) {
const auto snapshot = window.take_snapshot();
if (!snapshot) {
return false;
}
std::ofstream output(path, std::ios::binary);
output << "P6\n"
<< snapshot->width() << ' ' << snapshot->height() << "\n255\n";
for (const auto &pixel : *snapshot) {
output.put(static_cast<char>(pixel.r));
output.put(static_cast<char>(pixel.g));
output.put(static_cast<char>(pixel.b));
}
return output.good();
}
bool RegisterFontAwesome(slint::Window &window) {
return !window.window_handle()
.register_font_from_data(fa_solid_900_ttf,
fa_solid_900_ttf_len)
.has_value();
}
} // namespace
int main() { int main() {
auto window = crossdesk::ui::MainWindow::create(); auto window = crossdesk::ui::MainWindow::create();
auto stream = crossdesk::ui::StreamWindow::create(); auto stream = crossdesk::ui::StreamWindow::create();
auto server = crossdesk::ui::ServerWindow::create(); auto server = crossdesk::ui::ServerWindow::create();
if (!RegisterFontAwesome(window->window()) ||
!RegisterFontAwesome(stream->window()) ||
!RegisterFontAwesome(server->window())) {
return 1;
}
window->set_local_id("123 456 789"); window->set_local_id("123 456 789");
window->set_local_password("123456"); window->set_local_password("123456");
window->set_connection_dialog_open(true); window->set_connection_dialog_open(true);
window->set_connection_password_required(true); window->set_connection_password_required(true);
window->set_custom_titlebar(true);
window->set_wayland_titlebar(true);
window->set_settings_session_active(true); window->set_settings_session_active(true);
window->set_hardware_codec_available(false);
window->set_portable_service_settings_visible(true); window->set_portable_service_settings_visible(true);
assert(std::string(window->get_local_id()) == "123 456 789"); assert(std::string(window->get_local_id()) == "123 456 789");
assert(window->get_connection_dialog_open()); assert(window->get_connection_dialog_open());
assert(window->get_connection_password_required()); assert(window->get_connection_password_required());
assert(window->get_custom_titlebar());
assert(window->get_wayland_titlebar());
assert(window->get_settings_session_active()); assert(window->get_settings_session_active());
assert(!window->get_hardware_codec_available());
assert(window->get_portable_service_settings_visible()); assert(window->get_portable_service_settings_visible());
if (const char *snapshot_path =
std::getenv("CROSSDESK_MAIN_UI_SNAPSHOT")) {
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
if (!snapshot_written) {
return 2;
}
}
if (const char *snapshot_path =
std::getenv("CROSSDESK_MAIN_INACTIVE_UI_SNAPSHOT")) {
window->set_window_active(false);
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
window->set_window_active(true);
if (!snapshot_written) {
return 3;
}
}
window->set_settings_open(true); window->set_settings_open(true);
window->set_about_open(true); window->set_about_open(true);
assert(window->get_settings_open()); assert(window->get_settings_open());
assert(window->get_about_open()); assert(window->get_about_open());
if (const char *snapshot_path =
std::getenv("CROSSDESK_SETTINGS_UI_SNAPSHOT")) {
window->set_about_open(false);
window->set_connection_dialog_open(false);
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
if (!snapshot_written) {
return 6;
}
window->set_connection_dialog_open(true);
window->set_about_open(true);
}
window->set_remote_id_input("987654321"); window->set_remote_id_input("987654321");
window->invoke_reset_remote_id(); window->invoke_reset_remote_id();
assert(std::string(window->get_remote_id_input()).empty()); assert(std::string(window->get_remote_id_input()).empty());
@@ -62,7 +142,37 @@ int main() {
stream->set_tabs( stream->set_tabs(
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>( std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
std::vector{tab})); std::vector{tab}));
stream->set_custom_titlebar(true);
stream->set_window_maximized(true);
assert(stream->get_tabs()->row_count() == 1); assert(stream->get_tabs()->row_count() == 1);
assert(stream->get_custom_titlebar());
assert(stream->get_window_maximized());
if (const char *snapshot_path =
std::getenv("CROSSDESK_STREAM_UI_SNAPSHOT")) {
stream->set_window_maximized(false);
stream->show();
const bool snapshot_written =
WriteWindowSnapshot(stream->window(), snapshot_path);
stream->hide();
stream->set_window_maximized(true);
if (!snapshot_written) {
return 4;
}
}
if (const char *snapshot_path =
std::getenv("CROSSDESK_STREAM_INACTIVE_UI_SNAPSHOT")) {
stream->set_window_maximized(false);
stream->set_window_active(false);
stream->show();
const bool snapshot_written =
WriteWindowSnapshot(stream->window(), snapshot_path);
stream->hide();
stream->set_window_active(true);
stream->set_window_maximized(true);
if (!snapshot_written) {
return 5;
}
}
bool tab_reordered = false; bool tab_reordered = false;
stream->on_reorder_tab([&](int index, float x, float width) { stream->on_reorder_tab([&](int index, float x, float width) {
@@ -71,6 +181,32 @@ int main() {
stream->invoke_reorder_tab(0, 120.0f, 110.0f); stream->invoke_reorder_tab(0, 120.0f, 110.0f);
assert(tab_reordered); assert(tab_reordered);
bool maximize_requested = false;
stream->on_toggle_maximize_stream_window(
[&] { maximize_requested = true; });
stream->invoke_toggle_maximize_stream_window();
assert(maximize_requested);
bool keyboard_focus_changed = false;
bool keyboard_input_received = false;
stream->on_keyboard_focus_changed(
[&](bool focused) { keyboard_focus_changed = focused; });
stream->on_key_input(
[&](slint::SharedString text, bool pressed, bool, bool, bool, bool) {
keyboard_input_received = std::string(text) == "a" && pressed;
});
stream->window().set_size(
slint::LogicalSize(slint::Size<float>{1280.0f, 720.0f}));
stream->window().dispatch_pointer_press_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_pointer_release_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_key_press_event("a");
assert(keyboard_focus_changed);
assert(keyboard_input_received);
crossdesk::ui::FileTransferEntry transfer; crossdesk::ui::FileTransferEntry transfer;
transfer.name = "archive.zip"; transfer.name = "archive.zip";
transfer.status = "Sending"; transfer.status = "Sending";
+120
View File
@@ -0,0 +1,120 @@
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
namespace {
std::filesystem::path FindRepoRoot() {
std::filesystem::path current = std::filesystem::current_path();
while (!current.empty()) {
if (std::filesystem::exists(current / "xmake.lua") &&
std::filesystem::exists(
current / "submodules/minirtc/src/transport/ice_transport_controller.cpp")) {
return current;
}
current = current.parent_path();
}
return {};
}
std::string ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return {};
}
std::ostringstream stream;
stream << file.rdbuf();
return stream.str();
}
std::string ExtractSection(const std::string& source, const std::string& begin,
const std::string& end) {
const size_t begin_pos = source.find(begin);
if (begin_pos == std::string::npos) {
return {};
}
const size_t end_pos = source.find(end, begin_pos + begin.size());
if (end_pos == std::string::npos) {
return {};
}
return source.substr(begin_pos, end_pos - begin_pos);
}
bool ExpectContains(const char* name, const std::string& value,
const std::string& expected) {
if (value.find(expected) != std::string::npos) {
return true;
}
std::cerr << name << " missing expected text: " << expected << "\n";
return false;
}
bool ExpectNotContains(const char* name, const std::string& value,
const std::string& unexpected) {
if (value.find(unexpected) == std::string::npos) {
return true;
}
std::cerr << name << " contains unsafe text: " << unexpected << "\n";
return false;
}
} // namespace
int main() {
const std::filesystem::path repo_root = FindRepoRoot();
if (repo_root.empty()) {
std::cerr << "failed to locate repository root\n";
return 1;
}
const std::string controller = ReadFile(
repo_root /
"submodules/minirtc/src/transport/ice_transport_controller.cpp");
const std::string destroy = ExtractSection(
controller, "void IceTransportController::Destroy()",
"uint32_t IceTransportController::AddVideoSendChannel");
const std::string send_video = ExtractSection(
controller, "int IceTransportController::SendVideo(",
"void IceTransportController::MaybeDegradeResolutionOnEncodeTime");
const std::string on_video_encoded = ExtractSection(
controller, "int IceTransportController::OnVideoEncoded(",
"int IceTransportController::SendAudio(");
bool ok = true;
ok &= ExpectContains("Destroy", destroy,
"std::unique_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("Destroy", destroy,
"senders.swap(stream_senders_);");
ok &= ExpectContains("Destroy", destroy,
"codecs.push_back(std::move(context->codec));");
ok &= ExpectContains("Destroy", destroy, "senders.clear();");
ok &= ExpectContains("Destroy", destroy, "codecs.clear();");
ok &= ExpectNotContains("Destroy", destroy,
"std::shared_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("SendVideo", send_video,
"std::weak_ptr<IceTransportController> weak_self");
ok &= ExpectContains("SendVideo", send_video,
"std::weak_ptr<StreamContext> weak_context");
ok &= ExpectContains(
"SendVideo", send_video,
"[weak_self, weak_context, channel_name, queue_delay_ms");
ok &= ExpectContains("SendVideo", send_video,
"return self->OnVideoEncoded(");
ok &= ExpectNotContains("SendVideo", send_video,
"[this, channel_name, context");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"if (!is_running_.load())");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"std::shared_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"it->second != context");
return ok ? 0 : 1;
}
+35
View File
@@ -0,0 +1,35 @@
package("rust")
set_kind("toolchain")
set_homepage("https://rust-lang.org")
set_description("Rust is a general-purpose programming language emphasizing performance, type safety, and concurrency.")
add_versions("1.92.0", "")
add_deps("ca-certificates", {host = true, private = true})
add_deps("rustup", {host = true, private = true, system = false})
on_install(function(package)
import("core.tools.rustc.target_triple")
local host_target = assert(target_triple(package:plat(), package:arch()),
"failed to determine the Rust host target triple")
local version = package:version():shortstr()
local toolchain_name = version .. "-" .. host_target
local rustup_home = assert(os.getenv("RUSTUP_HOME"), "cannot find rustup home")
os.vrunv("rustup", {"install", "--no-self-update", toolchain_name})
os.vmv(path.join(rustup_home, "toolchains", toolchain_name, "*"), package:installdir())
-- Keep the Xmake-managed toolchain isolated from rustup after installation.
os.vrm(path.join(rustup_home, "toolchains", toolchain_name))
os.vrm(path.join(rustup_home, "update-hashes", toolchain_name))
package:addenv("RC", "bin/rustc" .. (is_host("windows") and ".exe" or ""))
package:mark_as_pathenv("RC")
end)
on_test(function(package)
os.vrun("cargo --version")
os.vrun("rustc --version")
end)
package_end()
+1
View File
@@ -10,6 +10,7 @@ package("slint")
add_configs("shared", {description = "Build the Slint runtime as a shared library", default = true, type = "boolean", readonly = true}) add_configs("shared", {description = "Build the Slint runtime as a shared library", default = true, type = "boolean", readonly = true})
add_deps("cmake") add_deps("cmake")
add_deps("rust 1.92.0", {host = true, private = true, system = false})
on_load(function(package) on_load(function(package)
package:add("includedirs", "include/slint") package:add("includedirs", "include/slint")
+1 -1
View File
@@ -1 +1 @@
includes("cpp-httplib", "slint") includes("cpp-httplib", "rust", "slint")
+27
View File
@@ -3,6 +3,22 @@ function setup_targets()
includes("submodules", "thirdparty") includes("submodules", "thirdparty")
local function copy_slint_runtime(target)
if not target:is_plat("windows") then
return
end
local slint = target:pkg("slint")
if not slint and target:dep("gui") then
slint = target:dep("gui"):pkg("slint")
end
assert(slint, "the Slint package is required to copy its Windows runtime")
local runtime_dir = path.join(slint:installdir(), "lib")
local runtime_dll = path.join(runtime_dir, "slint_cpp.dll")
assert(os.isfile(runtime_dll), "Slint runtime not found: " .. runtime_dll)
os.cp(runtime_dll, target:targetdir())
end
local crossdesk_windows_resource = "scripts/windows/crossdesk.rc" local crossdesk_windows_resource = "scripts/windows/crossdesk.rc"
if is_config("CROSSDESK_PORTABLE", true) then if is_config("CROSSDESK_PORTABLE", true) then
crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc" crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc"
@@ -57,6 +73,11 @@ function setup_targets()
set_default(false) set_default(false)
add_files("tests/connection_status_protocol_test.cpp") add_files("tests/connection_status_protocol_test.cpp")
target("video_callback_lifetime_test")
set_kind("binary")
set_default(false)
add_files("tests/video_callback_lifetime_test.cpp")
target("windows_manifest_resource_test") target("windows_manifest_resource_test")
set_kind("binary") set_kind("binary")
set_default(false) set_default(false)
@@ -87,10 +108,15 @@ function setup_targets()
set_kind("binary") set_kind("binary")
set_languages("c++20") set_languages("c++20")
set_default(false) set_default(false)
if is_os("windows") then
add_cxxflags("/bigobj")
end
add_packages("slint") add_packages("slint")
add_includedirs("src/gui/assets/fonts")
add_rules("slint") add_rules("slint")
add_files("src/gui/ui/crossdesk_ui.slint") add_files("src/gui/ui/crossdesk_ui.slint")
add_files("tests/slint_ui_smoke_test.cpp") add_files("tests/slint_ui_smoke_test.cpp")
after_build(copy_slint_runtime)
target("version_checker_test") target("version_checker_test")
set_kind("binary") set_kind("binary")
@@ -296,4 +322,5 @@ function setup_targets()
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper") add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
add_files(crossdesk_windows_resource) add_files(crossdesk_windows_resource)
end end
after_build(copy_slint_runtime)
end end