mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-28 16:54:11 +08:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
126f455681 | ||
|
|
d6d0a548b2 | ||
|
|
74ac849efe | ||
|
|
e2cd18dba4 | ||
|
|
d2b1329eca | ||
|
|
0653a95250 | ||
|
|
bd73ef55f1 |
@@ -158,7 +158,7 @@ Architecture: $DEBIAN_ARCH
|
|||||||
Maintainer: $MAINTAINER
|
Maintainer: $MAINTAINER
|
||||||
Description: $DESCRIPTION
|
Description: $DESCRIPTION
|
||||||
Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
|
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-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
|
||||||
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
|
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
|
||||||
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1
|
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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_
|
||||||
@@ -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_
|
||||||
@@ -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
|
||||||
@@ -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_
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "display_info.h"
|
#include "display_info.h"
|
||||||
|
#include "remote_cursor_shape.h"
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
@@ -28,6 +29,7 @@ typedef enum {
|
|||||||
service_status = 5,
|
service_status = 5,
|
||||||
service_command = 6,
|
service_command = 6,
|
||||||
keyboard_state = 7,
|
keyboard_state = 7,
|
||||||
|
cursor_state = 8,
|
||||||
} ControlType;
|
} ControlType;
|
||||||
typedef enum {
|
typedef enum {
|
||||||
move = 0,
|
move = 0,
|
||||||
@@ -70,6 +72,12 @@ typedef struct {
|
|||||||
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
|
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
|
||||||
} KeyboardState;
|
} KeyboardState;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t seq;
|
||||||
|
bool visible;
|
||||||
|
RemoteCursorShape shape;
|
||||||
|
} CursorState;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char host_name[64];
|
char host_name[64];
|
||||||
size_t host_name_size;
|
size_t host_name_size;
|
||||||
@@ -96,6 +104,7 @@ struct RemoteAction {
|
|||||||
Mouse m;
|
Mouse m;
|
||||||
Key k;
|
Key k;
|
||||||
KeyboardState ks;
|
KeyboardState ks;
|
||||||
|
CursorState cs;
|
||||||
HostInfo i;
|
HostInfo i;
|
||||||
bool a;
|
bool a;
|
||||||
int d;
|
int d;
|
||||||
@@ -141,6 +150,11 @@ struct RemoteAction {
|
|||||||
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
|
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
|
||||||
break;
|
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:
|
case ControlType::audio_capture:
|
||||||
j["audio_capture"] = a.a;
|
j["audio_capture"] = a.a;
|
||||||
break;
|
break;
|
||||||
@@ -219,6 +233,18 @@ struct RemoteAction {
|
|||||||
out.ks.pressed_count = count;
|
out.ks.pressed_count = count;
|
||||||
break;
|
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:
|
case ControlType::audio_capture:
|
||||||
out.a = j.at("audio_capture").get<bool>();
|
out.a = j.at("audio_capture").get<bool>();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1796,6 +1796,7 @@ void GuiApplication::Tick() {
|
|||||||
SyncStreamKeyboardFocus();
|
SyncStreamKeyboardFocus();
|
||||||
#endif
|
#endif
|
||||||
devices_.UpdateInteractions();
|
devices_.UpdateInteractions();
|
||||||
|
ShareLocalCursorState();
|
||||||
|
|
||||||
UpdateLocalization();
|
UpdateLocalization();
|
||||||
SyncMainWindow();
|
SyncMainWindow();
|
||||||
@@ -1805,6 +1806,63 @@ void GuiApplication::Tick() {
|
|||||||
SyncServerWindow();
|
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() {
|
void GuiApplication::HandlePasswordChangeResult() {
|
||||||
bool succeeded = false;
|
bool succeeded = false;
|
||||||
bool uncertain = false;
|
bool uncertain = false;
|
||||||
@@ -2364,6 +2422,23 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
? localization::receiving_screen[localization_language_index_]
|
? localization::receiving_screen[localization_language_index_]
|
||||||
: std::string{}));
|
: std::string{}));
|
||||||
(*ui_->stream)->set_mouse_control_enabled(props->control_mouse_);
|
(*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_);
|
(*ui_->stream)->set_audio_enabled(props->audio_capture_button_pressed_);
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
fullscreen_button_pressed_ = IsStreamWindowFullscreen();
|
fullscreen_button_pressed_ = IsStreamWindowFullscreen();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "runtime/cursor_state_provider.h"
|
||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
@@ -32,6 +33,7 @@ private:
|
|||||||
void BindStreamCallbacks();
|
void BindStreamCallbacks();
|
||||||
void BindServerCallbacks();
|
void BindServerCallbacks();
|
||||||
void Tick();
|
void Tick();
|
||||||
|
void ShareLocalCursorState();
|
||||||
void HandlePasswordChangeResult();
|
void HandlePasswordChangeResult();
|
||||||
void HandleCredentialRecovery();
|
void HandleCredentialRecovery();
|
||||||
void SyncMainWindow();
|
void SyncMainWindow();
|
||||||
@@ -70,6 +72,11 @@ private:
|
|||||||
bool OpenUrl(const std::string &url);
|
bool OpenUrl(const std::string &url);
|
||||||
|
|
||||||
std::unique_ptr<SlintUi> ui_;
|
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_{};
|
std::chrono::steady_clock::time_point next_video_frame_time_{};
|
||||||
#if defined(__linux__) && !defined(__APPLE__)
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
bool use_xwayland_gui_ = false;
|
bool use_xwayland_gui_ = false;
|
||||||
|
|||||||
@@ -338,6 +338,11 @@ void GuiRuntime::ResetRemoteSessionResources(
|
|||||||
props->render_rect_dirty_ = true;
|
props->render_rect_dirty_ = true;
|
||||||
props->stream_cleanup_pending_ = false;
|
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(
|
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -164,6 +164,40 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
return;
|
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) {
|
if (remote_action.type == ControlType::host_infomation) {
|
||||||
bool is_client_mode = false;
|
bool is_client_mode = false;
|
||||||
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
||||||
|
|||||||
@@ -249,6 +249,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus::Connected: {
|
case ConnectionStatus::Connected: {
|
||||||
runtime->ResetRemoteServiceStatus(*props);
|
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;
|
RemoteAction remote_action;
|
||||||
remote_action.i.display_num =
|
remote_action.i.display_num =
|
||||||
@@ -313,6 +318,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
props->connection_established_ = false;
|
props->connection_established_ = false;
|
||||||
props->enable_mouse_control_ = false;
|
props->enable_mouse_control_ = false;
|
||||||
runtime->ResetRemoteServiceStatus(*props);
|
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;
|
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
|
||||||
int native_snapshot_width = 0;
|
int native_snapshot_width = 0;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "device_controller.h"
|
||||||
#include "display_info.h"
|
#include "display_info.h"
|
||||||
#include "minirtc.h"
|
#include "minirtc.h"
|
||||||
|
|
||||||
@@ -148,6 +149,12 @@ struct RemoteSession {
|
|||||||
bool remote_service_available_ = false;
|
bool remote_service_available_ = false;
|
||||||
std::string remote_interactive_stage_;
|
std::string remote_interactive_stage_;
|
||||||
std::vector<DisplayInfo> display_info_list_;
|
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.
|
// Shared by minirtc callbacks, Slint rendering and SDL audio callbacks.
|
||||||
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
|
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
|
||||||
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
|
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
|
||||||
|
|||||||
@@ -133,6 +133,9 @@ export component StreamWindow inherits Window {
|
|||||||
in property <[string]> displays;
|
in property <[string]> displays;
|
||||||
in-out property <int> selected-display: 0;
|
in-out property <int> selected-display: 0;
|
||||||
in property <bool> mouse-control-enabled: true;
|
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> audio-enabled: true;
|
||||||
in property <bool> srtp-enabled: false;
|
in property <bool> srtp-enabled: false;
|
||||||
in property <bool> fullscreen-enabled: false;
|
in property <bool> fullscreen-enabled: false;
|
||||||
@@ -551,6 +554,36 @@ export component StreamWindow inherits Window {
|
|||||||
accept
|
accept
|
||||||
}
|
}
|
||||||
pointer := TouchArea {
|
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) => {
|
pointer-event(event) => {
|
||||||
let over-control = self.mouse-x >= control.x
|
let over-control = self.mouse-x >= control.x
|
||||||
&& self.mouse-x <= control.x + control.width
|
&& self.mouse-x <= control.x + control.width
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
#include "linux_cursor_shape.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
#include "shared_cursor_state.h"
|
||||||
#include "wayland_portal_shared.h"
|
#include "wayland_portal_shared.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
@@ -35,7 +37,8 @@ constexpr auto kPipeWireCloseSettleDelay = std::chrono::milliseconds(200);
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ScreenCapturerWayland::ScreenCapturerWayland() {}
|
ScreenCapturerWayland::ScreenCapturerWayland()
|
||||||
|
: cursor_theme_matcher_(std::make_unique<LinuxCursorThemeMatcher>()) {}
|
||||||
|
|
||||||
ScreenCapturerWayland::~ScreenCapturerWayland() { Destroy(); }
|
ScreenCapturerWayland::~ScreenCapturerWayland() { Destroy(); }
|
||||||
|
|
||||||
@@ -66,6 +69,10 @@ int ScreenCapturerWayland::Init(const int fps, cb_desktop_data cb) {
|
|||||||
callback_ = cb;
|
callback_ = cb;
|
||||||
pointer_granted_ = false;
|
pointer_granted_ = false;
|
||||||
shared_session_registered_ = 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_.clear();
|
||||||
display_info_list_.push_back(
|
display_info_list_.push_back(
|
||||||
DisplayInfo(display_name_, 0, 0, kFallbackWidth, kFallbackHeight));
|
DisplayInfo(display_name_, 0, 0, kFallbackWidth, kFallbackHeight));
|
||||||
@@ -100,6 +107,10 @@ int ScreenCapturerWayland::Start(bool show_cursor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show_cursor_ = show_cursor;
|
show_cursor_ = show_cursor;
|
||||||
|
cursor_metadata_enabled_ = false;
|
||||||
|
cursor_metadata_seen_ = false;
|
||||||
|
cursor_metadata_missing_buffers_ = 0;
|
||||||
|
ClearSharedCursorState();
|
||||||
paused_ = false;
|
paused_ = false;
|
||||||
pipewire_node_id_ = 0;
|
pipewire_node_id_ = 0;
|
||||||
UpdateDisplayGeometry(
|
UpdateDisplayGeometry(
|
||||||
@@ -119,6 +130,10 @@ int ScreenCapturerWayland::Stop() {
|
|||||||
thread_.join();
|
thread_.join();
|
||||||
}
|
}
|
||||||
pipewire_node_id_ = 0;
|
pipewire_node_id_ = 0;
|
||||||
|
cursor_metadata_enabled_ = false;
|
||||||
|
cursor_metadata_seen_ = false;
|
||||||
|
cursor_metadata_missing_buffers_ = 0;
|
||||||
|
ClearSharedCursorState();
|
||||||
UpdateDisplayGeometry(
|
UpdateDisplayGeometry(
|
||||||
logical_width_ > 0 ? logical_width_ : kFallbackWidth,
|
logical_width_ > 0 ? logical_width_ : kFallbackWidth,
|
||||||
logical_height_ > 0 ? logical_height_ : kFallbackHeight);
|
logical_height_ > 0 ? logical_height_ : kFallbackHeight);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct pw_thread_loop;
|
|||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -23,6 +24,8 @@ struct pw_thread_loop;
|
|||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
|
class LinuxCursorThemeMatcher;
|
||||||
|
|
||||||
class ScreenCapturerWayland : public ScreenCapturer {
|
class ScreenCapturerWayland : public ScreenCapturer {
|
||||||
public:
|
public:
|
||||||
enum class PipeWireConnectMode { kTargetObject, kNodeId, kAny };
|
enum class PipeWireConnectMode { kTargetObject, kNodeId, kAny };
|
||||||
@@ -51,6 +54,7 @@ class ScreenCapturerWayland : public ScreenCapturer {
|
|||||||
bool CreatePortalSession();
|
bool CreatePortalSession();
|
||||||
bool SelectPortalDevices();
|
bool SelectPortalDevices();
|
||||||
bool SelectPortalSource();
|
bool SelectPortalSource();
|
||||||
|
bool GetAvailableCursorModes(uint32_t* modes) const;
|
||||||
bool StartPortalSession();
|
bool StartPortalSession();
|
||||||
bool EnsurePipeWireRuntimeAvailable() const;
|
bool EnsurePipeWireRuntimeAvailable() const;
|
||||||
bool OpenPipeWireRemote();
|
bool OpenPipeWireRemote();
|
||||||
@@ -95,6 +99,9 @@ class ScreenCapturerWayland : public ScreenCapturer {
|
|||||||
bool pipewire_thread_loop_started_ = false;
|
bool pipewire_thread_loop_started_ = false;
|
||||||
bool pointer_granted_ = false;
|
bool pointer_granted_ = false;
|
||||||
bool shared_session_registered_ = 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;
|
bool portal_has_logical_size_ = false;
|
||||||
uint32_t spa_video_format_ = 0;
|
uint32_t spa_video_format_ = 0;
|
||||||
int frame_width_ = 0;
|
int frame_width_ = 0;
|
||||||
@@ -104,6 +111,7 @@ class ScreenCapturerWayland : public ScreenCapturer {
|
|||||||
int portal_stream_height_ = 0;
|
int portal_stream_height_ = 0;
|
||||||
int logical_width_ = 0;
|
int logical_width_ = 0;
|
||||||
int logical_height_ = 0;
|
int logical_height_ = 0;
|
||||||
|
std::unique_ptr<LinuxCursorThemeMatcher> cursor_theme_matcher_;
|
||||||
|
|
||||||
std::vector<uint8_t> y_plane_;
|
std::vector<uint8_t> y_plane_;
|
||||||
std::vector<uint8_t> uv_plane_;
|
std::vector<uint8_t> uv_plane_;
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
|
|
||||||
#include "display_stream_id.h"
|
#include "display_stream_id.h"
|
||||||
#include "libyuv.h"
|
#include "libyuv.h"
|
||||||
|
#include "linux_cursor_shape.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
#include "shared_cursor_state.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
@@ -199,6 +201,145 @@ int64_t NowMs() {
|
|||||||
.count();
|
.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) {
|
double SnapLikelyFractionalScale(double observed_scale) {
|
||||||
static constexpr double kCandidates[] = {
|
static constexpr double kCandidates[] = {
|
||||||
1.0, 1.25, 1.3333333333, 1.5, 1.6666666667, 1.75, 2.0, 2.25, 2.5, 3.0};
|
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];
|
uint8_t buffer[1024];
|
||||||
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
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;
|
uint32_t param_count = 0;
|
||||||
|
|
||||||
params[param_count++] =
|
params[param_count++] =
|
||||||
@@ -525,10 +666,29 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
|
|||||||
CROSSDESK_SPA_PARAM_META_SIZE,
|
CROSSDESK_SPA_PARAM_META_SIZE,
|
||||||
SPA_POD_Int(sizeof(struct spa_meta_header))));
|
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_) {
|
if (self->pw_stream_) {
|
||||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||||
if (pipewire) {
|
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);
|
self->pipewire_format_ready_.store(true);
|
||||||
@@ -786,6 +946,57 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() {
|
|||||||
return;
|
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];
|
const spa_data& data = spa_buffer->datas[0];
|
||||||
if (!data.chunk) {
|
if (!data.chunk) {
|
||||||
requeue();
|
requeue();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
#include "shared_cursor_state.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ constexpr const char* kPortalSessionPathPrefix =
|
|||||||
constexpr uint32_t kScreenCastSourceMonitor = 1u;
|
constexpr uint32_t kScreenCastSourceMonitor = 1u;
|
||||||
constexpr uint32_t kCursorModeHidden = 1u;
|
constexpr uint32_t kCursorModeHidden = 1u;
|
||||||
constexpr uint32_t kCursorModeEmbedded = 2u;
|
constexpr uint32_t kCursorModeEmbedded = 2u;
|
||||||
|
constexpr uint32_t kCursorModeMetadata = 4u;
|
||||||
constexpr uint32_t kRemoteDesktopDevicePointer = 2u;
|
constexpr uint32_t kRemoteDesktopDevicePointer = 2u;
|
||||||
|
|
||||||
std::string MakeToken(const char* prefix) {
|
std::string MakeToken(const char* prefix) {
|
||||||
@@ -482,6 +484,47 @@ bool ScreenCapturerWayland::SelectPortalSource() {
|
|||||||
return false;
|
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();
|
const char* session_handle = session_handle_.c_str();
|
||||||
return SendPortalRequestAndHandleResponse(
|
return SendPortalRequestAndHandleResponse(
|
||||||
dbus_connection_, kPortalScreenCastInterface, "SelectSources",
|
dbus_connection_, kPortalScreenCastInterface, "SelectSources",
|
||||||
@@ -496,9 +539,7 @@ bool ScreenCapturerWayland::SelectPortalSource() {
|
|||||||
&options);
|
&options);
|
||||||
AppendDictEntryUint32(&options, "types", kScreenCastSourceMonitor);
|
AppendDictEntryUint32(&options, "types", kScreenCastSourceMonitor);
|
||||||
AppendDictEntryBool(&options, "multiple", false);
|
AppendDictEntryBool(&options, "multiple", false);
|
||||||
AppendDictEntryUint32(
|
AppendDictEntryUint32(&options, "cursor_mode", cursor_mode);
|
||||||
&options, "cursor_mode",
|
|
||||||
show_cursor_ ? kCursorModeEmbedded : kCursorModeHidden);
|
|
||||||
AppendDictEntryString(&options, "handle_token",
|
AppendDictEntryString(&options, "handle_token",
|
||||||
MakeToken("crossdesk_req"));
|
MakeToken("crossdesk_req"));
|
||||||
dbus_message_iter_close_container(&iter, &options);
|
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() {
|
bool ScreenCapturerWayland::SelectPortalDevices() {
|
||||||
if (!dbus_connection_ || session_handle_.empty()) {
|
if (!dbus_connection_ || session_handle_.empty()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+1
-1
Submodule submodules/minirtc updated: d0a78e8bf9...3aa5fa5ed0
+3
-2
@@ -37,7 +37,7 @@ function setup_platform_settings()
|
|||||||
add_requires("libyuv")
|
add_requires("libyuv")
|
||||||
add_syslinks("pthread", "dl")
|
add_syslinks("pthread", "dl")
|
||||||
add_links("asound", "X11", "Xext", "Xrender", "Xft", "Xtst",
|
add_links("asound", "X11", "Xext", "Xrender", "Xft", "Xtst",
|
||||||
"Xrandr", "Xfixes")
|
"Xrandr", "Xfixes", "Xcursor")
|
||||||
add_existing_include_dirs({
|
add_existing_include_dirs({
|
||||||
"/usr/include/freetype2",
|
"/usr/include/freetype2",
|
||||||
"/usr/local/include/freetype2"
|
"/usr/local/include/freetype2"
|
||||||
@@ -84,7 +84,8 @@ function setup_platform_settings()
|
|||||||
add_ldflags("-Wl,-ld_classic")
|
add_ldflags("-Wl,-ld_classic")
|
||||||
end
|
end
|
||||||
add_cxflags("-Wno-unused-variable")
|
add_cxflags("-Wno-unused-variable")
|
||||||
add_frameworks("Cocoa", "Metal", "QuartzCore", "IOSurface",
|
add_frameworks("Cocoa", "CoreGraphics", "Metal", "QuartzCore",
|
||||||
|
"IOSurface",
|
||||||
"ScreenCaptureKit", "AVFoundation", "CoreMedia", "CoreVideo",
|
"ScreenCaptureKit", "AVFoundation", "CoreMedia", "CoreVideo",
|
||||||
"CoreAudio", "AudioToolbox")
|
"CoreAudio", "AudioToolbox")
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user