Compare commits

..
12 Commits
42 changed files with 2128 additions and 350 deletions
+3 -2
View File
@@ -17,6 +17,7 @@ MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
TRAY_RUNTIME_RECOMMENDS="libayatana-appindicator3-1 | libappindicator3-1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
@@ -145,7 +146,7 @@ for size in 16 24 32 48 64 96 128 256; do
"$ICON_BASE_DIR/${size}x${size}/apps/${PKG_NAME}.png"
done
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS"
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS, $TRAY_RUNTIME_RECOMMENDS"
if [[ -n "$EXTRA_RECOMMENDATION" ]]; then
RECOMMENDS="$RECOMMENDS, $EXTRA_RECOMMENDATION"
fi
@@ -157,7 +158,7 @@ Architecture: $DEBIAN_ARCH
Maintainer: $MAINTAINER
Description: $DESCRIPTION
Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0,
libxrender1, libxft2, libxrandr2, libxfixes3, libxcursor1, libxcb1, libxcb-randr0,
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1
+402
View File
@@ -0,0 +1,402 @@
#include "linux_cursor_shape.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstring>
#include <limits>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#if defined(__linux__) && !defined(__APPLE__)
#include <X11/Xcursor/Xcursor.h>
#include <X11/Xlib.h>
#endif
namespace crossdesk {
namespace {
constexpr LinuxCursorAlias kCursorAliases[] = {
{"help", RemoteCursorShape::help},
{"question-arrow", RemoteCursorShape::help},
{"left-ptr-help", RemoteCursorShape::help},
{"whats-this", RemoteCursorShape::help},
{"dnd-ask", RemoteCursorShape::help},
{"pointer", RemoteCursorShape::pointer},
{"hand", RemoteCursorShape::pointer},
{"hand1", RemoteCursorShape::pointer},
{"hand2", RemoteCursorShape::pointer},
{"link", RemoteCursorShape::pointer},
{"pointing-hand", RemoteCursorShape::pointer},
{"progress", RemoteCursorShape::progress},
{"left-ptr-watch", RemoteCursorShape::progress},
{"half-busy", RemoteCursorShape::progress},
{"wait", RemoteCursorShape::wait},
{"watch", RemoteCursorShape::wait},
{"crosshair", RemoteCursorShape::crosshair},
{"cross", RemoteCursorShape::crosshair},
{"cross-reverse", RemoteCursorShape::crosshair},
{"tcross", RemoteCursorShape::crosshair},
{"cell", RemoteCursorShape::crosshair},
{"plus", RemoteCursorShape::crosshair},
{"target", RemoteCursorShape::crosshair},
{"dotbox", RemoteCursorShape::crosshair},
{"dot-box-mask", RemoteCursorShape::crosshair},
{"text", RemoteCursorShape::text},
{"xterm", RemoteCursorShape::text},
{"ibeam", RemoteCursorShape::text},
{"vertical-text", RemoteCursorShape::text},
{"alias", RemoteCursorShape::alias},
{"dnd-link", RemoteCursorShape::alias},
{"copy", RemoteCursorShape::copy},
{"dnd-copy", RemoteCursorShape::copy},
{"move", RemoteCursorShape::move},
{"fleur", RemoteCursorShape::move},
{"size-all", RemoteCursorShape::move},
{"all-scroll", RemoteCursorShape::move},
{"dnd-move", RemoteCursorShape::move},
{"no-drop", RemoteCursorShape::no_drop},
{"dnd-no-drop", RemoteCursorShape::no_drop},
{"dnd-none", RemoteCursorShape::no_drop},
{"not-allowed", RemoteCursorShape::not_allowed},
{"forbidden", RemoteCursorShape::not_allowed},
{"crossed-circle", RemoteCursorShape::not_allowed},
{"circle", RemoteCursorShape::not_allowed},
{"pirate", RemoteCursorShape::not_allowed},
{"grab", RemoteCursorShape::grab},
{"openhand", RemoteCursorShape::grab},
{"open-hand", RemoteCursorShape::grab},
{"grabbing", RemoteCursorShape::grabbing},
{"closedhand", RemoteCursorShape::grabbing},
{"closed-hand", RemoteCursorShape::grabbing},
{"col-resize", RemoteCursorShape::col_resize},
{"row-resize", RemoteCursorShape::row_resize},
{"n-resize", RemoteCursorShape::n_resize},
{"top-side", RemoteCursorShape::n_resize},
{"top-tee", RemoteCursorShape::n_resize},
{"sb-up-arrow", RemoteCursorShape::n_resize},
{"e-resize", RemoteCursorShape::e_resize},
{"right-side", RemoteCursorShape::e_resize},
{"right-tee", RemoteCursorShape::e_resize},
{"sb-right-arrow", RemoteCursorShape::e_resize},
{"s-resize", RemoteCursorShape::s_resize},
{"bottom-side", RemoteCursorShape::s_resize},
{"bottom-tee", RemoteCursorShape::s_resize},
{"sb-down-arrow", RemoteCursorShape::s_resize},
{"w-resize", RemoteCursorShape::w_resize},
{"left-side", RemoteCursorShape::w_resize},
{"left-tee", RemoteCursorShape::w_resize},
{"sb-left-arrow", RemoteCursorShape::w_resize},
{"ne-resize", RemoteCursorShape::ne_resize},
{"top-right-corner", RemoteCursorShape::ne_resize},
{"ur-angle", RemoteCursorShape::ne_resize},
{"nw-resize", RemoteCursorShape::nw_resize},
{"top-left-corner", RemoteCursorShape::nw_resize},
{"ul-angle", RemoteCursorShape::nw_resize},
{"se-resize", RemoteCursorShape::se_resize},
{"bottom-right-corner", RemoteCursorShape::se_resize},
{"lr-angle", RemoteCursorShape::se_resize},
{"sw-resize", RemoteCursorShape::sw_resize},
{"bottom-left-corner", RemoteCursorShape::sw_resize},
{"ll-angle", RemoteCursorShape::sw_resize},
{"ew-resize", RemoteCursorShape::ew_resize},
{"sb-h-double-arrow", RemoteCursorShape::ew_resize},
{"h-double-arrow", RemoteCursorShape::ew_resize},
{"size-hor", RemoteCursorShape::ew_resize},
{"split-h", RemoteCursorShape::ew_resize},
{"exchange", RemoteCursorShape::ew_resize},
{"ns-resize", RemoteCursorShape::ns_resize},
{"sb-v-double-arrow", RemoteCursorShape::ns_resize},
{"v-double-arrow", RemoteCursorShape::ns_resize},
{"double-arrow", RemoteCursorShape::ns_resize},
{"size-ver", RemoteCursorShape::ns_resize},
{"split-v", RemoteCursorShape::ns_resize},
{"nesw-resize", RemoteCursorShape::nesw_resize},
{"fd-double-arrow", RemoteCursorShape::nesw_resize},
{"size-bdiag", RemoteCursorShape::nesw_resize},
{"nwse-resize", RemoteCursorShape::nwse_resize},
{"bd-double-arrow", RemoteCursorShape::nwse_resize},
{"size-fdiag", RemoteCursorShape::nwse_resize},
{"default", RemoteCursorShape::default_cursor},
{"left-ptr", RemoteCursorShape::default_cursor},
{"left-arrow", RemoteCursorShape::default_cursor},
{"arrow", RemoteCursorShape::default_cursor},
{"top-left-arrow", RemoteCursorShape::default_cursor},
{"right-ptr", RemoteCursorShape::default_cursor},
{"center-ptr", RemoteCursorShape::default_cursor},
{"context-menu", RemoteCursorShape::default_cursor},
};
std::string NormalizeCursorName(const std::string& raw_name) {
std::string normalized;
normalized.reserve(raw_name.size());
for (unsigned char ch : raw_name) {
if (ch == '_' || std::isspace(ch)) {
normalized.push_back('-');
} else {
normalized.push_back(static_cast<char>(std::tolower(ch)));
}
}
return normalized;
}
#if defined(__linux__) && !defined(__APPLE__)
struct CursorCandidate {
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
uint32_t width = 0;
uint32_t height = 0;
uint32_t xhot = 0;
uint32_t yhot = 0;
std::vector<uint32_t> argb;
};
uint8_t Alpha(uint32_t argb) {
return static_cast<uint8_t>((argb >> 24U) & 0xffU);
}
double BilinearAlpha(const CursorCandidate& candidate, double x, double y) {
if (candidate.width == 0 || candidate.height == 0) {
return 0.0;
}
x = std::max(0.0, std::min(x, static_cast<double>(candidate.width - 1)));
y = std::max(0.0, std::min(y, static_cast<double>(candidate.height - 1)));
const uint32_t x0 = static_cast<uint32_t>(std::floor(x));
const uint32_t y0 = static_cast<uint32_t>(std::floor(y));
const uint32_t x1 = std::min(x0 + 1, candidate.width - 1);
const uint32_t y1 = std::min(y0 + 1, candidate.height - 1);
const double fx = x - x0;
const double fy = y - y0;
const auto sample = [&](uint32_t px, uint32_t py) {
return static_cast<double>(
Alpha(candidate.argb[static_cast<size_t>(py) * candidate.width + px]));
};
const double top = sample(x0, y0) * (1.0 - fx) + sample(x1, y0) * fx;
const double bottom = sample(x0, y1) * (1.0 - fx) + sample(x1, y1) * fx;
return top * (1.0 - fy) + bottom * fy;
}
double SimilarityError(const LinuxCursorImageView& image,
const CursorCandidate& candidate) {
if (!image.argb || image.width == 0 || image.height == 0 ||
candidate.argb.empty()) {
return std::numeric_limits<double>::infinity();
}
double alpha_error = 0.0;
for (uint32_t y = 0; y < image.height; ++y) {
const double source_y =
(static_cast<double>(y) + 0.5) * candidate.height / image.height - 0.5;
for (uint32_t x = 0; x < image.width; ++x) {
const double source_x = (static_cast<double>(x) + 0.5) *
candidate.width / image.width -
0.5;
const double expected = BilinearAlpha(candidate, source_x, source_y);
const double actual = static_cast<double>(
Alpha(image.argb[static_cast<size_t>(y) * image.width + x]));
alpha_error += std::abs(actual - expected) / 255.0;
}
}
alpha_error /= static_cast<double>(image.width) * image.height;
const double observed_hot_x =
static_cast<double>(image.xhot) / std::max(1u, image.width);
const double observed_hot_y =
static_cast<double>(image.yhot) / std::max(1u, image.height);
const double candidate_hot_x =
static_cast<double>(candidate.xhot) / std::max(1u, candidate.width);
const double candidate_hot_y =
static_cast<double>(candidate.yhot) / std::max(1u, candidate.height);
const double hotspot_error =
std::abs(observed_hot_x - candidate_hot_x) +
std::abs(observed_hot_y - candidate_hot_y);
const double observed_aspect =
static_cast<double>(image.width) / image.height;
const double candidate_aspect =
static_cast<double>(candidate.width) / candidate.height;
const double aspect_error =
std::abs(std::log(observed_aspect / candidate_aspect));
return alpha_error + hotspot_error * 0.12 + aspect_error * 0.08;
}
bool SameImage(const LinuxCursorImageView& image,
const CursorCandidate& candidate) {
if (image.width != candidate.width || image.height != candidate.height ||
image.xhot != candidate.xhot || image.yhot != candidate.yhot) {
return false;
}
const size_t pixel_count =
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
return std::equal(image.argb, image.argb + pixel_count,
candidate.argb.begin());
}
#endif
} // namespace
const LinuxCursorAlias* LinuxCursorAliases(size_t* count) {
if (count) {
*count = sizeof(kCursorAliases) / sizeof(kCursorAliases[0]);
}
return kCursorAliases;
}
RemoteCursorShape ShapeFromLinuxCursorName(const std::string& raw_name) {
const std::string name = NormalizeCursorName(raw_name);
for (const auto& alias : kCursorAliases) {
if (name == alias.name) {
return alias.shape;
}
}
// Some toolkits prefix a theme or toolkit name to the standard cursor name.
// Restrict the fallback to separators so names such as "pointer-progress"
// still prefer their exact semantic alias above.
for (const auto& alias : kCursorAliases) {
const std::string suffix = std::string("-") + alias.name;
if (name.size() > suffix.size() &&
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0) {
return alias.shape;
}
}
return RemoteCursorShape::default_cursor;
}
#if defined(__linux__) && !defined(__APPLE__)
struct LinuxCursorThemeMatcher::Impl {
Display* display = XOpenDisplay(nullptr);
std::string theme;
int default_size = 24;
std::unordered_map<int, std::vector<CursorCandidate>> candidates_by_size;
Impl() {
if (!display) {
return;
}
if (const char* active_theme = XcursorGetTheme(display)) {
theme = active_theme;
}
const int active_size = XcursorGetDefaultSize(display);
if (active_size > 0) {
default_size = active_size;
}
}
~Impl() {
if (display) {
XCloseDisplay(display);
}
}
const std::vector<CursorCandidate>& Candidates(int requested_size) {
requested_size = std::max(1, requested_size);
auto existing = candidates_by_size.find(requested_size);
if (existing != candidates_by_size.end()) {
return existing->second;
}
std::vector<CursorCandidate> candidates;
const char* theme_name = theme.empty() ? nullptr : theme.c_str();
for (const auto& alias : kCursorAliases) {
XcursorImages* images =
XcursorLibraryLoadImages(alias.name, theme_name, requested_size);
if (!images) {
continue;
}
for (int index = 0; index < images->nimage; ++index) {
const XcursorImage* image = images->images[index];
if (!image || !image->pixels || image->width == 0 ||
image->height == 0) {
continue;
}
CursorCandidate candidate;
candidate.shape = alias.shape;
candidate.width = image->width;
candidate.height = image->height;
candidate.xhot = image->xhot;
candidate.yhot = image->yhot;
const size_t pixel_count = static_cast<size_t>(image->width) *
static_cast<size_t>(image->height);
candidate.argb.assign(image->pixels, image->pixels + pixel_count);
const bool duplicate =
std::any_of(candidates.begin(), candidates.end(),
[&](const CursorCandidate& current) {
return current.shape == candidate.shape &&
current.width == candidate.width &&
current.height == candidate.height &&
current.xhot == candidate.xhot &&
current.yhot == candidate.yhot &&
current.argb == candidate.argb;
});
if (!duplicate) {
candidates.push_back(std::move(candidate));
}
}
XcursorImagesDestroy(images);
}
return candidates_by_size
.emplace(requested_size, std::move(candidates))
.first->second;
}
};
#else
struct LinuxCursorThemeMatcher::Impl {};
#endif
LinuxCursorThemeMatcher::LinuxCursorThemeMatcher()
: impl_(std::make_unique<Impl>()) {}
LinuxCursorThemeMatcher::~LinuxCursorThemeMatcher() = default;
RemoteCursorShape LinuxCursorThemeMatcher::Match(
const LinuxCursorImageView& image) {
#if defined(__linux__) && !defined(__APPLE__)
if (!impl_ || !image.argb || image.width == 0 || image.height == 0) {
return RemoteCursorShape::default_cursor;
}
const int requested_size =
static_cast<int>(std::max(image.width, image.height));
const auto& candidates = impl_->Candidates(requested_size);
for (const auto& candidate : candidates) {
if (SameImage(image, candidate)) {
return candidate.shape;
}
}
double best_error = std::numeric_limits<double>::infinity();
RemoteCursorShape best_shape = RemoteCursorShape::default_cursor;
for (const auto& candidate : candidates) {
const double error = SimilarityError(image, candidate);
if (error < best_error) {
best_error = error;
best_shape = candidate.shape;
}
}
// Scaled theme cursors normally remain well below this threshold, while an
// unrelated custom application cursor should fall back to the default.
return best_error <= 0.20 ? best_shape
: RemoteCursorShape::default_cursor;
#else
(void)image;
return RemoteCursorShape::default_cursor;
#endif
}
} // namespace crossdesk
+49
View File
@@ -0,0 +1,49 @@
#ifndef CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
#define CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include "remote_cursor_shape.h"
namespace crossdesk {
struct LinuxCursorAlias {
const char* name;
RemoteCursorShape shape;
};
const LinuxCursorAlias* LinuxCursorAliases(size_t* count);
RemoteCursorShape ShapeFromLinuxCursorName(const std::string& name);
struct LinuxCursorImageView {
uint32_t width = 0;
uint32_t height = 0;
uint32_t xhot = 0;
uint32_t yhot = 0;
const uint32_t* argb = nullptr;
};
// Matches a compositor-provided cursor bitmap against the active Xcursor
// theme. This is needed on Wayland, where XFixes only observes XWayland
// cursors and cannot report cursors owned by native Wayland surfaces.
class LinuxCursorThemeMatcher {
public:
LinuxCursorThemeMatcher();
~LinuxCursorThemeMatcher();
LinuxCursorThemeMatcher(const LinuxCursorThemeMatcher&) = delete;
LinuxCursorThemeMatcher& operator=(const LinuxCursorThemeMatcher&) = delete;
RemoteCursorShape Match(const LinuxCursorImageView& image);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif // CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
+45
View File
@@ -0,0 +1,45 @@
#ifndef CROSSDESK_COMMON_REMOTE_CURSOR_SHAPE_H_
#define CROSSDESK_COMMON_REMOTE_CURSOR_SHAPE_H_
#include <cstdint>
namespace crossdesk {
// Keep these values aligned with Slint's MouseCursor enum. The wire protocol
// intentionally carries a semantic cursor instead of a platform handle so a
// Windows, macOS or Linux host can control a different desktop platform.
enum class RemoteCursorShape : uint8_t {
default_cursor = 0,
none,
help,
pointer,
progress,
wait,
crosshair,
text,
alias,
copy,
move,
no_drop,
not_allowed,
grab,
grabbing,
col_resize,
row_resize,
n_resize,
e_resize,
s_resize,
w_resize,
ne_resize,
nw_resize,
se_resize,
sw_resize,
ew_resize,
ns_resize,
nesw_resize,
nwse_resize,
};
} // namespace crossdesk
#endif // CROSSDESK_COMMON_REMOTE_CURSOR_SHAPE_H_
+42
View File
@@ -0,0 +1,42 @@
#include "shared_cursor_state.h"
#include <mutex>
namespace crossdesk {
namespace {
std::mutex g_cursor_state_mutex;
SharedCursorState g_cursor_state;
bool g_cursor_state_available = false;
} // namespace
void PublishSharedCursorState(bool visible, RemoteCursorShape shape) {
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
++g_cursor_state.generation;
g_cursor_state.visible = visible;
g_cursor_state.shape = visible ? shape : RemoteCursorShape::none;
g_cursor_state_available = true;
}
bool GetSharedCursorState(SharedCursorState* state) {
if (!state) {
return false;
}
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
if (!g_cursor_state_available) {
return false;
}
*state = g_cursor_state;
return true;
}
void ClearSharedCursorState() {
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
++g_cursor_state.generation;
g_cursor_state.visible = false;
g_cursor_state.shape = RemoteCursorShape::none;
g_cursor_state_available = false;
}
} // namespace crossdesk
+22
View File
@@ -0,0 +1,22 @@
#ifndef CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
#define CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
#include <cstdint>
#include "remote_cursor_shape.h"
namespace crossdesk {
struct SharedCursorState {
uint64_t generation = 0;
bool visible = false;
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
};
void PublishSharedCursorState(bool visible, RemoteCursorShape shape);
bool GetSharedCursorState(SharedCursorState* state);
void ClearSharedCursorState();
} // namespace crossdesk
#endif // CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
+2 -17
View File
@@ -34,6 +34,8 @@ int ConfigCenter::Load() {
persist_config_migration |= ini_.Delete(section_, "screen_content");
persist_config_migration |=
ini_.Delete(section_, "enable_desktop_quality_optimization");
persist_config_migration |=
ini_.Delete(section_, "enable_minimize_to_tray");
const long language_value =
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
@@ -109,8 +111,6 @@ int ConfigCenter::Load() {
enable_autostart_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
enable_minimize_to_tray_ = ini_.GetBoolValue(
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
portable_service_prompt_suppressed_ =
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
@@ -157,8 +157,6 @@ int ConfigCenter::Save() {
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_);
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
@@ -338,17 +336,6 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
return 0;
}
int ConfigCenter::SetMinimizeToTray(bool enable_minimize_to_tray) {
enable_minimize_to_tray_ = enable_minimize_to_tray;
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetAutostart(bool enable_autostart) {
enable_autostart_ = enable_autostart;
bool success = false;
@@ -448,8 +435,6 @@ int ConfigCenter::GetDefaultCoturnServerPort() const {
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
-3
View File
@@ -43,7 +43,6 @@ class ConfigCenter {
int SetServerPort(int signal_server_port);
int SetCoturnServerPort(int coturn_server_port);
int SetSelfHosted(bool enable_self_hosted);
int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon);
int SetPortableServicePromptSuppressed(bool suppressed);
@@ -66,7 +65,6 @@ class ConfigCenter {
int GetDefaultSignalServerPort() const;
int GetDefaultCoturnServerPort() const;
bool IsSelfHosted() const;
bool IsMinimizeToTray() const;
bool IsEnableAutostart() const;
bool IsEnableDaemon() const;
bool IsPortableServicePromptSuppressed() const;
@@ -94,7 +92,6 @@ class ConfigCenter {
int coturn_server_port_ = 0;
int coturn_server_port_default_ = 3478;
bool enable_self_hosted_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false;
bool enable_daemon_ = false;
bool portable_service_prompt_suppressed_ = false;
+26
View File
@@ -15,6 +15,7 @@
#include <string>
#include "display_info.h"
#include "remote_cursor_shape.h"
using json = nlohmann::json;
namespace crossdesk {
@@ -28,6 +29,7 @@ typedef enum {
service_status = 5,
service_command = 6,
keyboard_state = 7,
cursor_state = 8,
} ControlType;
typedef enum {
move = 0,
@@ -70,6 +72,12 @@ typedef struct {
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
} KeyboardState;
typedef struct {
uint32_t seq;
bool visible;
RemoteCursorShape shape;
} CursorState;
typedef struct {
char host_name[64];
size_t host_name_size;
@@ -96,6 +104,7 @@ struct RemoteAction {
Mouse m;
Key k;
KeyboardState ks;
CursorState cs;
HostInfo i;
bool a;
int d;
@@ -141,6 +150,11 @@ struct RemoteAction {
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
break;
}
case ControlType::cursor_state:
j["cursor_state"] = {{"seq", a.cs.seq},
{"visible", a.cs.visible},
{"shape", static_cast<int>(a.cs.shape)}};
break;
case ControlType::audio_capture:
j["audio_capture"] = a.a;
break;
@@ -219,6 +233,18 @@ struct RemoteAction {
out.ks.pressed_count = count;
break;
}
case ControlType::cursor_state: {
const auto& cursor_state_json = j.at("cursor_state");
const int shape = cursor_state_json.at("shape").get<int>();
if (shape < static_cast<int>(RemoteCursorShape::default_cursor) ||
shape > static_cast<int>(RemoteCursorShape::nwse_resize)) {
return false;
}
out.cs.seq = cursor_state_json.at("seq").get<uint32_t>();
out.cs.visible = cursor_state_json.at("visible").get<bool>();
out.cs.shape = static_cast<RemoteCursorShape>(shape);
break;
}
case ControlType::audio_capture:
out.a = j.at("audio_capture").get<bool>();
break;
+87 -20
View File
@@ -1163,22 +1163,24 @@ void GuiApplication::InitializeSystemTray() {
};
#if _WIN32
ui_->tray = std::make_unique<WinTray>(
std::move(show_window), std::move(hide_window), std::move(exit_app),
LoadSlintTrayIcon(), L"CrossDesk", localization_language_index_);
std::move(show_window), std::move(hide_window),
std::move(open_settings), std::move(exit_app), LoadSlintTrayIcon(),
L"CrossDesk", localization_language_index_);
#elif defined(__APPLE__)
ui_->tray = std::make_unique<MacTray>(
std::move(show_window), std::move(hide_window), std::move(open_settings),
std::move(exit_app), "CrossDesk", localization_language_index_);
#elif defined(__linux__)
ui_->tray = std::make_unique<LinuxTray>(
std::move(show_window), std::move(hide_window), std::move(exit_app),
"CrossDesk", localization_language_index_);
std::move(show_window), std::move(hide_window),
std::move(open_settings), std::move(exit_app), "CrossDesk",
localization_language_index_);
#endif
#endif
}
bool GuiApplication::MinimizeMainWindowToTray() {
if (!enable_minimize_to_tray_ || !ui_) {
if (!ui_) {
return false;
}
#if _WIN32 || defined(__APPLE__)
@@ -1208,7 +1210,6 @@ void GuiApplication::ResetSettingsUi() {
ui_->main->set_self_hosted_enabled(enable_self_hosted_);
ui_->main->set_autostart_enabled(enable_autostart_);
ui_->main->set_daemon_enabled(enable_daemon_);
ui_->main->set_minimize_to_tray_enabled(enable_minimize_to_tray_);
ui_->main->set_file_save_path(file_transfer_save_path_buf_);
ui_->main->set_server_host(signal_server_ip_self_);
ui_->main->set_server_port(signal_server_port_self_);
@@ -1218,12 +1219,10 @@ void GuiApplication::ResetSettingsUi() {
void GuiApplication::BindMainCallbacks() {
auto& main = ui_->main;
main->window().on_close_requested([this] {
if (MinimizeMainWindowToTray()) {
// MinimizeMainWindowToTray() ensures the tray icon exists. Let Slint
// complete the close request by hiding the main window as well.
return slint::CloseRequestResponse::HideWindow;
}
exit_ = true;
// Keep the application and all active sessions alive. The tray helper
// ensures that supported platforms still provide a way to restore the
// main window; Slint hides it regardless of tray availability.
MinimizeMainWindowToTray();
return slint::CloseRequestResponse::HideWindow;
});
main->on_main_title_drag([this](int phase, float x, float y) {
@@ -1240,11 +1239,7 @@ void GuiApplication::BindMainCallbacks() {
if (!ui_) {
return;
}
if (MinimizeMainWindowToTray()) {
ui_->main->hide();
return;
}
exit_ = true;
MinimizeMainWindowToTray();
ui_->main->hide();
});
main->on_copy_local_id([this] {
@@ -1801,6 +1796,7 @@ void GuiApplication::Tick() {
SyncStreamKeyboardFocus();
#endif
devices_.UpdateInteractions();
ShareLocalCursorState();
UpdateLocalization();
SyncMainWindow();
@@ -1810,6 +1806,63 @@ void GuiApplication::Tick() {
SyncServerWindow();
}
void GuiApplication::ShareLocalCursorState() {
constexpr auto kCursorStateHeartbeatInterval = 500ms;
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 (!is_server_mode_ || !peer_ || !has_connected_controller) {
has_shared_cursor_state_ = false;
last_cursor_state_share_time_ = {};
return;
}
CursorState sampled{};
if (!cursor_state_provider_.Sample(&sampled)) {
return;
}
const auto now = std::chrono::steady_clock::now();
const bool changed =
!has_shared_cursor_state_ ||
sampled.visible != last_shared_cursor_state_.visible ||
sampled.shape != last_shared_cursor_state_.shape;
const bool heartbeat_due =
last_cursor_state_share_time_.time_since_epoch().count() == 0 ||
now - last_cursor_state_share_time_ >= kCursorStateHeartbeatInterval;
if (!changed && !heartbeat_due) {
return;
}
sampled.seq = ++cursor_state_sequence_;
RemoteAction action{};
action.type = ControlType::cursor_state;
action.cs = sampled;
const std::string message = action.to_json();
const int result = SendDataFrame(peer_, message.c_str(), message.size(),
mouse_label_.c_str());
if (result != 0) {
LOG_WARN("Send cursor state failed, ret={}", result);
return;
}
if (changed) {
LOG_INFO("Sent cursor state: seq={}, visible={}, shape={}", sampled.seq,
sampled.visible, static_cast<int>(sampled.shape));
}
last_shared_cursor_state_ = sampled;
has_shared_cursor_state_ = true;
last_cursor_state_share_time_ = now;
}
void GuiApplication::HandlePasswordChangeResult() {
bool succeeded = false;
bool uncertain = false;
@@ -2369,6 +2422,23 @@ void GuiApplication::SyncStreamWindow() {
? localization::receiving_screen[localization_language_index_]
: std::string{}));
(*ui_->stream)->set_mouse_control_enabled(props->control_mouse_);
int remote_cursor_shape =
static_cast<int>(RemoteCursorShape::default_cursor);
bool remote_cursor_active = false;
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
remote_cursor_active =
status == ConnectionStatus::Connected && props->control_mouse_ &&
props->remote_cursor_state_received_;
if (remote_cursor_active) {
remote_cursor_shape = static_cast<int>(
props->remote_cursor_state_.visible
? props->remote_cursor_state_.shape
: RemoteCursorShape::none);
}
}
(*ui_->stream)->set_remote_cursor_active(remote_cursor_active);
(*ui_->stream)->set_remote_cursor_shape(remote_cursor_shape);
(*ui_->stream)->set_audio_enabled(props->audio_capture_button_pressed_);
#if defined(__APPLE__)
fullscreen_button_pressed_ = IsStreamWindowFullscreen();
@@ -2756,7 +2826,6 @@ void GuiApplication::SaveSettingsFromUi() {
enable_self_hosted_ = main->get_self_hosted_enabled();
enable_autostart_ = main->get_autostart_enabled();
enable_daemon_ = main->get_daemon_enabled();
enable_minimize_to_tray_ = main->get_minimize_to_tray_enabled();
localization_language_ =
static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
@@ -2775,7 +2844,6 @@ void GuiApplication::SaveSettingsFromUi() {
config_center_->SetSelfHosted(enable_self_hosted_);
config_center_->SetAutostart(enable_autostart_);
config_center_->SetDaemon(enable_daemon_);
config_center_->SetMinimizeToTray(enable_minimize_to_tray_);
const std::string path(main->get_file_save_path());
std::memset(file_transfer_save_path_buf_, 0,
@@ -2805,7 +2873,6 @@ void GuiApplication::SaveSettingsFromUi() {
enable_self_hosted_last_ = enable_self_hosted_;
enable_autostart_last_ = enable_autostart_;
enable_daemon_last_ = enable_daemon_;
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
file_transfer_save_path_last_ = path;
ui_->localized_language = -1;
+7
View File
@@ -5,6 +5,7 @@
#include <memory>
#include <string>
#include "runtime/cursor_state_provider.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
@@ -32,6 +33,7 @@ private:
void BindStreamCallbacks();
void BindServerCallbacks();
void Tick();
void ShareLocalCursorState();
void HandlePasswordChangeResult();
void HandleCredentialRecovery();
void SyncMainWindow();
@@ -70,6 +72,11 @@ private:
bool OpenUrl(const std::string &url);
std::unique_ptr<SlintUi> ui_;
CursorStateProvider cursor_state_provider_;
CursorState last_shared_cursor_state_{};
bool has_shared_cursor_state_ = false;
uint32_t cursor_state_sequence_ = 0;
std::chrono::steady_clock::time_point last_cursor_state_share_time_{};
std::chrono::steady_clock::time_point next_video_frame_time_{};
#if defined(__linux__) && !defined(__APPLE__)
bool use_xwayland_gui_ = false;
+4 -2
View File
@@ -134,8 +134,10 @@ void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
}
if (main_window_ &&
event.window.windowID == SDL_GetWindowID(main_window_) &&
MinimizeMainWindowToTray()) {
event.window.windowID == SDL_GetWindowID(main_window_)) {
if (!MinimizeMainWindowToTray()) {
SDL_HideWindow(main_window_);
}
break;
}
+1 -1
View File
@@ -150,7 +150,7 @@ int GuiApplication::DrawServerWindow() {
}
bool GuiApplication::MinimizeMainWindowToTray() {
if (!enable_minimize_to_tray_ || !main_window_) {
if (!main_window_) {
return false;
}
+51 -28
View File
@@ -135,35 +135,58 @@ inline std::string FormatDisplayLabel(size_t display_index,
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
#if _WIN32
inline const wchar_t* GetExitProgramLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
#if _WIN32
inline const wchar_t* GetWindowsLocalizedLabel(
int language_index, const char* key, const wchar_t* fallback,
std::vector<std::wstring>& cache) {
const int normalized_index = detail::ClampLanguageIndex(language_index);
std::wstring& cached_text = cache[normalized_index];
if (!cached_text.empty()) {
return cached_text.c_str();
}
const std::string& utf8_text =
detail::GetTranslatedText("exit_program", normalized_index);
if (utf8_text.empty()) {
cached_text = L"Exit";
return cached_text.c_str();
}
int wide_length =
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
if (wide_length <= 0) {
cached_text = L"Exit";
return cached_text.c_str();
}
cached_text.resize(static_cast<size_t>(wide_length - 1));
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, cached_text.data(),
wide_length);
return cached_text.c_str();
}
#endif
std::wstring& cached_text = cache[normalized_index];
if (!cached_text.empty()) {
return cached_text.c_str();
}
const std::string& utf8_text =
detail::GetTranslatedText(key, normalized_index);
if (utf8_text.empty()) {
cached_text = fallback;
return cached_text.c_str();
}
int wide_length =
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
if (wide_length <= 0) {
cached_text = fallback;
return cached_text.c_str();
}
cached_text.resize(static_cast<size_t>(wide_length));
if (MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1,
cached_text.data(), wide_length) <= 0) {
cached_text = fallback;
return cached_text.c_str();
}
cached_text.pop_back();
return cached_text.c_str();
}
inline const wchar_t* GetShowMainWindowLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
return GetWindowsLocalizedLabel(language_index, "show_main_window",
L"Show Main Window", cache);
}
inline const wchar_t* GetSettingsLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
return GetWindowsLocalizedLabel(language_index, "settings", L"Settings",
cache);
}
inline const wchar_t* GetExitProgramLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
return GetWindowsLocalizedLabel(language_index, "exit_program", L"Exit",
cache);
}
#endif
} // namespace localization
} // namespace crossdesk
@@ -215,8 +215,6 @@ struct TranslationRow {
X(file_transfer_save_path, u8"文件接收保存路径:", \
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
@@ -233,6 +231,8 @@ struct TranslationRow {
X(permission_required_message, u8"该应用需要授权以下权限:", \
"The application requires the following permissions:", \
u8"Для работы приложения требуются следующие разрешения:") \
X(show_main_window, u8"显示主界面", "Show Main Window", \
u8"Показать главное окно") \
X(exit_program, u8"退出", "Exit", u8"Выход")
inline constexpr TranslationRow kTranslationRows[] = {
@@ -310,7 +310,6 @@ int SettingsManager::Load() {
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart();
owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon();
owner_.enable_minimize_to_tray_ = owner_.config_center_->IsMinimizeToTray();
#if _WIN32 && CROSSDESK_PORTABLE
owner_.portable_service_prompt_suppressed_ =
owner_.config_center_->IsPortableServicePromptSuppressed();
@@ -333,7 +332,6 @@ int SettingsManager::Load() {
owner_.enable_srtp_last_ = owner_.enable_srtp_;
owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_;
owner_.enable_autostart_last_ = owner_.enable_autostart_;
owner_.enable_minimize_to_tray_last_ = owner_.enable_minimize_to_tray_;
LOG_INFO("Load settings from cache file");
return 0;
+556 -68
View File
@@ -9,6 +9,7 @@
#include <X11/Xutil.h>
#include <X11/extensions/shape.h>
#include <X11/extensions/Xrender.h>
#include <dlfcn.h>
#include <algorithm>
#include <chrono>
@@ -32,14 +33,121 @@ namespace crossdesk {
namespace {
constexpr int kTrayIconSize = 24;
constexpr int kMenuHeight = 28;
constexpr int kMenuItemHeight = 28;
constexpr int kMenuItemCount = 3;
constexpr int kMenuVerticalPadding = 4;
constexpr int kMenuHeight =
kMenuItemHeight * kMenuItemCount + kMenuVerticalPadding * 2;
constexpr int kMenuHorizontalPadding = 12;
constexpr int kMenuCornerRadius = 8;
constexpr int kMenuBorderWidth = 1;
constexpr int kMenuItemHorizontalInset = 4;
constexpr int kMenuItemVerticalInset = 2;
constexpr int kMenuItemCornerRadius = 5;
constexpr int kMenuSeparatorHorizontalInset = 8;
constexpr int kDockTimeoutMs = 800;
constexpr int kMaxTrayEventsPerTick = 32;
constexpr int kMaxTrayEventsDuringEmbed = 16;
constexpr long kSystemTrayRequestDock = 0;
constexpr long kXEmbedMapped = 1;
constexpr long kXEmbedEmbeddedNotify = 0;
constexpr int kAppIndicatorCategoryApplicationStatus = 0;
constexpr int kAppIndicatorStatusPassive = 0;
constexpr int kAppIndicatorStatusActive = 1;
// AppIndicator exports the menu to the desktop shell, which lets GNOME, KDE,
// and compatible panels render it with their own theme. Load either maintained
// implementation dynamically so the legacy XEmbed path remains available.
struct DesktopIndicatorApi {
using SignalCallback = void (*)(void*, void*);
using SignalDestroyNotify = void (*)(void*, void*);
void* library = nullptr;
void* (*indicator_new_with_path)(const char*, const char*, int,
const char*) = nullptr;
void (*indicator_set_status)(void*, int) = nullptr;
void (*indicator_set_menu)(void*, void*) = nullptr;
void (*indicator_set_title)(void*, const char*) = nullptr;
int (*gtk_init_check)(int*, char***) = nullptr;
void* (*gtk_menu_new)() = nullptr;
void* (*gtk_menu_item_new_with_label)(const char*) = nullptr;
void* (*gtk_separator_menu_item_new)() = nullptr;
void (*gtk_menu_shell_append)(void*, void*) = nullptr;
void (*gtk_widget_show_all)(void*) = nullptr;
void (*gtk_widget_destroy)(void*) = nullptr;
int (*gtk_events_pending)() = nullptr;
int (*gtk_main_iteration_do)(int) = nullptr;
unsigned long (*signal_connect_data)(void*, const char*, SignalCallback,
void*, SignalDestroyNotify,
int) = nullptr;
void (*object_unref)(void*) = nullptr;
};
template <typename T>
bool LoadDesktopIndicatorSymbol(void* library, T* function,
const char* symbol_name) {
*function = reinterpret_cast<T>(dlsym(library, symbol_name));
return *function != nullptr;
}
void UnloadDesktopIndicatorApi(DesktopIndicatorApi* api) {
if (api->library) {
dlclose(api->library);
}
*api = DesktopIndicatorApi{};
}
bool LoadDesktopIndicatorApi(DesktopIndicatorApi* api) {
for (const char* library_name : {"libayatana-appindicator3.so.1",
"libappindicator3.so.1"}) {
api->library = dlopen(library_name, RTLD_NOW | RTLD_LOCAL);
if (api->library) {
break;
}
}
if (!api->library) {
return false;
}
const bool loaded =
LoadDesktopIndicatorSymbol(api->library,
&api->indicator_new_with_path,
"app_indicator_new_with_path") &&
LoadDesktopIndicatorSymbol(api->library, &api->indicator_set_status,
"app_indicator_set_status") &&
LoadDesktopIndicatorSymbol(api->library, &api->indicator_set_menu,
"app_indicator_set_menu") &&
LoadDesktopIndicatorSymbol(api->library, &api->indicator_set_title,
"app_indicator_set_title") &&
LoadDesktopIndicatorSymbol(api->library, &api->gtk_init_check,
"gtk_init_check") &&
LoadDesktopIndicatorSymbol(api->library, &api->gtk_menu_new,
"gtk_menu_new") &&
LoadDesktopIndicatorSymbol(api->library,
&api->gtk_menu_item_new_with_label,
"gtk_menu_item_new_with_label") &&
LoadDesktopIndicatorSymbol(api->library,
&api->gtk_separator_menu_item_new,
"gtk_separator_menu_item_new") &&
LoadDesktopIndicatorSymbol(api->library, &api->gtk_menu_shell_append,
"gtk_menu_shell_append") &&
LoadDesktopIndicatorSymbol(api->library, &api->gtk_widget_show_all,
"gtk_widget_show_all") &&
LoadDesktopIndicatorSymbol(api->library, &api->gtk_widget_destroy,
"gtk_widget_destroy") &&
LoadDesktopIndicatorSymbol(api->library, &api->gtk_events_pending,
"gtk_events_pending") &&
LoadDesktopIndicatorSymbol(api->library, &api->gtk_main_iteration_do,
"gtk_main_iteration_do") &&
LoadDesktopIndicatorSymbol(api->library, &api->signal_connect_data,
"g_signal_connect_data") &&
LoadDesktopIndicatorSymbol(api->library, &api->object_unref,
"g_object_unref");
if (!loaded) {
UnloadDesktopIndicatorApi(api);
}
return loaded;
}
bool IsAsciiPrintable(const std::string& text) {
for (unsigned char ch : text) {
@@ -50,7 +158,21 @@ bool IsAsciiPrintable(const std::string& text) {
return true;
}
std::string GetMenuLabel(int language_index) {
std::string GetShowMainWindowMenuLabel(int language_index) {
const int normalized_index = localization::detail::ClampLanguageIndex(
language_index);
const std::string& label = localization::show_main_window[normalized_index];
return label.empty() ? "Show Main Window" : label;
}
std::string GetSettingsMenuLabel(int language_index) {
const int normalized_index = localization::detail::ClampLanguageIndex(
language_index);
const std::string& label = localization::settings[normalized_index];
return label.empty() ? "Settings" : label;
}
std::string GetExitMenuLabel(int language_index) {
const int normalized_index = localization::detail::ClampLanguageIndex(
language_index);
const std::string& label = localization::exit_program[normalized_index];
@@ -73,6 +195,62 @@ unsigned long AllocateColor(Display* display, int screen, const char* name,
return fallback;
}
void FillRoundedRectangle(Display* display, Drawable drawable, GC gc, int x,
int y, int width, int height, int radius) {
if (!display || !drawable || !gc || width <= 0 || height <= 0) {
return;
}
const int clamped_radius =
std::clamp(radius, 0, std::min(width, height) / 2);
if (clamped_radius == 0) {
XFillRectangle(display, drawable, gc, x, y, width, height);
return;
}
const int diameter = clamped_radius * 2;
XFillRectangle(display, drawable, gc, x + clamped_radius, y,
width - diameter, height);
XFillRectangle(display, drawable, gc, x, y + clamped_radius, width,
height - diameter);
XFillArc(display, drawable, gc, x, y, diameter, diameter, 0, 360 * 64);
XFillArc(display, drawable, gc, x + width - diameter, y, diameter, diameter,
0, 360 * 64);
XFillArc(display, drawable, gc, x, y + height - diameter, diameter, diameter,
0, 360 * 64);
XFillArc(display, drawable, gc, x + width - diameter,
y + height - diameter, diameter, diameter, 0, 360 * 64);
}
bool IsPointInsideRoundedRectangle(int point_x, int point_y, int x, int y,
int width, int height, int radius) {
if (point_x < x || point_y < y || point_x >= x + width ||
point_y >= y + height) {
return false;
}
const int clamped_radius =
std::clamp(radius, 0, std::min(width, height) / 2);
if (clamped_radius == 0 ||
(point_x >= x + clamped_radius &&
point_x < x + width - clamped_radius) ||
(point_y >= y + clamped_radius &&
point_y < y + height - clamped_radius)) {
return true;
}
const int center_x = point_x < x + clamped_radius
? x + clamped_radius
: x + width - clamped_radius - 1;
const int center_y = point_y < y + clamped_radius
? y + clamped_radius
: y + height - clamped_radius - 1;
const int delta_x = point_x - center_x;
const int delta_y = point_y - center_y;
return delta_x * delta_x + delta_y * delta_y <=
clamped_radius * clamped_radius;
}
std::vector<std::filesystem::path> BuildIconCandidatePaths() {
std::vector<std::filesystem::path> paths = {
"icons/linux/crossdesk_32x32.png",
@@ -117,6 +295,22 @@ std::vector<std::filesystem::path> BuildIconCandidatePaths() {
return paths;
}
std::pair<std::string, std::string> ResolveDesktopIndicatorIcon() {
for (const auto& candidate : BuildIconCandidatePaths()) {
std::error_code error;
if (!std::filesystem::exists(candidate, error) || error) {
continue;
}
const auto absolute_path = std::filesystem::absolute(candidate, error);
if (!error) {
return {absolute_path.stem().string(),
absolute_path.parent_path().string()};
}
}
return {"crossdesk", {}};
}
int CountMaskBits(unsigned long mask) {
int count = 0;
while (mask) {
@@ -187,20 +381,42 @@ struct LinuxTrayImpl {
tooltip(std::move(tray_tooltip)),
language_index(language_index_value),
exit_event_type(tray_exit_event_type),
menu_label(GetMenuLabel(language_index_value)),
menu_ascii_label(IsAsciiPrintable(menu_label) ? menu_label : "Exit") {}
show_menu_label(GetShowMainWindowMenuLabel(language_index_value)),
show_menu_ascii_label(IsAsciiPrintable(show_menu_label)
? show_menu_label
: "Show Main Window"),
settings_menu_label(GetSettingsMenuLabel(language_index_value)),
settings_menu_ascii_label(IsAsciiPrintable(settings_menu_label)
? settings_menu_label
: "Settings"),
exit_menu_label(GetExitMenuLabel(language_index_value)),
exit_menu_ascii_label(IsAsciiPrintable(exit_menu_label)
? exit_menu_label
: "Exit") {}
explicit LinuxTrayImpl(std::function<void()> show_window_callback,
std::function<void()> hide_window_callback,
std::function<void()> open_settings_callback,
std::function<void()> exit_callback,
std::string tray_tooltip, int language_index_value)
: show_window(std::move(show_window_callback)),
hide_window(std::move(hide_window_callback)),
open_settings(std::move(open_settings_callback)),
exit_app(std::move(exit_callback)),
tooltip(std::move(tray_tooltip)),
language_index(language_index_value),
menu_label(GetMenuLabel(language_index_value)),
menu_ascii_label(IsAsciiPrintable(menu_label) ? menu_label : "Exit") {}
show_menu_label(GetShowMainWindowMenuLabel(language_index_value)),
show_menu_ascii_label(IsAsciiPrintable(show_menu_label)
? show_menu_label
: "Show Main Window"),
settings_menu_label(GetSettingsMenuLabel(language_index_value)),
settings_menu_ascii_label(IsAsciiPrintable(settings_menu_label)
? settings_menu_label
: "Settings"),
exit_menu_label(GetExitMenuLabel(language_index_value)),
exit_menu_ascii_label(IsAsciiPrintable(exit_menu_label)
? exit_menu_label
: "Exit") {}
~LinuxTrayImpl() { RemoveTrayIcon(); }
@@ -218,6 +434,7 @@ struct LinuxTrayImpl {
}
void RemoveTrayIcon() {
RemoveDesktopIndicator();
HideMenu();
if (display && icon_window) {
@@ -264,9 +481,137 @@ struct LinuxTrayImpl {
tray_manager_window = 0;
}
void ProcessEvents() { ProcessPendingEvents(kMaxTrayEventsPerTick); }
void ProcessEvents() {
ProcessDesktopIndicatorEvents();
ProcessPendingEvents(kMaxTrayEventsPerTick);
}
private:
static void OnDesktopShowWindow(void*, void* user_data) {
auto* self = static_cast<LinuxTrayImpl*>(user_data);
if (self) {
self->ShowWindow();
}
}
static void OnDesktopOpenSettings(void*, void* user_data) {
auto* self = static_cast<LinuxTrayImpl*>(user_data);
if (self) {
self->OpenSettings();
}
}
static void OnDesktopExit(void*, void* user_data) {
auto* self = static_cast<LinuxTrayImpl*>(user_data);
if (self) {
self->RequestExit();
}
}
bool EnsureDesktopIndicator() {
if (desktop_indicator) {
return true;
}
if (desktop_indicator_attempted) {
return false;
}
desktop_indicator_attempted = true;
if (!LoadDesktopIndicatorApi(&desktop_indicator_api)) {
LOG_INFO(
"Desktop AppIndicator runtime unavailable; using XEmbed tray "
"fallback");
return false;
}
if (!desktop_indicator_api.gtk_init_check(nullptr, nullptr)) {
LOG_INFO(
"GTK could not connect to the desktop; using XEmbed tray fallback");
return false;
}
desktop_menu = desktop_indicator_api.gtk_menu_new();
void* show_item = desktop_indicator_api.gtk_menu_item_new_with_label(
show_menu_label.c_str());
void* settings_item = desktop_indicator_api.gtk_menu_item_new_with_label(
settings_menu_label.c_str());
void* separator = desktop_indicator_api.gtk_separator_menu_item_new();
void* exit_item = desktop_indicator_api.gtk_menu_item_new_with_label(
exit_menu_label.c_str());
if (!desktop_menu || !show_item || !settings_item || !separator ||
!exit_item) {
if (desktop_menu) {
desktop_indicator_api.gtk_widget_destroy(desktop_menu);
desktop_menu = nullptr;
}
return false;
}
desktop_indicator_api.signal_connect_data(
show_item, "activate", &LinuxTrayImpl::OnDesktopShowWindow, this,
nullptr, 0);
desktop_indicator_api.signal_connect_data(
settings_item, "activate", &LinuxTrayImpl::OnDesktopOpenSettings, this,
nullptr, 0);
desktop_indicator_api.signal_connect_data(
exit_item, "activate", &LinuxTrayImpl::OnDesktopExit, this, nullptr, 0);
desktop_indicator_api.gtk_menu_shell_append(desktop_menu, show_item);
desktop_indicator_api.gtk_menu_shell_append(desktop_menu, settings_item);
desktop_indicator_api.gtk_menu_shell_append(desktop_menu, separator);
desktop_indicator_api.gtk_menu_shell_append(desktop_menu, exit_item);
desktop_indicator_api.gtk_widget_show_all(desktop_menu);
const auto [icon_name, icon_path] = ResolveDesktopIndicatorIcon();
desktop_indicator = desktop_indicator_api.indicator_new_with_path(
"crossdesk", icon_name.c_str(),
kAppIndicatorCategoryApplicationStatus,
icon_path.empty() ? nullptr : icon_path.c_str());
if (!desktop_indicator) {
desktop_indicator_api.gtk_widget_destroy(desktop_menu);
desktop_menu = nullptr;
return false;
}
desktop_indicator_api.indicator_set_title(desktop_indicator,
tooltip.c_str());
desktop_indicator_api.indicator_set_menu(desktop_indicator, desktop_menu);
desktop_indicator_api.indicator_set_status(desktop_indicator,
kAppIndicatorStatusActive);
ProcessDesktopIndicatorEvents();
LOG_INFO("Linux tray registered through the desktop AppIndicator service");
return true;
}
void RemoveDesktopIndicator() {
if (desktop_indicator) {
desktop_indicator_api.indicator_set_status(
desktop_indicator, kAppIndicatorStatusPassive);
}
if (desktop_menu) {
desktop_indicator_api.gtk_widget_destroy(desktop_menu);
desktop_menu = nullptr;
}
if (desktop_indicator) {
desktop_indicator_api.object_unref(desktop_indicator);
desktop_indicator = nullptr;
}
// GTK registers process-wide types and callbacks. Keep the dynamically
// loaded module resident after initialization and let process teardown
// release it, instead of invalidating those registrations with dlclose().
}
void ProcessDesktopIndicatorEvents() {
if (!desktop_indicator) {
return;
}
int processed = 0;
while (processed < kMaxTrayEventsPerTick &&
desktop_indicator_api.gtk_events_pending()) {
desktop_indicator_api.gtk_main_iteration_do(0);
++processed;
}
}
int ProcessPendingEvents(int max_events) {
if (!display) {
return 0;
@@ -314,6 +659,8 @@ struct LinuxTrayImpl {
SelectArgbVisual();
brand_pixel = AllocateColor(display, screen, "#2563eb", black_pixel);
hover_pixel = AllocateColor(display, screen, "#e5e7eb", white_pixel);
menu_border_pixel =
AllocateColor(display, screen, "#d1d5db", black_pixel);
int shape_event_base = 0;
int shape_error_base = 0;
shape_available =
@@ -328,6 +675,10 @@ struct LinuxTrayImpl {
xembed_info_atom = XInternAtom(display, "_XEMBED_INFO", False);
utf8_string_atom = XInternAtom(display, "UTF8_STRING", False);
net_wm_name_atom = XInternAtom(display, "_NET_WM_NAME", False);
net_wm_window_type_atom =
XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
net_wm_window_type_popup_menu_atom =
XInternAtom(display, "_NET_WM_WINDOW_TYPE_POPUP_MENU", False);
char** missing_charset_list = nullptr;
int missing_charset_count = 0;
@@ -386,6 +737,9 @@ struct LinuxTrayImpl {
}
bool EnsureTrayIcon() {
if (EnsureDesktopIndicator()) {
return true;
}
if (docked && icon_window) {
return true;
}
@@ -540,6 +894,17 @@ struct LinuxTrayImpl {
HandleButtonRelease(event.xbutton);
break;
case MotionNotify:
if (event.xmotion.window == menu_window) {
const int hovered_item =
MenuItemAt(event.xmotion.x, event.xmotion.y);
if (hovered_item != hovered_menu_item) {
hovered_menu_item = hovered_item;
DrawMenu();
}
}
break;
case LeaveNotify:
if (event.xcrossing.window == menu_window) {
HideMenu();
@@ -553,12 +918,14 @@ struct LinuxTrayImpl {
void HandleButtonRelease(const XButtonEvent& event) {
if (menu_window && event.window == menu_window) {
const bool inside_menu =
event.x >= 0 && event.y >= 0 && event.x < menu_width &&
event.y < kMenuHeight;
const bool activate_exit = inside_menu && event.button == Button1;
const int selected_item =
event.button == Button1 ? MenuItemAt(event.x, event.y) : -1;
HideMenu();
if (activate_exit) {
if (selected_item == 0) {
ShowWindow();
} else if (selected_item == 1) {
OpenSettings();
} else if (selected_item == 2) {
RequestExit();
}
return;
@@ -571,9 +938,7 @@ struct LinuxTrayImpl {
return;
}
if (event.button == Button1) {
ShowWindow();
} else if (event.button == Button3) {
if (event.button == Button1 || event.button == Button3) {
ShowMenu(event.x_root, event.y_root);
}
}
@@ -824,29 +1189,76 @@ struct LinuxTrayImpl {
return false;
}
int MenuTextWidth() const {
int MenuTextWidth(const std::string& label,
const std::string& ascii_label) const {
if (menu_font) {
XGlyphInfo extents{};
XftTextExtentsUtf8(display, menu_font,
reinterpret_cast<const FcChar8*>(menu_label.c_str()),
static_cast<int>(menu_label.size()), &extents);
reinterpret_cast<const FcChar8*>(label.c_str()),
static_cast<int>(label.size()), &extents);
return extents.xOff;
}
if (font_set) {
XRectangle ink{};
XRectangle logical{};
Xutf8TextExtents(font_set, menu_label.c_str(),
static_cast<int>(menu_label.size()), &ink, &logical);
Xutf8TextExtents(font_set, label.c_str(), static_cast<int>(label.size()),
&ink, &logical);
return logical.width;
}
if (fallback_font) {
return XTextWidth(fallback_font, menu_ascii_label.c_str(),
static_cast<int>(menu_ascii_label.size()));
return XTextWidth(fallback_font, ascii_label.c_str(),
static_cast<int>(ascii_label.size()));
}
return static_cast<int>(menu_ascii_label.size()) * 8;
return static_cast<int>(ascii_label.size()) * 8;
}
int MenuItemAt(int x, int y) const {
if (!IsPointInsideRoundedRectangle(x, y, 0, 0, menu_width, kMenuHeight,
kMenuCornerRadius)) {
return -1;
}
const int content_y = y - kMenuVerticalPadding;
if (content_y < 0 ||
content_y >= kMenuItemHeight * kMenuItemCount) {
return -1;
}
return content_y / kMenuItemHeight;
}
void ApplyMenuShape() {
if (!shape_available || !display || !menu_window) {
return;
}
Pixmap shape_mask =
XCreatePixmap(display, menu_window, menu_width, kMenuHeight, 1);
if (!shape_mask) {
return;
}
GC shape_gc = XCreateGC(display, shape_mask, 0, nullptr);
if (!shape_gc) {
XFreePixmap(display, shape_mask);
return;
}
XSetForeground(display, shape_gc, 0);
XFillRectangle(display, shape_mask, shape_gc, 0, 0, menu_width,
kMenuHeight);
XSetForeground(display, shape_gc, 1);
FillRoundedRectangle(display, shape_mask, shape_gc, 0, 0, menu_width,
kMenuHeight, kMenuCornerRadius);
XShapeCombineMask(display, menu_window, ShapeBounding, 0, 0, shape_mask,
ShapeSet);
XShapeCombineMask(display, menu_window, ShapeInput, 0, 0, shape_mask,
ShapeSet);
XFreeGC(display, shape_gc);
XFreePixmap(display, shape_mask);
}
void ShowMenu(int root_x, int root_y) {
@@ -856,7 +1268,15 @@ struct LinuxTrayImpl {
HideMenu();
menu_width = std::max(72, MenuTextWidth() + kMenuHorizontalPadding * 2);
menu_width =
std::max({72,
MenuTextWidth(show_menu_label, show_menu_ascii_label) +
kMenuHorizontalPadding * 2,
MenuTextWidth(settings_menu_label,
settings_menu_ascii_label) +
kMenuHorizontalPadding * 2,
MenuTextWidth(exit_menu_label, exit_menu_ascii_label) +
kMenuHorizontalPadding * 2});
const int display_width = DisplayWidth(display, screen);
const int display_height = DisplayHeight(display, screen);
const int x = std::clamp(root_x, 0, std::max(0, display_width - menu_width));
@@ -865,23 +1285,31 @@ struct LinuxTrayImpl {
XSetWindowAttributes attrs{};
attrs.override_redirect = True;
attrs.background_pixel = white_pixel;
attrs.border_pixel = black_pixel;
attrs.background_pixel = menu_border_pixel;
attrs.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask |
LeaveWindowMask;
PointerMotionMask | LeaveWindowMask;
menu_window = XCreateWindow(
display, root_window, x, y, menu_width, kMenuHeight, 1, CopyFromParent,
display, root_window, x, y, menu_width, kMenuHeight, 0, CopyFromParent,
InputOutput, CopyFromParent,
CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &attrs);
CWOverrideRedirect | CWBackPixel | CWEventMask, &attrs);
if (!menu_window) {
return;
}
if (net_wm_window_type_atom && net_wm_window_type_popup_menu_atom) {
const Atom window_type = net_wm_window_type_popup_menu_atom;
XChangeProperty(display, menu_window, net_wm_window_type_atom, XA_ATOM,
32, PropModeReplace,
reinterpret_cast<const unsigned char*>(&window_type), 1);
}
ApplyMenuShape();
menu_visible = true;
hovered_menu_item = -1;
XMapRaised(display, menu_window);
XGrabPointer(display, menu_window, False,
ButtonPressMask | ButtonReleaseMask, GrabModeAsync,
GrabModeAsync, None, None, CurrentTime);
ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
DrawMenu();
XFlush(display);
}
@@ -889,6 +1317,7 @@ struct LinuxTrayImpl {
void HideMenu() {
if (!display || !menu_window) {
menu_visible = false;
hovered_menu_item = -1;
return;
}
@@ -896,6 +1325,7 @@ struct LinuxTrayImpl {
XDestroyWindow(display, menu_window);
menu_window = 0;
menu_visible = false;
hovered_menu_item = -1;
XFlush(display);
}
@@ -905,47 +1335,82 @@ struct LinuxTrayImpl {
}
GC gc = XCreateGC(display, menu_window, 0, nullptr);
XSetForeground(display, gc, menu_border_pixel);
FillRoundedRectangle(display, menu_window, gc, 0, 0, menu_width,
kMenuHeight, kMenuCornerRadius);
XSetForeground(display, gc, white_pixel);
XFillRectangle(display, menu_window, gc, 0, 0, menu_width, kMenuHeight);
FillRoundedRectangle(
display, menu_window, gc, kMenuBorderWidth, kMenuBorderWidth,
menu_width - kMenuBorderWidth * 2,
kMenuHeight - kMenuBorderWidth * 2,
kMenuCornerRadius - kMenuBorderWidth);
if (hovered_menu_item >= 0 && hovered_menu_item < kMenuItemCount) {
XSetForeground(display, gc, hover_pixel);
FillRoundedRectangle(
display, menu_window, gc, kMenuItemHorizontalInset,
kMenuVerticalPadding + hovered_menu_item * kMenuItemHeight +
kMenuItemVerticalInset,
menu_width - kMenuItemHorizontalInset * 2,
kMenuItemHeight - kMenuItemVerticalInset * 2,
kMenuItemCornerRadius);
}
XSetForeground(display, gc, hover_pixel);
XFillRectangle(display, menu_window, gc, 1, 1, menu_width - 2,
kMenuHeight - 2);
XDrawLine(display, menu_window, gc, kMenuSeparatorHorizontalInset,
kMenuVerticalPadding + kMenuItemHeight * 2,
menu_width - kMenuSeparatorHorizontalInset,
kMenuVerticalPadding + kMenuItemHeight * 2);
XSetForeground(display, gc, black_pixel);
int baseline = kMenuHeight / 2 + 5;
if (menu_font && menu_text_color_allocated) {
XftDraw* draw = XftDrawCreate(display, menu_window,
DefaultVisual(display, screen),
DefaultColormap(display, screen));
if (draw) {
baseline = (kMenuHeight - (menu_font->ascent + menu_font->descent)) /
XftDraw* xft_draw = menu_font && menu_text_color_allocated
? XftDrawCreate(display, menu_window,
DefaultVisual(display, screen),
DefaultColormap(display, screen))
: nullptr;
auto draw_label = [&](int item_index, const std::string& label,
const std::string& ascii_label) {
const int item_y =
kMenuVerticalPadding + item_index * kMenuItemHeight;
int baseline = item_y + kMenuItemHeight / 2 + 5;
if (xft_draw) {
baseline = item_y +
(kMenuItemHeight -
(menu_font->ascent + menu_font->descent)) /
2 +
menu_font->ascent;
XftDrawStringUtf8(
draw, &menu_text_color, menu_font, kMenuHorizontalPadding, baseline,
reinterpret_cast<const FcChar8*>(menu_label.c_str()),
static_cast<int>(menu_label.size()));
XftDrawDestroy(draw);
xft_draw, &menu_text_color, menu_font, kMenuHorizontalPadding,
baseline, reinterpret_cast<const FcChar8*>(label.c_str()),
static_cast<int>(label.size()));
} else if (font_set) {
XFontSetExtents* extents = XExtentsOfFontSet(font_set);
if (extents) {
baseline = item_y +
(kMenuItemHeight -
extents->max_logical_extent.height) /
2 -
extents->max_logical_extent.y;
}
Xutf8DrawString(display, menu_window, font_set, gc,
kMenuHorizontalPadding, baseline, label.c_str(),
static_cast<int>(label.size()));
} else {
if (fallback_font) {
XSetFont(display, gc, fallback_font->fid);
baseline = item_y +
(kMenuItemHeight + fallback_font->ascent -
fallback_font->descent) /
2;
}
XDrawString(display, menu_window, gc, kMenuHorizontalPadding, baseline,
ascii_label.c_str(),
static_cast<int>(ascii_label.size()));
}
} else if (font_set) {
XFontSetExtents* extents = XExtentsOfFontSet(font_set);
if (extents) {
baseline = (kMenuHeight - extents->max_logical_extent.height) / 2 -
extents->max_logical_extent.y;
}
Xutf8DrawString(display, menu_window, font_set, gc,
kMenuHorizontalPadding, baseline, menu_label.c_str(),
static_cast<int>(menu_label.size()));
} else {
if (fallback_font) {
XSetFont(display, gc, fallback_font->fid);
baseline = (kMenuHeight + fallback_font->ascent -
fallback_font->descent) /
2;
}
XDrawString(display, menu_window, gc, kMenuHorizontalPadding, baseline,
menu_ascii_label.c_str(),
static_cast<int>(menu_ascii_label.size()));
};
draw_label(0, show_menu_label, show_menu_ascii_label);
draw_label(1, settings_menu_label, settings_menu_ascii_label);
draw_label(2, exit_menu_label, exit_menu_ascii_label);
if (xft_draw) {
XftDrawDestroy(xft_draw);
}
XFreeGC(display, gc);
@@ -962,6 +1427,14 @@ struct LinuxTrayImpl {
}
}
void OpenSettings() {
if (open_settings) {
open_settings();
} else {
ShowWindow();
}
}
void RequestExit() {
if (exit_app) {
exit_app();
@@ -978,12 +1451,21 @@ struct LinuxTrayImpl {
::SDL_Window* app_window = nullptr;
std::function<void()> show_window;
std::function<void()> hide_window;
std::function<void()> open_settings;
std::function<void()> exit_app;
std::string tooltip;
int language_index = 0;
uint32_t exit_event_type = 0;
std::string menu_label;
std::string menu_ascii_label;
std::string show_menu_label;
std::string show_menu_ascii_label;
std::string settings_menu_label;
std::string settings_menu_ascii_label;
std::string exit_menu_label;
std::string exit_menu_ascii_label;
DesktopIndicatorApi desktop_indicator_api;
void* desktop_indicator = nullptr;
void* desktop_menu = nullptr;
bool desktop_indicator_attempted = false;
Display* display = nullptr;
int screen = 0;
::Window root_window = 0;
@@ -996,10 +1478,13 @@ struct LinuxTrayImpl {
Atom xembed_info_atom = None;
Atom utf8_string_atom = None;
Atom net_wm_name_atom = None;
Atom net_wm_window_type_atom = None;
Atom net_wm_window_type_popup_menu_atom = None;
unsigned long black_pixel = 0;
unsigned long white_pixel = 0;
unsigned long brand_pixel = 0;
unsigned long hover_pixel = 0;
unsigned long menu_border_pixel = 0;
Visual* icon_visual = nullptr;
int icon_depth = 0;
Colormap icon_colormap = 0;
@@ -1014,6 +1499,7 @@ struct LinuxTrayImpl {
int icon_width = 0;
int icon_height = 0;
int menu_width = 72;
int hovered_menu_item = -1;
bool docked = false;
bool embedded = false;
bool menu_visible = false;
@@ -1028,11 +1514,13 @@ LinuxTray::LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
LinuxTray::LinuxTray(std::function<void()> show_window,
std::function<void()> hide_window,
std::function<void()> open_settings,
std::function<void()> exit_app,
const std::string& tooltip, int language_index)
: impl_(std::make_unique<LinuxTrayImpl>(
std::move(show_window), std::move(hide_window), std::move(exit_app),
tooltip, language_index)) {}
std::move(show_window), std::move(hide_window),
std::move(open_settings), std::move(exit_app), tooltip,
language_index)) {}
LinuxTray::~LinuxTray() = default;
+3 -1
View File
@@ -25,7 +25,9 @@ class LinuxTray {
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index, uint32_t exit_event_type);
LinuxTray(std::function<void()> show_window,
std::function<void()> hide_window, std::function<void()> exit_app,
std::function<void()> hide_window,
std::function<void()> open_settings,
std::function<void()> exit_app,
const std::string& tooltip, int language_index);
~LinuxTray();
+22 -8
View File
@@ -13,6 +13,7 @@
@interface CrossDeskMacTrayTarget : NSObject
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
- (void)statusItemClicked:(id)sender;
- (void)showMainWindow:(id)sender;
- (void)openSettings:(id)sender;
- (void)exitApplication:(id)sender;
@end
@@ -84,6 +85,19 @@ struct MacTrayImpl {
}
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
[menu setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameAqua]];
NSString *show_main_window_title =
NSStringFromUtf8(localization::show_main_window
[localization::detail::ClampLanguageIndex(
language_index)]);
NSMenuItem *show_main_window_item =
[[NSMenuItem alloc] initWithTitle:show_main_window_title
action:@selector(showMainWindow:)
keyEquivalent:@""];
[show_main_window_item setTarget:target];
[menu addItem:show_main_window_item];
NSString *settings_title =
NSStringFromUtf8(localization::settings
[localization::detail::ClampLanguageIndex(
@@ -287,14 +301,7 @@ void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
if (!owner_) {
return;
}
NSEvent *event = [NSApp currentEvent];
if (event && [event type] == NSEventTypeRightMouseUp) {
owner_->ShowMenu();
return;
}
owner_->ShowWindow();
owner_->ShowMenu();
}
- (void)exitApplication:(id)sender {
@@ -304,6 +311,13 @@ void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
}
}
- (void)showMainWindow:(id)sender {
(void)sender;
if (owner_) {
owner_->ShowWindow();
}
}
- (void)openSettings:(id)sender {
(void)sender;
if (owner_) {
+29
View File
@@ -64,11 +64,13 @@ WinTray::WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
WinTray::WinTray(std::function<void()> show_window,
std::function<void()> hide_window,
std::function<void()> open_settings,
std::function<void()> exit_app, HICON icon,
const std::wstring& tooltip, int language_index)
: WinTray(nullptr, icon, tooltip, language_index) {
show_window_ = std::move(show_window);
hide_window_ = std::move(hide_window);
open_settings_ = std::move(open_settings);
exit_app_ = std::move(exit_app);
}
@@ -97,6 +99,14 @@ void WinTray::ShowApplicationWindow() {
}
}
void WinTray::OpenSettings() {
if (open_settings_) {
open_settings_();
} else {
ShowApplicationWindow();
}
}
bool WinTray::HandleTrayMessage(MSG* msg) {
if (!msg || msg->message != WM_TRAY_CALLBACK) return false;
@@ -111,6 +121,20 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
POINT pt;
GetCursorPos(&pt);
HMENU menu = CreatePopupMenu();
HBRUSH menu_background = CreateSolidBrush(RGB(255, 255, 255));
if (menu_background) {
MENUINFO menu_info{};
menu_info.cbSize = sizeof(menu_info);
menu_info.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS;
menu_info.hbrBack = menu_background;
SetMenuInfo(menu, &menu_info);
}
AppendMenuW(menu, MF_STRING, 1002,
localization::GetShowMainWindowLabel(language_index_));
AppendMenuW(menu, MF_STRING, 1003,
localization::GetSettingsLabel(language_index_));
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
AppendMenuW(menu, MF_STRING, 1001,
localization::GetExitProgramLabel(language_index_));
@@ -119,6 +143,9 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
TrackPopupMenu(menu, TPM_RETURNCMD | TPM_NONOTIFY | TPM_LEFTALIGN,
pt.x, pt.y, 0, hwnd_message_only_, nullptr);
DestroyMenu(menu);
if (menu_background) {
DeleteObject(menu_background);
}
// handle menu command
if (cmd == 1001) {
@@ -131,6 +158,8 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
}
} else if (cmd == 1002) {
ShowApplicationWindow();
} else if (cmd == 1003) {
OpenSettings();
}
break;
}
+5 -1
View File
@@ -22,7 +22,9 @@ class WinTray {
WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
int language_index);
WinTray(std::function<void()> show_window,
std::function<void()> hide_window, std::function<void()> exit_app,
std::function<void()> hide_window,
std::function<void()> open_settings,
std::function<void()> exit_app,
HICON icon, const std::wstring& tooltip, int language_index);
~WinTray();
@@ -32,6 +34,7 @@ class WinTray {
private:
void ShowApplicationWindow();
void OpenSettings();
HWND app_hwnd_;
HWND hwnd_message_only_;
@@ -41,6 +44,7 @@ class WinTray {
NOTIFYICONDATA nid_;
std::function<void()> show_window_;
std::function<void()> hide_window_;
std::function<void()> open_settings_;
std::function<void()> exit_app_;
};
} // namespace crossdesk
+5
View File
@@ -338,6 +338,11 @@ void GuiRuntime::ResetRemoteSessionResources(
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = false;
}
{
std::lock_guard<std::mutex> lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
}
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
+124
View File
@@ -0,0 +1,124 @@
#include "runtime/cursor_state_provider.h"
#if defined(_WIN32)
#include <windows.h>
namespace crossdesk {
namespace {
bool IsSystemCursor(HCURSOR cursor, LPCWSTR resource) {
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
}
RemoteCursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
if (IsSystemCursor(cursor, IDC_HELP)) return RemoteCursorShape::help;
if (IsSystemCursor(cursor, IDC_HAND)) return RemoteCursorShape::pointer;
if (IsSystemCursor(cursor, IDC_APPSTARTING))
return RemoteCursorShape::progress;
if (IsSystemCursor(cursor, IDC_WAIT)) return RemoteCursorShape::wait;
if (IsSystemCursor(cursor, IDC_CROSS)) return RemoteCursorShape::crosshair;
if (IsSystemCursor(cursor, IDC_IBEAM)) return RemoteCursorShape::text;
if (IsSystemCursor(cursor, IDC_NO))
return RemoteCursorShape::not_allowed;
if (IsSystemCursor(cursor, IDC_SIZEALL)) return RemoteCursorShape::move;
if (IsSystemCursor(cursor, IDC_SIZEWE)) return RemoteCursorShape::ew_resize;
if (IsSystemCursor(cursor, IDC_SIZENS)) return RemoteCursorShape::ns_resize;
if (IsSystemCursor(cursor, IDC_SIZENESW))
return RemoteCursorShape::nesw_resize;
if (IsSystemCursor(cursor, IDC_SIZENWSE))
return RemoteCursorShape::nwse_resize;
if (IsSystemCursor(cursor, IDC_UPARROW)) return RemoteCursorShape::n_resize;
return RemoteCursorShape::default_cursor;
}
} // namespace
struct CursorStateProvider::Impl {};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(CursorState* state) {
if (!state) return false;
CURSORINFO info{};
info.cbSize = sizeof(info);
if (!GetCursorInfo(&info)) return false;
state->seq = 0;
state->visible = (info.flags & CURSOR_SHOWING) != 0;
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
: RemoteCursorShape::none;
return true;
}
} // namespace crossdesk
#elif defined(__linux__) && !defined(__APPLE__)
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
#include <string>
#include "linux_cursor_shape.h"
#include "platform.h"
#include "shared_cursor_state.h"
namespace crossdesk {
namespace {
bool CursorHasVisiblePixel(const XFixesCursorImage& image) {
const size_t pixel_count =
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
for (size_t index = 0; index < pixel_count; ++index) {
if (((image.pixels[index] >> 24U) & 0xffU) != 0) return true;
}
return false;
}
} // namespace
struct CursorStateProvider::Impl {
Display* display = XOpenDisplay(nullptr);
~Impl() {
if (display) XCloseDisplay(display);
}
};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(CursorState* state) {
if (!state || !impl_) return false;
if (IsWaylandSession()) {
SharedCursorState shared{};
if (GetSharedCursorState(&shared)) {
state->seq = 0;
state->visible = shared.visible;
state->shape = shared.visible ? shared.shape : RemoteCursorShape::none;
return true;
}
}
if (!impl_->display) return false;
XFixesCursorImage* image = XFixesGetCursorImage(impl_->display);
if (!image) return false;
const std::string name = image->name ? image->name : "";
state->seq = 0;
state->visible = CursorHasVisiblePixel(*image);
state->shape = state->visible ? ShapeFromLinuxCursorName(name)
: RemoteCursorShape::none;
XFree(image);
return true;
}
} // namespace crossdesk
#endif
+29
View File
@@ -0,0 +1,29 @@
#ifndef CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
#define CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
#include <memory>
#include "device_controller.h"
namespace crossdesk {
// Samples the cursor that is currently displayed by the controlled desktop
// and converts platform-specific cursor handles to protocol cursor shapes.
class CursorStateProvider {
public:
CursorStateProvider();
~CursorStateProvider();
CursorStateProvider(const CursorStateProvider&) = delete;
CursorStateProvider& operator=(const CursorStateProvider&) = delete;
bool Sample(CursorState* state);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
@@ -0,0 +1,165 @@
#include "runtime/cursor_state_provider.h"
#if defined(__APPLE__)
#import <AppKit/AppKit.h>
#import <CoreGraphics/CGRemoteOperation.h>
#include <cstdint>
#include <vector>
namespace crossdesk {
namespace {
struct CursorFingerprint {
uint64_t pixel_hash = 0;
size_t width = 0;
size_t height = 0;
size_t bytes_per_row = 0;
CFIndex pixel_bytes = 0;
NSPoint hot_spot = NSZeroPoint;
};
struct KnownCursor {
CursorFingerprint fingerprint;
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
};
bool FingerprintCursor(NSCursor* cursor, CursorFingerprint* fingerprint) {
if (!cursor || !fingerprint || !cursor.image) return false;
NSRect proposed_rect = {NSZeroPoint, cursor.image.size};
CGImageRef image =
[cursor.image CGImageForProposedRect:&proposed_rect
context:nil
hints:nil];
if (!image) return false;
CGDataProviderRef provider = CGImageGetDataProvider(image);
if (!provider) return false;
CFDataRef data = CGDataProviderCopyData(provider);
if (!data) return false;
// NSCursor.currentSystemCursor returns a fresh NSCursor/NSImage wrapper on
// each query. Hash the decoded CGImage pixels so it can still be matched to
// AppKit's semantic cursor instances without encoding a large TIFF every
// frame.
constexpr uint64_t kFnvOffsetBasis = 1469598103934665603ULL;
constexpr uint64_t kFnvPrime = 1099511628211ULL;
uint64_t hash = kFnvOffsetBasis;
const UInt8* bytes = CFDataGetBytePtr(data);
const CFIndex length = CFDataGetLength(data);
for (CFIndex index = 0; index < length; ++index) {
hash ^= bytes[index];
hash *= kFnvPrime;
}
fingerprint->pixel_hash = hash;
fingerprint->width = CGImageGetWidth(image);
fingerprint->height = CGImageGetHeight(image);
fingerprint->bytes_per_row = CGImageGetBytesPerRow(image);
fingerprint->pixel_bytes = length;
fingerprint->hot_spot = cursor.hotSpot;
CFRelease(data);
return true;
}
bool SameCursor(const CursorFingerprint& left,
const CursorFingerprint& right) {
return left.pixel_hash == right.pixel_hash && left.width == right.width &&
left.height == right.height &&
left.bytes_per_row == right.bytes_per_row &&
left.pixel_bytes == right.pixel_bytes &&
NSEqualPoints(left.hot_spot, right.hot_spot);
}
void AddKnownCursor(std::vector<KnownCursor>* cursors, NSCursor* cursor,
RemoteCursorShape shape) {
CursorFingerprint fingerprint;
if (FingerprintCursor(cursor, &fingerprint)) {
cursors->push_back({fingerprint, shape});
}
}
std::vector<KnownCursor> BuildKnownCursors() {
std::vector<KnownCursor> cursors;
cursors.reserve(16);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
AddKnownCursor(&cursors, NSCursor.arrowCursor,
RemoteCursorShape::default_cursor);
AddKnownCursor(&cursors, NSCursor.pointingHandCursor,
RemoteCursorShape::pointer);
AddKnownCursor(&cursors, NSCursor.crosshairCursor,
RemoteCursorShape::crosshair);
AddKnownCursor(&cursors, NSCursor.IBeamCursor, RemoteCursorShape::text);
AddKnownCursor(&cursors, NSCursor.IBeamCursorForVerticalLayout,
RemoteCursorShape::text);
AddKnownCursor(&cursors, NSCursor.operationNotAllowedCursor,
RemoteCursorShape::not_allowed);
AddKnownCursor(&cursors, NSCursor.dragLinkCursor, RemoteCursorShape::alias);
AddKnownCursor(&cursors, NSCursor.dragCopyCursor, RemoteCursorShape::copy);
AddKnownCursor(&cursors, NSCursor.openHandCursor, RemoteCursorShape::grab);
AddKnownCursor(&cursors, NSCursor.closedHandCursor,
RemoteCursorShape::grabbing);
AddKnownCursor(&cursors, NSCursor.resizeLeftRightCursor,
RemoteCursorShape::ew_resize);
AddKnownCursor(&cursors, NSCursor.resizeUpDownCursor,
RemoteCursorShape::ns_resize);
AddKnownCursor(&cursors, NSCursor.resizeUpCursor,
RemoteCursorShape::n_resize);
AddKnownCursor(&cursors, NSCursor.resizeRightCursor,
RemoteCursorShape::e_resize);
AddKnownCursor(&cursors, NSCursor.resizeDownCursor,
RemoteCursorShape::s_resize);
AddKnownCursor(&cursors, NSCursor.resizeLeftCursor,
RemoteCursorShape::w_resize);
#pragma clang diagnostic pop
return cursors;
}
RemoteCursorShape ShapeFromMacCursor(NSCursor* cursor) {
CursorFingerprint fingerprint;
if (!FingerprintCursor(cursor, &fingerprint)) {
return RemoteCursorShape::default_cursor;
}
// Sampling begins from the UI tick, after AppKit has initialized its cursor
// catalog. Cache those fingerprints because only the current cursor needs
// to be decoded on subsequent frames.
static const std::vector<KnownCursor> known_cursors = BuildKnownCursors();
for (const KnownCursor& known : known_cursors) {
if (SameCursor(fingerprint, known.fingerprint)) return known.shape;
}
return RemoteCursorShape::default_cursor;
}
} // namespace
struct CursorStateProvider::Impl {};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(CursorState* state) {
if (!state) return false;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSCursor* cursor = NSCursor.currentSystemCursor;
const bool visible = CGCursorIsVisible();
#pragma clang diagnostic pop
state->seq = 0;
state->visible = visible && cursor != nil;
state->shape = state->visible ? ShapeFromMacCursor(cursor)
: RemoteCursorShape::none;
return true;
}
} // namespace crossdesk
#endif
+34
View File
@@ -164,6 +164,40 @@ void PeerEventHandler::OnReceiveDataBuffer(
return;
}
if (remote_action.type == ControlType::cursor_state) {
std::shared_ptr<GuiRuntime::RemoteSession> props;
{
std::shared_lock lock(runtime->remote_sessions_mutex_);
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
props = props_it->second;
}
}
if (!props) {
return;
}
std::lock_guard lock(props->remote_cursor_state_mutex_);
const uint32_t previous_seq = props->remote_cursor_state_.seq;
const bool is_newer =
!props->remote_cursor_state_received_ ||
static_cast<int32_t>(remote_action.cs.seq - previous_seq) > 0;
if (is_newer) {
const bool changed =
!props->remote_cursor_state_received_ ||
props->remote_cursor_state_.visible != remote_action.cs.visible ||
props->remote_cursor_state_.shape != remote_action.cs.shape;
props->remote_cursor_state_ = remote_action.cs;
props->remote_cursor_state_received_ = true;
if (changed) {
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}",
remote_action.cs.seq, remote_action.cs.visible,
static_cast<int>(remote_action.cs.shape));
}
}
return;
}
if (remote_action.type == ControlType::host_infomation) {
bool is_client_mode = false;
std::shared_ptr<GuiRuntime::RemoteSession> props;
+10
View File
@@ -249,6 +249,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
switch (status) {
case ConnectionStatus::Connected: {
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
{
RemoteAction remote_action;
remote_action.i.display_num =
@@ -313,6 +318,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
props->connection_established_ = false;
props->enable_mouse_control_ = false;
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
int native_snapshot_width = 0;
+7
View File
@@ -13,6 +13,7 @@
#include <string>
#include <vector>
#include "device_controller.h"
#include "display_info.h"
#include "minirtc.h"
@@ -148,6 +149,12 @@ struct RemoteSession {
bool remote_service_available_ = false;
std::string remote_interactive_stage_;
std::vector<DisplayInfo> display_info_list_;
// Cursor snapshots arrive on the transport callback thread and are applied
// by the Slint UI thread. Keep the snapshot atomic as a unit so visibility
// and shape cannot briefly come from different protocol messages.
std::mutex remote_cursor_state_mutex_;
CursorState remote_cursor_state_{};
bool remote_cursor_state_received_ = false;
// Shared by minirtc callbacks, Slint rendering and SDL audio callbacks.
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
-2
View File
@@ -140,8 +140,6 @@ struct UserSettingsState {
bool enable_autostart_last_ = false;
bool enable_daemon_ = false;
bool enable_daemon_last_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_minimize_to_tray_last_ = false;
char file_transfer_save_path_buf_[512] = "";
std::string file_transfer_save_path_last_;
char signal_server_ip_self_[256] = "";
-7
View File
@@ -46,7 +46,6 @@ export global UiStrings {
in-out property <string> self-hosted: "Self-hosted server";
in-out property <string> autostart: "Start with system";
in-out property <string> daemon: "Run in background";
in-out property <string> minimize-to-tray: "Minimize to tray";
in-out property <string> file-save-path: "File save path";
in-out property <string> default-desktop: "Desktop";
in-out property <string> server-host: "Server host";
@@ -242,7 +241,6 @@ export component MainWindow inherits Window {
in-out property <bool> self-hosted-enabled: false;
in-out property <bool> autostart-enabled: false;
in-out property <bool> daemon-enabled: false;
in-out property <bool> minimize-to-tray-enabled: false;
in-out property <string> file-save-path: "";
in-out property <string> server-host: "";
in-out property <string> server-port: "";
@@ -1488,11 +1486,6 @@ export component MainWindow inherits Window {
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; }
+62 -2
View File
@@ -133,6 +133,9 @@ export component StreamWindow inherits Window {
in property <[string]> displays;
in-out property <int> selected-display: 0;
in property <bool> mouse-control-enabled: true;
// Numeric values mirror crossdesk::RemoteCursorShape and Slint's MouseCursor.
in property <bool> remote-cursor-active: false;
in property <int> remote-cursor-shape: 0;
in property <bool> audio-enabled: true;
in property <bool> srtp-enabled: false;
in property <bool> fullscreen-enabled: false;
@@ -172,6 +175,10 @@ export component StreamWindow inherits Window {
private property <bool> display-menu-animation-enabled: false;
private property <float> display-menu-progress: 0.0;
private property <bool> shortcut-menu-open: false;
// A popup can remove itself from the scene in its clicked callback before
// the video input layer observes the same pointer release. Remember that
// the press belonged to local UI so that release cannot reach the peer.
private property <bool> suppress-next-remote-pointer-release: false;
private property <bool> control-docked-left: true;
private property <bool> control-dragging: false;
private property <length> control-drag-x: 0px;
@@ -547,6 +554,36 @@ export component StreamWindow inherits Window {
accept
}
pointer := TouchArea {
mouse-cursor: !root.remote-cursor-active ? MouseCursor.default
: root.remote-cursor-shape == 1 ? MouseCursor.none
: root.remote-cursor-shape == 2 ? MouseCursor.help
: root.remote-cursor-shape == 3 ? MouseCursor.pointer
: root.remote-cursor-shape == 4 ? MouseCursor.progress
: root.remote-cursor-shape == 5 ? MouseCursor.wait
: root.remote-cursor-shape == 6 ? MouseCursor.crosshair
: root.remote-cursor-shape == 7 ? MouseCursor.text
: root.remote-cursor-shape == 8 ? MouseCursor.alias
: root.remote-cursor-shape == 9 ? MouseCursor.copy
: root.remote-cursor-shape == 10 ? MouseCursor.move
: root.remote-cursor-shape == 11 ? MouseCursor.no-drop
: root.remote-cursor-shape == 12 ? MouseCursor.not-allowed
: root.remote-cursor-shape == 13 ? MouseCursor.grab
: root.remote-cursor-shape == 14 ? MouseCursor.grabbing
: root.remote-cursor-shape == 15 ? MouseCursor.col-resize
: root.remote-cursor-shape == 16 ? MouseCursor.row-resize
: root.remote-cursor-shape == 17 ? MouseCursor.n-resize
: root.remote-cursor-shape == 18 ? MouseCursor.e-resize
: root.remote-cursor-shape == 19 ? MouseCursor.s-resize
: root.remote-cursor-shape == 20 ? MouseCursor.w-resize
: root.remote-cursor-shape == 21 ? MouseCursor.ne-resize
: root.remote-cursor-shape == 22 ? MouseCursor.nw-resize
: root.remote-cursor-shape == 23 ? MouseCursor.se-resize
: root.remote-cursor-shape == 24 ? MouseCursor.sw-resize
: root.remote-cursor-shape == 25 ? MouseCursor.ew-resize
: root.remote-cursor-shape == 26 ? MouseCursor.ns-resize
: root.remote-cursor-shape == 27 ? MouseCursor.nesw-resize
: root.remote-cursor-shape == 28 ? MouseCursor.nwse-resize
: MouseCursor.default;
pointer-event(event) => {
let over-control = self.mouse-x >= control.x
&& self.mouse-x <= control.x + control.width
@@ -554,7 +591,14 @@ export component StreamWindow inherits Window {
&& self.mouse-y <= control.y + control.height;
let over-local-ui = root.is-local-control-area(
self.mouse-x, self.mouse-y);
let suppress-remote-release =
root.suppress-next-remote-pointer-release
&& (event.kind == PointerEventKind.up
|| event.kind == PointerEventKind.cancel);
if event.kind == PointerEventKind.down && !over-local-ui {
// Discard a stale marker if a popup consumed the
// whole previous gesture, including its release.
root.suppress-next-remote-pointer-release = false;
input-focus.focus();
}
if event.kind == PointerEventKind.down
@@ -568,9 +612,13 @@ export component StreamWindow inherits Window {
// The control bar overlays the remote video. Events in
// it and its attached menus (including padding and gaps)
// are local UI input and must never reach the peer.
} else if !root.control-dragging && !over-local-ui {
} else if !root.control-dragging && !over-local-ui
&& !suppress-remote-release {
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
}
if suppress-remote-release {
root.suppress-next-remote-pointer-release = false;
}
}
moved => {
if root.control-dragging {
@@ -871,6 +919,11 @@ export component StreamWindow inherits Window {
// hover stable while still consuming padding, gaps and scroll
// events locally instead of forwarding them to the peer.
display-touch := TouchArea {
pointer-event(event) => {
if event.kind == PointerEventKind.down {
root.suppress-next-remote-pointer-release = true;
}
}
clicked => {
let index = floor((self.mouse-y - 4px) / 30px);
let row-y = self.mouse-y - 4px - index * 30px;
@@ -896,7 +949,14 @@ export component StreamWindow inherits Window {
for shortcut in ["Ctrl+Alt+Del", "Win+L"]: Rectangle {
height: 29px; background: shortcut-touch.has-hover ? #e8eef9 : transparent;
Text { x: 8px; text: shortcut; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
shortcut-touch := TouchArea { clicked => { root.send-shortcut(shortcut); root.shortcut-menu-open = false; } }
shortcut-touch := TouchArea {
pointer-event(event) => {
if event.kind == PointerEventKind.down {
root.suppress-next-remote-pointer-release = true;
}
}
clicked => { root.send-shortcut(shortcut); root.shortcut-menu-open = false; }
}
}
}
}
-2
View File
@@ -57,8 +57,6 @@ inline int ApplyMainWindowStrings(
Text(localization::self_hosted_server_config[language]));
strings.set_autostart(Text(localization::enable_autostart[language]));
strings.set_daemon(Text(localization::enable_daemon[language]));
strings.set_minimize_to_tray(
Text(localization::minimize_to_tray[language]));
strings.set_file_save_path(
Text(localization::file_transfer_save_path[language]));
strings.set_default_desktop(Text(localization::default_desktop[language]));
+5 -2
View File
@@ -299,8 +299,11 @@ int GuiApplication::TitleBar(bool main_window) {
}
if (close_button_clicked) {
const bool minimized_to_tray = main_window && MinimizeMainWindowToTray();
if (!minimized_to_tray) {
if (main_window) {
if (!MinimizeMainWindowToTray()) {
SDL_HideWindow(main_window_);
}
} else {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
@@ -355,26 +355,6 @@ int GuiApplication::SettingWindow() {
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s",
localization::minimize_to_tray[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
}
ImGui::Checkbox("##enable_minimize_to_tray_",
&enable_minimize_to_tray_);
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
@@ -618,13 +598,6 @@ int GuiApplication::SettingWindow() {
}
enable_daemon_last_ = enable_daemon_;
if (enable_minimize_to_tray_) {
config_center_->SetMinimizeToTray(true);
} else {
config_center_->SetMinimizeToTray(false);
}
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
// File transfer save path
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
file_transfer_save_path_last_ = file_transfer_save_path_buf_;
@@ -12,8 +12,10 @@
#include <cstring>
#include <thread>
#include "linux_cursor_shape.h"
#include "platform.h"
#include "rd_log.h"
#include "shared_cursor_state.h"
#include "wayland_portal_shared.h"
namespace crossdesk {
@@ -35,7 +37,8 @@ constexpr auto kPipeWireCloseSettleDelay = std::chrono::milliseconds(200);
} // namespace
ScreenCapturerWayland::ScreenCapturerWayland() {}
ScreenCapturerWayland::ScreenCapturerWayland()
: cursor_theme_matcher_(std::make_unique<LinuxCursorThemeMatcher>()) {}
ScreenCapturerWayland::~ScreenCapturerWayland() { Destroy(); }
@@ -66,6 +69,10 @@ int ScreenCapturerWayland::Init(const int fps, cb_desktop_data cb) {
callback_ = cb;
pointer_granted_ = false;
shared_session_registered_ = false;
cursor_metadata_enabled_ = false;
cursor_metadata_seen_ = false;
cursor_metadata_missing_buffers_ = 0;
ClearSharedCursorState();
display_info_list_.clear();
display_info_list_.push_back(
DisplayInfo(display_name_, 0, 0, kFallbackWidth, kFallbackHeight));
@@ -100,6 +107,10 @@ int ScreenCapturerWayland::Start(bool show_cursor) {
}
show_cursor_ = show_cursor;
cursor_metadata_enabled_ = false;
cursor_metadata_seen_ = false;
cursor_metadata_missing_buffers_ = 0;
ClearSharedCursorState();
paused_ = false;
pipewire_node_id_ = 0;
UpdateDisplayGeometry(
@@ -119,6 +130,10 @@ int ScreenCapturerWayland::Stop() {
thread_.join();
}
pipewire_node_id_ = 0;
cursor_metadata_enabled_ = false;
cursor_metadata_seen_ = false;
cursor_metadata_missing_buffers_ = 0;
ClearSharedCursorState();
UpdateDisplayGeometry(
logical_width_ > 0 ? logical_width_ : kFallbackWidth,
logical_height_ > 0 ? logical_height_ : kFallbackHeight);
@@ -15,6 +15,7 @@ struct pw_thread_loop;
#include <atomic>
#include <cstdint>
#include <memory>
#include <string>
#include <thread>
#include <vector>
@@ -23,6 +24,8 @@ struct pw_thread_loop;
namespace crossdesk {
class LinuxCursorThemeMatcher;
class ScreenCapturerWayland : public ScreenCapturer {
public:
enum class PipeWireConnectMode { kTargetObject, kNodeId, kAny };
@@ -51,6 +54,7 @@ class ScreenCapturerWayland : public ScreenCapturer {
bool CreatePortalSession();
bool SelectPortalDevices();
bool SelectPortalSource();
bool GetAvailableCursorModes(uint32_t* modes) const;
bool StartPortalSession();
bool EnsurePipeWireRuntimeAvailable() const;
bool OpenPipeWireRemote();
@@ -95,6 +99,9 @@ class ScreenCapturerWayland : public ScreenCapturer {
bool pipewire_thread_loop_started_ = false;
bool pointer_granted_ = false;
bool shared_session_registered_ = false;
bool cursor_metadata_enabled_ = false;
bool cursor_metadata_seen_ = false;
uint32_t cursor_metadata_missing_buffers_ = 0;
bool portal_has_logical_size_ = false;
uint32_t spa_video_format_ = 0;
int frame_width_ = 0;
@@ -104,6 +111,7 @@ class ScreenCapturerWayland : public ScreenCapturer {
int portal_stream_height_ = 0;
int logical_width_ = 0;
int logical_height_ = 0;
std::unique_ptr<LinuxCursorThemeMatcher> cursor_theme_matcher_;
std::vector<uint8_t> y_plane_;
std::vector<uint8_t> uv_plane_;
@@ -17,7 +17,9 @@
#include "display_stream_id.h"
#include "libyuv.h"
#include "linux_cursor_shape.h"
#include "rd_log.h"
#include "shared_cursor_state.h"
namespace crossdesk {
@@ -199,6 +201,145 @@ int64_t NowMs() {
.count();
}
constexpr uint32_t kMaxCursorDimension = 512;
constexpr int kCursorMetadataSize =
static_cast<int>(sizeof(spa_meta_cursor) + sizeof(spa_meta_bitmap) +
64u + kMaxCursorDimension * kMaxCursorDimension * 4u);
enum class CursorBitmapResult { kNoUpdate, kInvisible, kImage, kInvalid };
CursorBitmapResult ReadCursorBitmap(const spa_meta& metadata,
const spa_meta_cursor& cursor,
std::vector<uint32_t>* argb,
LinuxCursorImageView* view) {
if (!metadata.data || !argb || !view || cursor.bitmap_offset == 0) {
return CursorBitmapResult::kNoUpdate;
}
const size_t bitmap_offset = cursor.bitmap_offset;
if (bitmap_offset > metadata.size ||
metadata.size - bitmap_offset < sizeof(spa_meta_bitmap)) {
return CursorBitmapResult::kInvalid;
}
const auto* metadata_bytes = static_cast<const uint8_t*>(metadata.data);
const auto* bitmap = reinterpret_cast<const spa_meta_bitmap*>(
metadata_bytes + bitmap_offset);
if (bitmap->format == 0) {
return CursorBitmapResult::kNoUpdate;
}
if (bitmap->offset == 0) {
return CursorBitmapResult::kInvisible;
}
const uint32_t width = bitmap->size.width;
const uint32_t height = bitmap->size.height;
if (width == 0 || height == 0 || width > kMaxCursorDimension ||
height > kMaxCursorDimension || bitmap->stride < 0 ||
static_cast<uint32_t>(bitmap->stride) < width * 4u) {
return CursorBitmapResult::kInvalid;
}
const size_t pixel_offset = bitmap_offset + bitmap->offset;
const size_t required_size =
static_cast<size_t>(bitmap->stride) * (height - 1u) + width * 4u;
if (pixel_offset > metadata.size ||
required_size > metadata.size - pixel_offset) {
return CursorBitmapResult::kInvalid;
}
enum class ChannelOrder { kBgra, kRgba, kArgb, kAbgr, kBgrx, kRgbx };
ChannelOrder channel_order;
switch (bitmap->format) {
case SPA_VIDEO_FORMAT_BGRA:
channel_order = ChannelOrder::kBgra;
break;
case SPA_VIDEO_FORMAT_RGBA:
channel_order = ChannelOrder::kRgba;
break;
case SPA_VIDEO_FORMAT_ARGB:
channel_order = ChannelOrder::kArgb;
break;
case SPA_VIDEO_FORMAT_ABGR:
channel_order = ChannelOrder::kAbgr;
break;
case SPA_VIDEO_FORMAT_BGRx:
channel_order = ChannelOrder::kBgrx;
break;
case SPA_VIDEO_FORMAT_RGBx:
channel_order = ChannelOrder::kRgbx;
break;
default:
return CursorBitmapResult::kInvalid;
}
argb->resize(static_cast<size_t>(width) * height);
const uint8_t* pixels = metadata_bytes + pixel_offset;
bool has_visible_pixel = false;
for (uint32_t y = 0; y < height; ++y) {
const uint8_t* row = pixels + static_cast<size_t>(y) * bitmap->stride;
for (uint32_t x = 0; x < width; ++x) {
const uint8_t* pixel = row + static_cast<size_t>(x) * 4u;
uint8_t red = 0;
uint8_t green = 0;
uint8_t blue = 0;
uint8_t alpha = 0xff;
switch (channel_order) {
case ChannelOrder::kBgra:
blue = pixel[0];
green = pixel[1];
red = pixel[2];
alpha = pixel[3];
break;
case ChannelOrder::kRgba:
red = pixel[0];
green = pixel[1];
blue = pixel[2];
alpha = pixel[3];
break;
case ChannelOrder::kArgb:
alpha = pixel[0];
red = pixel[1];
green = pixel[2];
blue = pixel[3];
break;
case ChannelOrder::kAbgr:
alpha = pixel[0];
blue = pixel[1];
green = pixel[2];
red = pixel[3];
break;
case ChannelOrder::kBgrx:
blue = pixel[0];
green = pixel[1];
red = pixel[2];
break;
case ChannelOrder::kRgbx:
red = pixel[0];
green = pixel[1];
blue = pixel[2];
break;
}
has_visible_pixel = has_visible_pixel || alpha != 0;
(*argb)[static_cast<size_t>(y) * width + x] =
(static_cast<uint32_t>(alpha) << 24U) |
(static_cast<uint32_t>(red) << 16U) |
(static_cast<uint32_t>(green) << 8U) | blue;
}
}
if (!has_visible_pixel) {
return CursorBitmapResult::kInvisible;
}
view->width = width;
view->height = height;
view->xhot = static_cast<uint32_t>(std::max(0, cursor.hotspot.x));
view->yhot = static_cast<uint32_t>(std::max(0, cursor.hotspot.y));
view->argb = argb->data();
return CursorBitmapResult::kImage;
}
double SnapLikelyFractionalScale(double observed_scale) {
static constexpr double kCandidates[] = {
1.0, 1.25, 1.3333333333, 1.5, 1.6666666667, 1.75, 2.0, 2.25, 2.5, 3.0};
@@ -500,7 +641,7 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
uint8_t buffer[1024];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const spa_pod* params[2];
const spa_pod* params[3];
uint32_t param_count = 0;
params[param_count++] =
@@ -525,10 +666,29 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
CROSSDESK_SPA_PARAM_META_SIZE,
SPA_POD_Int(sizeof(struct spa_meta_header))));
if (self->cursor_metadata_enabled_) {
params[param_count++] =
reinterpret_cast<const spa_pod*>(spa_pod_builder_add_object(
&builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
CROSSDESK_SPA_PARAM_META_TYPE, SPA_POD_Id(SPA_META_Cursor),
CROSSDESK_SPA_PARAM_META_SIZE,
SPA_POD_CHOICE_RANGE_Int(
kCursorMetadataSize,
static_cast<int>(sizeof(struct spa_meta_cursor)),
kCursorMetadataSize)));
}
if (self->pw_stream_) {
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
if (pipewire) {
pipewire->stream_update_params(self->pw_stream_, params, param_count);
const int update_result = pipewire->stream_update_params(
self->pw_stream_, params, param_count);
LOG_INFO(
"PipeWire buffer parameters updated: cursor_metadata={}, "
"cursor_meta_max_size={}, result={}",
self->cursor_metadata_enabled_,
self->cursor_metadata_enabled_ ? kCursorMetadataSize : 0,
update_result);
}
}
self->pipewire_format_ready_.store(true);
@@ -786,6 +946,57 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() {
return;
}
if (cursor_metadata_enabled_) {
spa_meta* cursor_metadata =
spa_buffer_find_meta(spa_buffer, SPA_META_Cursor);
if (!cursor_metadata) {
++cursor_metadata_missing_buffers_;
if (cursor_metadata_missing_buffers_ == 120) {
LOG_WARN(
"PipeWire stream has not attached SPA_META_Cursor after 120 "
"buffers; cursor metadata negotiation was not accepted");
}
} else {
cursor_metadata_missing_buffers_ = 0;
if (!cursor_metadata_seen_) {
cursor_metadata_seen_ = true;
LOG_INFO("PipeWire cursor metadata attached, size={}",
cursor_metadata->size);
}
}
if (cursor_metadata && cursor_metadata->data &&
cursor_metadata->size >= sizeof(spa_meta_cursor)) {
const auto* cursor =
static_cast<const spa_meta_cursor*>(cursor_metadata->data);
if (spa_meta_cursor_is_valid(cursor)) {
std::vector<uint32_t> cursor_argb;
LinuxCursorImageView cursor_view;
const CursorBitmapResult cursor_result = ReadCursorBitmap(
*cursor_metadata, *cursor, &cursor_argb, &cursor_view);
if (cursor_result == CursorBitmapResult::kInvisible) {
PublishSharedCursorState(false, RemoteCursorShape::none);
LOG_INFO("PipeWire cursor became invisible, id={}", cursor->id);
} else if (cursor_result == CursorBitmapResult::kImage) {
const RemoteCursorShape shape =
cursor_theme_matcher_
? cursor_theme_matcher_->Match(cursor_view)
: RemoteCursorShape::default_cursor;
PublishSharedCursorState(true, shape);
LOG_INFO(
"PipeWire cursor shape update: id={}, size={}x{}, "
"hotspot=({},{}), shape={}",
cursor->id, cursor_view.width, cursor_view.height,
cursor_view.xhot, cursor_view.yhot, static_cast<int>(shape));
} else if (cursor_result == CursorBitmapResult::kInvalid) {
PublishSharedCursorState(true, RemoteCursorShape::default_cursor);
LOG_WARN("Using default shape for malformed PipeWire cursor "
"metadata, id={}",
cursor->id);
}
}
}
}
const spa_data& data = spa_buffer->datas[0];
if (!data.chunk) {
requeue();
@@ -12,6 +12,7 @@
#include <string>
#include "rd_log.h"
#include "shared_cursor_state.h"
namespace crossdesk {
@@ -35,6 +36,7 @@ constexpr const char* kPortalSessionPathPrefix =
constexpr uint32_t kScreenCastSourceMonitor = 1u;
constexpr uint32_t kCursorModeHidden = 1u;
constexpr uint32_t kCursorModeEmbedded = 2u;
constexpr uint32_t kCursorModeMetadata = 4u;
constexpr uint32_t kRemoteDesktopDevicePointer = 2u;
std::string MakeToken(const char* prefix) {
@@ -482,6 +484,47 @@ bool ScreenCapturerWayland::SelectPortalSource() {
return false;
}
uint32_t available_cursor_modes = kCursorModeHidden | kCursorModeEmbedded;
if (!GetAvailableCursorModes(&available_cursor_modes)) {
LOG_WARN(
"Unable to query portal cursor modes; using embedded cursor fallback");
}
uint32_t cursor_mode = 0;
cursor_metadata_enabled_ = false;
if (!show_cursor_ && (available_cursor_modes & kCursorModeMetadata) != 0) {
cursor_mode = kCursorModeMetadata;
cursor_metadata_enabled_ = true;
} else if ((available_cursor_modes & kCursorModeEmbedded) != 0) {
cursor_mode = kCursorModeEmbedded;
} else if ((available_cursor_modes & kCursorModeMetadata) != 0) {
cursor_mode = kCursorModeMetadata;
cursor_metadata_enabled_ = true;
} else if ((available_cursor_modes & kCursorModeHidden) != 0) {
cursor_mode = kCursorModeHidden;
}
if (cursor_mode == 0) {
LOG_ERROR("Wayland portal reports no supported cursor mode");
return false;
}
if (cursor_mode == kCursorModeEmbedded) {
// When metadata is unavailable, embedding is the only way to preserve the
// exact Wayland cursor. Hide the controller-side cursor to avoid a double
// cursor over the video stream.
PublishSharedCursorState(false, RemoteCursorShape::none);
} else if (!cursor_metadata_enabled_) {
// A portal without metadata or embedded cursor support cannot expose the
// native Wayland shape. Keep a visible default cursor on the controller.
PublishSharedCursorState(true, RemoteCursorShape::default_cursor);
}
LOG_INFO("Wayland portal cursor mode: {}",
cursor_mode == kCursorModeMetadata
? "metadata"
: (cursor_mode == kCursorModeEmbedded ? "embedded" : "hidden"));
const char* session_handle = session_handle_.c_str();
return SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalScreenCastInterface, "SelectSources",
@@ -496,9 +539,7 @@ bool ScreenCapturerWayland::SelectPortalSource() {
&options);
AppendDictEntryUint32(&options, "types", kScreenCastSourceMonitor);
AppendDictEntryBool(&options, "multiple", false);
AppendDictEntryUint32(
&options, "cursor_mode",
show_cursor_ ? kCursorModeEmbedded : kCursorModeHidden);
AppendDictEntryUint32(&options, "cursor_mode", cursor_mode);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_req"));
dbus_message_iter_close_container(&iter, &options);
@@ -515,6 +556,50 @@ bool ScreenCapturerWayland::SelectPortalSource() {
});
}
bool ScreenCapturerWayland::GetAvailableCursorModes(uint32_t* modes) const {
if (!dbus_connection_ || !modes) {
return false;
}
DBusMessage* message = dbus_message_new_method_call(
kPortalBusName, kPortalObjectPath, "org.freedesktop.DBus.Properties",
"Get");
if (!message) {
return false;
}
const char* interface_name = kPortalScreenCastInterface;
const char* property_name = "AvailableCursorModes";
dbus_message_append_args(message, DBUS_TYPE_STRING, &interface_name,
DBUS_TYPE_STRING, &property_name,
DBUS_TYPE_INVALID);
DBusError error;
dbus_error_init(&error);
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
dbus_connection_, message, -1, &error);
dbus_message_unref(message);
if (!reply) {
LogDbusError("Get AvailableCursorModes", &error);
dbus_error_free(&error);
return false;
}
DBusMessageIter iter;
bool parsed = false;
if (dbus_message_iter_init(reply, &iter) &&
dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_VARIANT) {
DBusMessageIter variant;
dbus_message_iter_recurse(&iter, &variant);
if (dbus_message_iter_get_arg_type(&variant) == DBUS_TYPE_UINT32) {
dbus_message_iter_get_basic(&variant, modes);
parsed = true;
}
}
dbus_message_unref(reply);
return parsed;
}
bool ScreenCapturerWayland::SelectPortalDevices() {
if (!dbus_connection_ || session_handle_.empty()) {
return false;
-139
View File
@@ -1,139 +0,0 @@
#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 / "src/gui/ui/stream_window.slint")) {
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();
}
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 unexpected text: " << unexpected << "\n";
return false;
}
bool ExpectContainsAtLeast(const char* name, const std::string& value,
const std::string& expected, size_t min_count) {
size_t count = 0;
size_t pos = 0;
while ((pos = value.find(expected, pos)) != std::string::npos) {
++count;
pos += expected.size();
}
if (count >= min_count) {
return true;
}
std::cerr << name << " expected at least " << min_count
<< " occurrences of: " << expected << ", found " << count << "\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 stream =
ReadFile(repo_root / "src/gui/ui/stream_window.slint");
const std::string common = ReadFile(repo_root / "src/gui/ui/common.slint");
const std::string application =
ReadFile(repo_root / "src/gui/application/gui_application.cpp");
bool ok = true;
ok &= ExpectContains("stream_window.slint", stream,
"private property <bool> display-menu-open: false;");
ok &= ExpectContains("stream_window.slint", stream,
"root.shortcut-menu-open = false;");
ok &= ExpectContains("stream_window.slint", stream,
"if root.display-menu-open: Rectangle");
ok &=
ExpectContains("stream_window.slint", stream, "display-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream,
"root.switch-display(index)");
ok &= ExpectContains("stream_window.slint", stream,
"if root.shortcut-menu-open: Rectangle");
ok &=
ExpectContains("stream_window.slint", stream, "shortcut-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream,
"} else if !root.control-dragging && !over-control {");
ok &= ExpectContains("common.slint", common,
"if touch.has-hover && root.tooltip != \"\": Rectangle");
ok &= ExpectContainsAtLeast("stream_window.slint", stream, "tooltip:", 9);
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.select-display");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.send-shortcut");
ok &=
ExpectContains("stream_window.slint", stream,
"root.mouse-control-enabled ? StreamStrings.release-mouse "
": StreamStrings.control-mouse");
ok &= ExpectContains(
"stream_window.slint", stream,
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.select-file");
ok &= ExpectContains("stream_window.slint", stream,
"root.stats-visible ? StreamStrings.hide-stats : "
"StreamStrings.show-stats");
ok &=
ExpectContains("stream_window.slint", stream,
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen "
": StreamStrings.fullscreen");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.disconnect");
ok &= ExpectContains("stream_window.slint", stream,
"root.control-expanded ? StreamStrings.collapse-control "
": StreamStrings.expand-control");
ok &=
ExpectContains("gui_application.cpp", application, "set_select_display(");
ok &= ExpectContains("gui_application.cpp", application, "set_show_stats(");
ok &=
ExpectContains("gui_application.cpp", application, "set_expand_control(");
ok &= ExpectContains("gui_application.cpp", application,
"set_collapse_control(");
ok &= ExpectContains("gui_application.cpp", application,
"(*ui_->stream)->global<ui::StreamStrings>()");
ok &=
ExpectNotContains("gui_application.cpp", application, "#include <imgui");
return ok ? 0 : 1;
}
+3 -2
View File
@@ -37,7 +37,7 @@ function setup_platform_settings()
add_requires("libyuv")
add_syslinks("pthread", "dl")
add_links("asound", "X11", "Xext", "Xrender", "Xft", "Xtst",
"Xrandr", "Xfixes")
"Xrandr", "Xfixes", "Xcursor")
add_existing_include_dirs({
"/usr/include/freetype2",
"/usr/local/include/freetype2"
@@ -84,7 +84,8 @@ function setup_platform_settings()
add_ldflags("-Wl,-ld_classic")
end
add_cxflags("-Wno-unused-variable")
add_frameworks("Cocoa", "Metal", "QuartzCore", "IOSurface",
add_frameworks("Cocoa", "CoreGraphics", "Metal", "QuartzCore",
"IOSurface",
"ScreenCaptureKit", "AVFoundation", "CoreMedia", "CoreVideo",
"CoreAudio", "AudioToolbox")
end
-5
View File
@@ -100,11 +100,6 @@ function setup_targets()
add_includedirs("src/service/windows")
add_files("tests/windows_sas_guard_test.cpp")
target("display_popup_hover_state_test")
set_kind("binary")
set_default(false)
add_files("tests/display_popup_hover_state_test.cpp")
target("slint_ui_smoke_test")
set_kind("binary")
set_languages("c++20")