mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-23 04:53:06 +08:00
Compare commits
17
Commits
2186ebd7eb
...
v1.4.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
126f455681 | ||
|
|
d6d0a548b2 | ||
|
|
74ac849efe | ||
|
|
e2cd18dba4 | ||
|
|
d2b1329eca | ||
|
|
0653a95250 | ||
|
|
bd73ef55f1 | ||
|
|
b854512c24 | ||
|
|
4528caf0f4 | ||
|
|
f6e89725aa | ||
|
|
62f79a8e44 | ||
|
|
096bc68d35 | ||
|
|
8ea4668305 | ||
|
|
21eec76008 | ||
|
|
3e63675787 | ||
|
|
439f137764 | ||
|
|
f537147f54 |
@@ -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
|
||||
|
||||
@@ -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_
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <libyuv.h>
|
||||
#include <slint.h>
|
||||
#include <tinyfiledialogs.h>
|
||||
|
||||
@@ -29,16 +28,14 @@
|
||||
#include "crossdesk_ui.h"
|
||||
#include "fa_solid_900.h"
|
||||
#include "localization.h"
|
||||
#include "nv12_scaler.h"
|
||||
#include "platform.h"
|
||||
#include "platform/video_renderer.h"
|
||||
#include "rendering/slint_video_presenter.h"
|
||||
#if _WIN32
|
||||
#include <windows.h>
|
||||
#include <GL/gl.h>
|
||||
|
||||
#include "platform/tray/win_tray.h"
|
||||
#include "platform/windows_opengl_video_renderer.h"
|
||||
#elif defined(__APPLE__)
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#include "platform/tray/mac_tray.h"
|
||||
#include "platform/window_drag.h"
|
||||
#elif defined(__linux__)
|
||||
@@ -60,41 +57,6 @@ namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
constexpr GLint kGlClampToEdge = 0x812F;
|
||||
#else
|
||||
constexpr int kMetalAttachmentAttemptLimit = 30;
|
||||
#endif
|
||||
|
||||
struct VideoRenderSize {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
bool operator==(const VideoRenderSize&) const = default;
|
||||
};
|
||||
|
||||
VideoRenderSize FitVideoToRenderArea(int source_width, int source_height,
|
||||
int area_width, int area_height) {
|
||||
if (source_width <= 0 || source_height <= 0 || area_width <= 0 ||
|
||||
area_height <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const double scale =
|
||||
std::min({1.0, static_cast<double>(area_width) / source_width,
|
||||
static_cast<double>(area_height) / source_height});
|
||||
int width = std::max(2, static_cast<int>(std::floor(source_width * scale)));
|
||||
int height =
|
||||
std::max(2, static_cast<int>(std::floor(source_height * scale)));
|
||||
|
||||
// NV12 chroma samples cover 2x2 pixels. Keep the render buffer even-sized
|
||||
// so libyuv never needs to read a partial chroma sample at the edge.
|
||||
width &= ~1;
|
||||
height &= ~1;
|
||||
return {std::min(width, source_width & ~1),
|
||||
std::min(height, source_height & ~1)};
|
||||
}
|
||||
|
||||
std::string CreatePasswordChangeRequestId(uint64_t sequence) {
|
||||
const auto timestamp = std::chrono::system_clock::now()
|
||||
.time_since_epoch()
|
||||
@@ -930,19 +892,7 @@ struct GuiApplication::SlintUi {
|
||||
std::shared_ptr<slint::VectorModel<slint::SharedString>>
|
||||
controller_name_model =
|
||||
std::make_shared<slint::VectorModel<slint::SharedString>>();
|
||||
std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
|
||||
std::vector<uint8_t> scaled_video_frame;
|
||||
std::vector<uint8_t> scaled_video_frame_scratch;
|
||||
#if !defined(__APPLE__)
|
||||
std::mutex video_gl_mutex;
|
||||
std::vector<uint8_t> video_gl_conversion_frame;
|
||||
std::vector<uint8_t> video_gl_pending_frame;
|
||||
uint32_t video_gl_texture = 0;
|
||||
VideoRenderSize video_gl_texture_size;
|
||||
VideoRenderSize video_gl_image_size;
|
||||
VideoRenderSize video_gl_pending_size;
|
||||
bool video_gl_pending_dirty = false;
|
||||
#endif
|
||||
std::unique_ptr<SlintVideoPresenter> video_presenter;
|
||||
std::vector<std::string> tab_order;
|
||||
std::vector<std::string> tab_ids;
|
||||
std::string tab_model_signature;
|
||||
@@ -957,9 +907,6 @@ struct GuiApplication::SlintUi {
|
||||
WindowDragState server_drag;
|
||||
int main_native_titlebar_attempts = 30;
|
||||
int stream_live_resize_configuration_attempts = 0;
|
||||
#if defined(__APPLE__)
|
||||
int stream_video_attachment_attempts = 0;
|
||||
#endif
|
||||
int stream_initial_position_attempts = 0;
|
||||
int server_initial_position_attempts = 0;
|
||||
#if _WIN32
|
||||
@@ -1114,15 +1061,9 @@ void GuiApplication::InitializeModules() {
|
||||
if (modules_inited_) {
|
||||
return;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
windows_opengl_video_renderer_ =
|
||||
std::make_unique<WindowsOpenGlVideoRenderer>();
|
||||
#elif defined(__APPLE__)
|
||||
// Metal setup can log device, shader, or pipeline failures. Initialize it
|
||||
// here, after Run() has selected the application's log directory, rather
|
||||
// than from GuiRuntime's constructor before InitializeLogger().
|
||||
mac_metal_video_renderer_ = std::make_unique<MacMetalVideoRenderer>();
|
||||
#endif
|
||||
// Renderer construction can log device, shader, or pipeline failures.
|
||||
// Initialize it after Run() has selected the application's log directory.
|
||||
video_renderer_ = CreateVideoRenderer();
|
||||
devices_.Initialize();
|
||||
CreateConnectionPeer();
|
||||
modules_inited_ = true;
|
||||
@@ -1157,6 +1098,8 @@ void GuiApplication::InitializeUi() {
|
||||
}
|
||||
#endif
|
||||
ui_ = std::make_unique<SlintUi>();
|
||||
ui_->video_presenter =
|
||||
std::make_unique<SlintVideoPresenter>(*video_renderer_);
|
||||
#if _WIN32
|
||||
ui_->main->set_custom_titlebar(true);
|
||||
#elif defined(__linux__)
|
||||
@@ -1220,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__)
|
||||
@@ -1265,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_);
|
||||
@@ -1275,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) {
|
||||
@@ -1297,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] {
|
||||
@@ -1858,6 +1796,7 @@ void GuiApplication::Tick() {
|
||||
SyncStreamKeyboardFocus();
|
||||
#endif
|
||||
devices_.UpdateInteractions();
|
||||
ShareLocalCursorState();
|
||||
|
||||
UpdateLocalization();
|
||||
SyncMainWindow();
|
||||
@@ -1867,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;
|
||||
@@ -2261,9 +2257,6 @@ void GuiApplication::SyncStreamKeyboardFocus() {
|
||||
}
|
||||
|
||||
void GuiApplication::SyncStreamWindow() {
|
||||
#if defined(__APPLE__)
|
||||
bool stream_window_created_this_sync = false;
|
||||
#endif
|
||||
bool has_sessions = false;
|
||||
{
|
||||
std::shared_lock lock(remote_sessions_mutex_);
|
||||
@@ -2275,15 +2268,7 @@ void GuiApplication::SyncStreamWindow() {
|
||||
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
|
||||
#endif
|
||||
RegisterFontAwesome((*ui_->stream)->window());
|
||||
#if defined(__APPLE__)
|
||||
// Commit the first visible AppKit window as an ordinary opaque window.
|
||||
// The process-wide Slint event loop is already running here, so
|
||||
// appkit_view() may exist before show(); that does not mean the native
|
||||
// titlebar style has been committed yet.
|
||||
(*ui_->stream)->set_native_video_enabled(false);
|
||||
#else
|
||||
ConfigureStreamVideoRenderer();
|
||||
#endif
|
||||
// Slint globals belong to a component tree. Force the next localization
|
||||
// pass to initialize the newly-created, independent stream window tree.
|
||||
ui_->localized_language = -1;
|
||||
@@ -2293,15 +2278,11 @@ void GuiApplication::SyncStreamWindow() {
|
||||
(*ui_->stream)->set_stats_rows(ui_->stats_model);
|
||||
BindStreamCallbacks();
|
||||
(*ui_->stream)->show();
|
||||
#if defined(__APPLE__)
|
||||
stream_window_created_this_sync = true;
|
||||
#endif
|
||||
// Repeat after the next layout pass because the backend resolves the
|
||||
// initial DPI and preferred size asynchronously on some platforms.
|
||||
ui_->stream_initial_position_attempts = 2;
|
||||
#if defined(__APPLE__)
|
||||
ui_->stream_live_resize_configuration_attempts = 30;
|
||||
ui_->stream_video_attachment_attempts = kMetalAttachmentAttemptLimit;
|
||||
#endif
|
||||
stream_window_created_ = true;
|
||||
stream_window_inited_ = true;
|
||||
@@ -2321,24 +2302,10 @@ void GuiApplication::SyncStreamWindow() {
|
||||
--ui_->stream_live_resize_configuration_attempts;
|
||||
}
|
||||
}
|
||||
if (ui_->stream_video_attachment_attempts > 0 &&
|
||||
!stream_window_created_this_sync) {
|
||||
ConfigureStreamVideoRenderer();
|
||||
const bool metal_unavailable =
|
||||
!mac_metal_video_renderer_ || !mac_metal_video_renderer_->IsReady();
|
||||
const bool metal_attached =
|
||||
mac_metal_video_renderer_ && mac_metal_video_renderer_->IsAttached();
|
||||
if (metal_unavailable) {
|
||||
ui_->stream_video_attachment_attempts = 0;
|
||||
} else if (metal_attached) {
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
ui_->stream_video_attachment_attempts = 0;
|
||||
} else if (--ui_->stream_video_attachment_attempts == 0) {
|
||||
(*ui_->stream)->set_native_video_enabled(false);
|
||||
LOG_WARN("Unable to attach the Metal video surface; using CPU rendering");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (ui_->video_presenter && ui_->video_presenter->EnsureAttached()) {
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
if (ui_->stream_initial_position_attempts > 0) {
|
||||
if (PositionWindowAtCenter(
|
||||
(*ui_->stream)->window(), ui_->main->window(),
|
||||
@@ -2351,29 +2318,20 @@ void GuiApplication::SyncStreamWindow() {
|
||||
}
|
||||
if (!has_sessions) {
|
||||
SetStreamKeyboardFocus(false);
|
||||
#if defined(_WIN32)
|
||||
if (windows_opengl_video_renderer_) {
|
||||
windows_opengl_video_renderer_->SetSelectedStream({});
|
||||
}
|
||||
#endif
|
||||
#if defined(__APPLE__)
|
||||
void* stream_view = (*ui_->stream)->window().appkit_view();
|
||||
SetStreamWindowFullscreen(false);
|
||||
UnregisterStreamWindow(stream_view);
|
||||
if (mac_metal_video_renderer_) {
|
||||
mac_metal_video_renderer_->SetSelectedStream({});
|
||||
mac_metal_video_renderer_->Detach();
|
||||
}
|
||||
#endif
|
||||
if (ui_->video_presenter) {
|
||||
ui_->video_presenter->Detach();
|
||||
}
|
||||
(*ui_->stream)->hide();
|
||||
ui_->stream.reset();
|
||||
stream_window_created_ = false;
|
||||
stream_window_inited_ = false;
|
||||
ui_->stream_initial_position_attempts = 0;
|
||||
ui_->stream_live_resize_configuration_attempts = 0;
|
||||
#if defined(__APPLE__)
|
||||
ui_->stream_video_attachment_attempts = 0;
|
||||
#endif
|
||||
focused_remote_id_.clear();
|
||||
controlled_remote_id_.clear();
|
||||
ui_->tab_order.clear();
|
||||
@@ -2464,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();
|
||||
@@ -2590,7 +2565,7 @@ void GuiApplication::SyncStreamWindow() {
|
||||
}
|
||||
|
||||
void GuiApplication::SyncStreamVideoFrame() {
|
||||
if (!ui_ || !ui_->stream) {
|
||||
if (!ui_ || !ui_->stream || !ui_->video_presenter) {
|
||||
return;
|
||||
}
|
||||
auto props = SelectedSession();
|
||||
@@ -2598,421 +2573,46 @@ void GuiApplication::SyncStreamVideoFrame() {
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
if (mac_metal_video_renderer_ && mac_metal_video_renderer_->IsReady() &&
|
||||
mac_metal_video_renderer_->IsAttached()) {
|
||||
const double top_inset =
|
||||
!fullscreen_button_pressed_ && ui_->tab_ids.size() > 1 ? 30.0 : 0.0;
|
||||
const auto render_outcome = mac_metal_video_renderer_->RenderLatest(
|
||||
props->remote_id_, top_inset, !fullscreen_button_pressed_);
|
||||
if (render_outcome.result ==
|
||||
MacMetalVideoRenderer::RenderResult::rendered) {
|
||||
if (!(*ui_->stream)->get_has_frame()) {
|
||||
(*ui_->stream)->set_has_frame(true);
|
||||
}
|
||||
if (!(*ui_->stream)->get_receiving_text().empty()) {
|
||||
(*ui_->stream)->set_receiving_text("");
|
||||
}
|
||||
if (render_outcome.width > 0 && render_outcome.height > 0 &&
|
||||
render_outcome.sequence > 0) {
|
||||
(*ui_->stream)
|
||||
->set_stats_resolution(
|
||||
UiText(std::to_string(render_outcome.width) + "x" +
|
||||
std::to_string(render_outcome.height)));
|
||||
if (ui_->displayed_frame_sequence[props->remote_id_] !=
|
||||
render_outcome.sequence) {
|
||||
ui_->displayed_frame_sequence[props->remote_id_] =
|
||||
render_outcome.sequence;
|
||||
++props->frame_count_;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (render_outcome.result == MacMetalVideoRenderer::RenderResult::empty) {
|
||||
if ((*ui_->stream)->get_has_frame()) {
|
||||
(*ui_->stream)->set_has_frame(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (render_outcome.result == MacMetalVideoRenderer::RenderResult::idle) {
|
||||
return;
|
||||
}
|
||||
// A transient Metal error must not change renderer ownership mid-window.
|
||||
// Keep the last frame and retry on the next UI tick.
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
if (windows_opengl_video_renderer_ &&
|
||||
windows_opengl_video_renderer_->IsReady()) {
|
||||
windows_opengl_video_renderer_->SetSelectedStream(props->remote_id_);
|
||||
SubmitCachedFrameToOpenGl(props);
|
||||
if (!(*ui_->stream)->get_native_video_enabled()) {
|
||||
(*ui_->stream)->set_native_video_enabled(true);
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint64_t sequence = 0;
|
||||
{
|
||||
std::lock_guard lock(props->video_frame_mutex_);
|
||||
width = props->video_width_;
|
||||
height = props->video_height_;
|
||||
sequence = props->video_frame_sequence_;
|
||||
}
|
||||
const bool has_frame = width > 0 && height > 0 && sequence > 0;
|
||||
(*ui_->stream)->set_has_frame(has_frame);
|
||||
if (has_frame) {
|
||||
(*ui_->stream)->set_receiving_text("");
|
||||
(*ui_->stream)
|
||||
->set_stats_resolution(UiText(std::to_string(width) + "x" +
|
||||
std::to_string(height)));
|
||||
if (ui_->displayed_frame_sequence[props->remote_id_] != sequence) {
|
||||
ui_->displayed_frame_sequence[props->remote_id_] = sequence;
|
||||
++props->frame_count_;
|
||||
}
|
||||
}
|
||||
(*ui_->stream)->window().request_redraw();
|
||||
return;
|
||||
}
|
||||
if ((*ui_->stream)->get_native_video_enabled()) {
|
||||
(*ui_->stream)->set_native_video_enabled(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
std::shared_ptr<std::vector<unsigned char>> frame;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint64_t sequence = 0;
|
||||
SlintVideoPresenter::Frame frame;
|
||||
frame.remote_id = props->remote_id_;
|
||||
{
|
||||
std::lock_guard lock(props->video_frame_mutex_);
|
||||
frame = props->front_frame_;
|
||||
width = props->video_width_;
|
||||
height = props->video_height_;
|
||||
sequence = props->video_frame_sequence_;
|
||||
frame.nv12 = props->front_frame_;
|
||||
frame.width = props->video_width_;
|
||||
frame.height = props->video_height_;
|
||||
frame.sequence = props->video_frame_sequence_;
|
||||
}
|
||||
|
||||
const size_t nv12_size = static_cast<size_t>(width) * height * 3 / 2;
|
||||
if (!frame || width <= 0 || height <= 0 || frame->size() < nv12_size) {
|
||||
(*ui_->stream)->set_has_frame(false);
|
||||
return;
|
||||
const auto result = ui_->video_presenter->Present(frame);
|
||||
if (result.width > 0 && result.height > 0) {
|
||||
(*ui_->stream)
|
||||
->set_stats_resolution(UiText(std::to_string(result.width) + "x" +
|
||||
std::to_string(result.height)));
|
||||
}
|
||||
if (ui_->displayed_frame_sequence[props->remote_id_] == sequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto window_size = (*ui_->stream)->window().size();
|
||||
const VideoRenderSize render_size = FitVideoToRenderArea(
|
||||
width, height, static_cast<int>(window_size.width),
|
||||
static_cast<int>(window_size.height));
|
||||
const uint8_t* y_plane = frame->data();
|
||||
const uint8_t* uv_plane =
|
||||
frame->data() + static_cast<size_t>(width) * height;
|
||||
int output_width = width;
|
||||
int output_height = height;
|
||||
|
||||
if (render_size.width > 0 && render_size.height > 0 &&
|
||||
(render_size.width != width || render_size.height != height)) {
|
||||
const size_t scaled_nv12_size =
|
||||
static_cast<size_t>(render_size.width) * render_size.height * 3 / 2;
|
||||
ui_->scaled_video_frame.resize(scaled_nv12_size);
|
||||
uint8_t* scaled_y = ui_->scaled_video_frame.data();
|
||||
uint8_t* scaled_uv =
|
||||
scaled_y +
|
||||
static_cast<size_t>(render_size.width) * render_size.height;
|
||||
if (ScaleNv12ViaI420(
|
||||
y_plane, width, uv_plane, width, width, height, scaled_y,
|
||||
render_size.width, scaled_uv, render_size.width, render_size.width,
|
||||
render_size.height, libyuv::kFilterBox,
|
||||
&ui_->scaled_video_frame_scratch) == 0) {
|
||||
y_plane = scaled_y;
|
||||
uv_plane = scaled_uv;
|
||||
output_width = render_size.width;
|
||||
output_height = render_size.height;
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
uint32_t texture_id = 0;
|
||||
{
|
||||
std::lock_guard lock(ui_->video_gl_mutex);
|
||||
texture_id = ui_->video_gl_texture;
|
||||
}
|
||||
if (texture_id != 0) {
|
||||
ui_->video_gl_conversion_frame.resize(
|
||||
static_cast<size_t>(output_width) * output_height * 4);
|
||||
if (libyuv::NV12ToABGR(
|
||||
y_plane, output_width, uv_plane, output_width,
|
||||
ui_->video_gl_conversion_frame.data(), output_width * 4,
|
||||
output_width, output_height) != 0) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(ui_->video_gl_mutex);
|
||||
if (ui_->video_gl_texture == 0) {
|
||||
return;
|
||||
}
|
||||
texture_id = ui_->video_gl_texture;
|
||||
ui_->video_gl_pending_frame.swap(ui_->video_gl_conversion_frame);
|
||||
ui_->video_gl_pending_size = {output_width, output_height};
|
||||
ui_->video_gl_pending_dirty = true;
|
||||
}
|
||||
const VideoRenderSize output_size{output_width, output_height};
|
||||
if (ui_->video_gl_image_size != output_size) {
|
||||
(*ui_->stream)
|
||||
->set_frame(slint::Image::create_from_borrowed_gl_2d_rgba_texture(
|
||||
texture_id,
|
||||
slint::Size<uint32_t>{static_cast<uint32_t>(output_width),
|
||||
static_cast<uint32_t>(output_height)}));
|
||||
ui_->video_gl_image_size = output_size;
|
||||
}
|
||||
(*ui_->stream)->set_has_frame(true);
|
||||
(*ui_->stream)->set_receiving_text("");
|
||||
(*ui_->stream)->window().request_redraw();
|
||||
ui_->displayed_frame_sequence[props->remote_id_] = sequence;
|
||||
if (result.new_frame) {
|
||||
++props->frame_count_;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(output_width,
|
||||
output_height);
|
||||
if (libyuv::NV12ToRAW(
|
||||
y_plane, output_width, uv_plane, output_width,
|
||||
reinterpret_cast<uint8_t*>(pixels.begin()), output_width * 3,
|
||||
output_width, output_height) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
(*ui_->stream)->set_frame(slint::Image(std::move(pixels)));
|
||||
(*ui_->stream)->set_has_frame(true);
|
||||
(*ui_->stream)->set_receiving_text("");
|
||||
ui_->displayed_frame_sequence[props->remote_id_] = sequence;
|
||||
++props->frame_count_;
|
||||
}
|
||||
|
||||
void GuiApplication::ConfigureStreamVideoRenderer() {
|
||||
#if defined(__APPLE__)
|
||||
if (!ui_ || !ui_->stream) {
|
||||
if (!ui_ || !ui_->stream || !ui_->video_presenter) {
|
||||
return;
|
||||
}
|
||||
const bool native_video_enabled =
|
||||
mac_metal_video_renderer_ && mac_metal_video_renderer_->IsReady();
|
||||
if (!native_video_enabled) {
|
||||
(*ui_->stream)->set_native_video_enabled(false);
|
||||
return;
|
||||
}
|
||||
if (void* view = (*ui_->stream)->window().appkit_view()) {
|
||||
// Slint applies transparency through a property binding. Set it before
|
||||
// restoring the AppKit titlebar so the later operation wins regardless of
|
||||
// whether the backend commits the binding synchronously or next tick.
|
||||
(*ui_->stream)->set_native_video_enabled(true);
|
||||
if (!mac_metal_video_renderer_->Attach(view)) {
|
||||
(*ui_->stream)->set_native_video_enabled(false);
|
||||
} else if (auto selected_session = SelectedSession()) {
|
||||
// Metal owns a separate monotonic sequence, so discard a CPU-renderer
|
||||
// sequence recorded during the short window-attachment gap.
|
||||
ui_->displayed_frame_sequence.erase(selected_session->remote_id_);
|
||||
if (mac_metal_video_renderer_->SetSelectedStream(
|
||||
selected_session->remote_id_)) {
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
SubmitCachedFrameToMetal(selected_session);
|
||||
}
|
||||
} else {
|
||||
// Keep the initial window opaque until winit has created its native
|
||||
// NSWindow. Creating it transparent opts into a full-size titlebar whose
|
||||
// title field sits underneath the standard traffic-light buttons.
|
||||
(*ui_->stream)->set_native_video_enabled(false);
|
||||
}
|
||||
return;
|
||||
#else
|
||||
if (!ui_ || !ui_->stream) {
|
||||
return;
|
||||
}
|
||||
const auto error = (*ui_->stream)->window().set_rendering_notifier(
|
||||
[this](slint::RenderingState state, slint::GraphicsAPI graphics_api) {
|
||||
if (!ui_ || graphics_api != slint::GraphicsAPI::NativeOpenGL) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(ui_->video_gl_mutex);
|
||||
if (state == slint::RenderingState::RenderingSetup) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
if (texture == 0) {
|
||||
return;
|
||||
}
|
||||
GLint previous_texture = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, kGlClampToEdge);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, kGlClampToEdge);
|
||||
glBindTexture(GL_TEXTURE_2D,
|
||||
static_cast<GLuint>(previous_texture));
|
||||
ui_->video_gl_texture = texture;
|
||||
#if defined(_WIN32)
|
||||
if (windows_opengl_video_renderer_ &&
|
||||
windows_opengl_video_renderer_->Setup()) {
|
||||
windows_opengl_video_renderer_->SetSelectedStream(
|
||||
focused_remote_id_);
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
ui_->video_presenter->PrepareWindow(
|
||||
*ui_->stream,
|
||||
[this] {
|
||||
SlintVideoPresenter::SurfaceState state;
|
||||
state.selected_stream = focused_remote_id_;
|
||||
state.fullscreen = fullscreen_button_pressed_;
|
||||
if (ui_) {
|
||||
state.tab_count = ui_->tab_ids.size();
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
if (state == slint::RenderingState::BeforeRendering &&
|
||||
windows_opengl_video_renderer_ &&
|
||||
windows_opengl_video_renderer_->IsReady() && ui_->stream &&
|
||||
(*ui_->stream)->get_native_video_enabled()) {
|
||||
const auto window_size = (*ui_->stream)->window().size();
|
||||
const float scale_factor =
|
||||
std::max(1.0f, (*ui_->stream)->window().scale_factor());
|
||||
const int top_inset =
|
||||
!fullscreen_button_pressed_ && ui_->tab_ids.size() > 1
|
||||
? static_cast<int>(std::lround(30.0f * scale_factor))
|
||||
: 0;
|
||||
windows_opengl_video_renderer_->RenderLatest(
|
||||
focused_remote_id_, static_cast<int>(window_size.width),
|
||||
static_cast<int>(window_size.height), top_inset);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (state == slint::RenderingState::BeforeRendering &&
|
||||
ui_->video_gl_texture != 0 && ui_->video_gl_pending_dirty &&
|
||||
!ui_->video_gl_pending_frame.empty()) {
|
||||
GLint previous_texture = 0;
|
||||
GLint previous_unpack_alignment = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
||||
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
|
||||
glBindTexture(GL_TEXTURE_2D, ui_->video_gl_texture);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
if (ui_->video_gl_texture_size != ui_->video_gl_pending_size) {
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
|
||||
ui_->video_gl_pending_size.width,
|
||||
ui_->video_gl_pending_size.height, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, ui_->video_gl_pending_frame.data());
|
||||
ui_->video_gl_texture_size = ui_->video_gl_pending_size;
|
||||
} else {
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,
|
||||
ui_->video_gl_pending_size.width,
|
||||
ui_->video_gl_pending_size.height, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
ui_->video_gl_pending_frame.data());
|
||||
}
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
|
||||
glBindTexture(GL_TEXTURE_2D,
|
||||
static_cast<GLuint>(previous_texture));
|
||||
ui_->video_gl_pending_dirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == slint::RenderingState::RenderingTeardown) {
|
||||
#if defined(_WIN32)
|
||||
if (windows_opengl_video_renderer_) {
|
||||
windows_opengl_video_renderer_->Teardown();
|
||||
}
|
||||
#endif
|
||||
if (ui_->video_gl_texture != 0) {
|
||||
const GLuint texture = ui_->video_gl_texture;
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
ui_->video_gl_texture = 0;
|
||||
ui_->video_gl_texture_size = {};
|
||||
ui_->video_gl_image_size = {};
|
||||
ui_->video_gl_pending_size = {};
|
||||
ui_->video_gl_pending_dirty = false;
|
||||
ui_->video_gl_pending_frame.clear();
|
||||
}
|
||||
});
|
||||
if (error.has_value()) {
|
||||
LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers");
|
||||
#if defined(_WIN32)
|
||||
} else if (windows_opengl_video_renderer_) {
|
||||
windows_opengl_video_renderer_->SetSelectedStream(focused_remote_id_);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
return state;
|
||||
},
|
||||
[this] { video_frame_dirty_.store(true, std::memory_order_release); });
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
void GuiApplication::SubmitCachedFrameToOpenGl(
|
||||
const std::shared_ptr<RemoteSession>& session) {
|
||||
if (!session || !windows_opengl_video_renderer_ ||
|
||||
!windows_opengl_video_renderer_->IsReady() ||
|
||||
session->connection_status_.load() != ConnectionStatus::Connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<std::vector<unsigned char>> cached_frame;
|
||||
int cached_width = 0;
|
||||
int cached_height = 0;
|
||||
{
|
||||
std::lock_guard lock(session->video_frame_mutex_);
|
||||
if (session->front_frame_ && !session->front_frame_->empty()) {
|
||||
cached_frame = session->front_frame_;
|
||||
cached_width = session->video_width_;
|
||||
cached_height = session->video_height_;
|
||||
} else if (session->thumbnail_frame_ &&
|
||||
!session->thumbnail_frame_->empty()) {
|
||||
cached_frame = session->thumbnail_frame_;
|
||||
cached_width = session->thumbnail_width_;
|
||||
cached_height = session->thumbnail_height_;
|
||||
}
|
||||
}
|
||||
|
||||
if (cached_frame && cached_width > 0 && cached_height > 0 &&
|
||||
windows_opengl_video_renderer_->SubmitCachedNv12(
|
||||
session->remote_id_, cached_frame->data(), cached_frame->size(),
|
||||
cached_width, cached_height) ==
|
||||
WindowsOpenGlVideoRenderer::SubmitResult::submitted) {
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
void GuiApplication::SubmitCachedFrameToMetal(
|
||||
const std::shared_ptr<RemoteSession>& session) {
|
||||
if (!session || !mac_metal_video_renderer_ ||
|
||||
!mac_metal_video_renderer_->IsReady() ||
|
||||
session->connection_status_.load() != ConnectionStatus::Connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<std::vector<unsigned char>> cached_frame;
|
||||
int cached_width = 0;
|
||||
int cached_height = 0;
|
||||
{
|
||||
std::lock_guard lock(session->video_frame_mutex_);
|
||||
if (session->front_frame_ && !session->front_frame_->empty()) {
|
||||
cached_frame = session->front_frame_;
|
||||
cached_width = session->video_width_;
|
||||
cached_height = session->video_height_;
|
||||
} else if (session->thumbnail_frame_ &&
|
||||
!session->thumbnail_frame_->empty()) {
|
||||
cached_frame = session->thumbnail_frame_;
|
||||
cached_width = session->thumbnail_width_;
|
||||
cached_height = session->thumbnail_height_;
|
||||
}
|
||||
}
|
||||
|
||||
if (cached_frame && cached_width > 0 && cached_height > 0 &&
|
||||
mac_metal_video_renderer_->SubmitCachedNv12(
|
||||
session->remote_id_, cached_frame->data(), cached_frame->size(),
|
||||
cached_width, cached_height) ==
|
||||
MacMetalVideoRenderer::SubmitResult::submitted) {
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void GuiApplication::ScheduleNextVideoFrame() {
|
||||
if (!ui_) {
|
||||
return;
|
||||
@@ -3028,28 +2628,19 @@ void GuiApplication::ScheduleNextVideoFrame() {
|
||||
if (now >= next_video_frame_time_) {
|
||||
const bool frame_dirty =
|
||||
video_frame_dirty_.exchange(false, std::memory_order_acq_rel);
|
||||
#if defined(__APPLE__)
|
||||
const bool native_render_needed =
|
||||
mac_metal_video_renderer_ && mac_metal_video_renderer_->IsReady() &&
|
||||
mac_metal_video_renderer_->IsAttached() && ui_->stream &&
|
||||
mac_metal_video_renderer_->NeedsSurfaceRedraw();
|
||||
const bool native_render_needed = ui_->stream && ui_->video_presenter &&
|
||||
ui_->video_presenter->NeedsRedraw();
|
||||
if (frame_dirty || native_render_needed) {
|
||||
SyncStreamVideoFrame();
|
||||
}
|
||||
#else
|
||||
if (frame_dirty) {
|
||||
SyncStreamVideoFrame();
|
||||
}
|
||||
#endif
|
||||
do {
|
||||
next_video_frame_time_ += frame_interval;
|
||||
} while (next_video_frame_time_ <= now);
|
||||
}
|
||||
|
||||
const auto after_render = std::chrono::steady_clock::now();
|
||||
const auto remaining =
|
||||
std::max(next_video_frame_time_ - after_render,
|
||||
std::chrono::steady_clock::duration::zero());
|
||||
const auto remaining = std::max(next_video_frame_time_ - after_render,
|
||||
std::chrono::steady_clock::duration::zero());
|
||||
const auto delay_ns =
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(remaining).count();
|
||||
const auto delay_ms = std::max<int64_t>(1, (delay_ns + 999'999) / 1'000'000);
|
||||
@@ -3235,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_);
|
||||
@@ -3254,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,
|
||||
@@ -3284,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;
|
||||
|
||||
@@ -3321,20 +2909,17 @@ void GuiApplication::SelectStreamTab(int index) {
|
||||
return;
|
||||
}
|
||||
const std::string selected_remote_id = ui_->tab_ids[index];
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
const bool selection_changed = focused_remote_id_ != selected_remote_id;
|
||||
bool renderer_selection_changed = false;
|
||||
#endif
|
||||
if (!controlled_remote_id_.empty() &&
|
||||
controlled_remote_id_ != selected_remote_id) {
|
||||
keyboard_.ForceReleasePressedKeys();
|
||||
}
|
||||
focused_remote_id_ = selected_remote_id;
|
||||
controlled_remote_id_ = selected_remote_id;
|
||||
#if defined(_WIN32)
|
||||
if (windows_opengl_video_renderer_) {
|
||||
if (ui_->video_presenter) {
|
||||
renderer_selection_changed =
|
||||
windows_opengl_video_renderer_->SetSelectedStream(selected_remote_id);
|
||||
ui_->video_presenter->SelectStream(selected_remote_id);
|
||||
}
|
||||
if (renderer_selection_changed) {
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
@@ -3342,18 +2927,6 @@ void GuiApplication::SelectStreamTab(int index) {
|
||||
if (selection_changed && ui_->stream) {
|
||||
(*ui_->stream)->set_has_frame(false);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
if (mac_metal_video_renderer_) {
|
||||
renderer_selection_changed =
|
||||
mac_metal_video_renderer_->SetSelectedStream(selected_remote_id);
|
||||
}
|
||||
if (renderer_selection_changed) {
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
if (selection_changed && ui_->stream) {
|
||||
(*ui_->stream)->set_has_frame(false);
|
||||
}
|
||||
#endif
|
||||
std::shared_ptr<RemoteSession> selected_session;
|
||||
{
|
||||
std::shared_lock lock(remote_sessions_mutex_);
|
||||
@@ -3366,20 +2939,11 @@ void GuiApplication::SelectStreamTab(int index) {
|
||||
if (selected != remote_sessions_.end()) {
|
||||
selected_session = selected->second;
|
||||
}
|
||||
start_keyboard_capturer_ =
|
||||
selected_session && selected_session->control_mouse_ &&
|
||||
selected_session->connection_status_.load() ==
|
||||
ConnectionStatus::Connected;
|
||||
start_keyboard_capturer_ = selected_session &&
|
||||
selected_session->control_mouse_ &&
|
||||
selected_session->connection_status_.load() ==
|
||||
ConnectionStatus::Connected;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
if (renderer_selection_changed) {
|
||||
SubmitCachedFrameToOpenGl(selected_session);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
if (renderer_selection_changed) {
|
||||
SubmitCachedFrameToMetal(selected_session);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) {
|
||||
@@ -3423,21 +2987,15 @@ void GuiApplication::CloseStreamTab(const std::string& remote_id) {
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
remote_sessions_.erase(remote_id);
|
||||
}
|
||||
ui_->displayed_frame_sequence.erase(remote_id);
|
||||
if (ui_->video_presenter) {
|
||||
ui_->video_presenter->ForgetStream(remote_id);
|
||||
}
|
||||
if (focused_remote_id_ == remote_id) {
|
||||
focused_remote_id_.clear();
|
||||
controlled_remote_id_.clear();
|
||||
#if defined(_WIN32)
|
||||
if (windows_opengl_video_renderer_ &&
|
||||
windows_opengl_video_renderer_->SetSelectedStream({})) {
|
||||
if (ui_->video_presenter && ui_->video_presenter->SelectStream({})) {
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
if (mac_metal_video_renderer_ &&
|
||||
mac_metal_video_renderer_->SetSelectedStream({})) {
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3631,20 +3189,14 @@ void GuiApplication::Cleanup() {
|
||||
WaitForThumbnailSaveTasks();
|
||||
devices_.DestroyAudioOutput();
|
||||
if (ui_->stream) {
|
||||
#if defined(_WIN32)
|
||||
if (windows_opengl_video_renderer_) {
|
||||
windows_opengl_video_renderer_->SetSelectedStream({});
|
||||
}
|
||||
#endif
|
||||
#if defined(__APPLE__)
|
||||
void* stream_view = (*ui_->stream)->window().appkit_view();
|
||||
SetStreamWindowFullscreen(false);
|
||||
UnregisterStreamWindow(stream_view);
|
||||
if (mac_metal_video_renderer_) {
|
||||
mac_metal_video_renderer_->SetSelectedStream({});
|
||||
mac_metal_video_renderer_->Detach();
|
||||
}
|
||||
#endif
|
||||
if (ui_->video_presenter) {
|
||||
ui_->video_presenter->Detach();
|
||||
}
|
||||
(*ui_->stream)->hide();
|
||||
ui_->stream.reset();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -41,14 +43,6 @@ private:
|
||||
void SyncStreamVideoFrame();
|
||||
void ScheduleNextVideoFrame();
|
||||
void ConfigureStreamVideoRenderer();
|
||||
#if defined(_WIN32)
|
||||
void SubmitCachedFrameToOpenGl(
|
||||
const std::shared_ptr<RemoteSession>& session);
|
||||
#endif
|
||||
#if defined(__APPLE__)
|
||||
void SubmitCachedFrameToMetal(
|
||||
const std::shared_ptr<RemoteSession>& session);
|
||||
#endif
|
||||
void SyncStreamKeyboardFocus();
|
||||
void SetStreamKeyboardFocus(bool focused);
|
||||
void SyncServerWindow();
|
||||
@@ -78,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ int GuiApplication::DrawServerWindow() {
|
||||
}
|
||||
|
||||
bool GuiApplication::MinimizeMainWindowToTray() {
|
||||
if (!enable_minimize_to_tray_ || !main_window_) {
|
||||
if (!main_window_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "platform/video_renderer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// macOS video compositor used by CrossDesk only. MiniRTC continues to expose
|
||||
@@ -15,51 +17,31 @@ namespace crossdesk {
|
||||
// copy into shared Metal buffers and converts NV12 to RGB in the fragment
|
||||
// shader. AppKit attachment and rendering are main-thread operations, while
|
||||
// SubmitNv12() is safe to call from MiniRTC's decode callback thread.
|
||||
class MacMetalVideoRenderer {
|
||||
class MacMetalVideoRenderer final : public VideoRenderer {
|
||||
public:
|
||||
enum class SubmitResult {
|
||||
submitted,
|
||||
not_selected,
|
||||
dropped,
|
||||
failed,
|
||||
};
|
||||
|
||||
enum class RenderResult {
|
||||
rendered,
|
||||
idle,
|
||||
empty,
|
||||
failed,
|
||||
};
|
||||
|
||||
struct RenderOutcome {
|
||||
RenderResult result = RenderResult::failed;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
MacMetalVideoRenderer();
|
||||
~MacMetalVideoRenderer();
|
||||
~MacMetalVideoRenderer() override;
|
||||
|
||||
MacMetalVideoRenderer(const MacMetalVideoRenderer&) = delete;
|
||||
MacMetalVideoRenderer& operator=(const MacMetalVideoRenderer&) = delete;
|
||||
|
||||
bool IsReady() const;
|
||||
bool IsReady() const override;
|
||||
bool IsActive() const override;
|
||||
|
||||
// The selected stream is the only stream uploaded. Frames for background
|
||||
// tabs are intentionally dropped to avoid wasting memory bandwidth.
|
||||
bool SetSelectedStream(std::string remote_id);
|
||||
void DiscardStream(std::string_view remote_id);
|
||||
bool SetSelectedStream(std::string remote_id) override;
|
||||
void DiscardStream(std::string_view remote_id) override;
|
||||
|
||||
// Copies one tightly packed NV12 frame into a free shared Metal slot.
|
||||
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t* data,
|
||||
size_t size, int width, int height);
|
||||
size_t size, int width, int height) override;
|
||||
|
||||
// Seeds a newly selected stream from its retained CPU snapshot without
|
||||
// replacing a newer decoded frame that is already queued or rendering.
|
||||
SubmitResult SubmitCachedNv12(std::string_view remote_id,
|
||||
const uint8_t* data, size_t size, int width,
|
||||
int height);
|
||||
int height) override;
|
||||
|
||||
// Attaches a native CAMetalLayer-backed sibling below Slint's NSView and
|
||||
// restores the containing AppKit window's ordinary opaque titlebar.
|
||||
@@ -84,7 +66,7 @@ class MacMetalVideoRenderer {
|
||||
// a second per-frame CPU copy during normal playback.
|
||||
bool CopyLatestNv12(std::string_view remote_id,
|
||||
std::vector<unsigned char>* output, int* width,
|
||||
int* height) const;
|
||||
int* height) const override;
|
||||
|
||||
private:
|
||||
SubmitResult SubmitNv12Internal(std::string_view remote_id,
|
||||
|
||||
@@ -602,6 +602,10 @@ bool MacMetalVideoRenderer::IsReady() const {
|
||||
return impl_ && impl_->Ready();
|
||||
}
|
||||
|
||||
bool MacMetalVideoRenderer::IsActive() const {
|
||||
return IsReady() && IsAttached();
|
||||
}
|
||||
|
||||
bool MacMetalVideoRenderer::SetSelectedStream(std::string remote_id) {
|
||||
if (!impl_) {
|
||||
return false;
|
||||
|
||||
+202
-86
@@ -1,13 +1,20 @@
|
||||
#include "platform/windows_opengl_video_renderer.h"
|
||||
#include "platform/opengl_video_renderer.h"
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#error "WindowsOpenGlVideoRenderer is only available on Windows"
|
||||
#if !defined(_WIN32) && !defined(__linux__)
|
||||
#error "OpenGlVideoRenderer is only available on Windows and Linux"
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#elif defined(__linux__)
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
#if defined(__linux__)
|
||||
#include <GL/glx.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -49,6 +56,7 @@ struct SharedFrameState {
|
||||
};
|
||||
|
||||
template <typename T> bool LoadOpenGlFunction(T *function, const char *name) {
|
||||
#if defined(_WIN32)
|
||||
PROC address = wglGetProcAddress(name);
|
||||
if (address == nullptr || address == reinterpret_cast<PROC>(1) ||
|
||||
address == reinterpret_cast<PROC>(2) ||
|
||||
@@ -58,6 +66,14 @@ template <typename T> bool LoadOpenGlFunction(T *function, const char *name) {
|
||||
address = module ? GetProcAddress(module, name) : nullptr;
|
||||
}
|
||||
*function = reinterpret_cast<T>(address);
|
||||
#elif defined(__linux__)
|
||||
void *address = dlsym(RTLD_DEFAULT, name);
|
||||
if (address == nullptr) {
|
||||
address = reinterpret_cast<void *>(
|
||||
glXGetProcAddressARB(reinterpret_cast<const GLubyte *>(name)));
|
||||
}
|
||||
*function = reinterpret_cast<T>(address);
|
||||
#endif
|
||||
return *function != nullptr;
|
||||
}
|
||||
|
||||
@@ -85,7 +101,10 @@ struct OpenGlFunctions {
|
||||
PFNGLGETVERTEXATTRIBPOINTERVPROC get_vertex_attrib_pointer_v = nullptr;
|
||||
PFNGLLINKPROGRAMPROC link_program = nullptr;
|
||||
PFNGLSHADERSOURCEPROC shader_source = nullptr;
|
||||
PFNGLUNIFORM1FPROC uniform_1f = nullptr;
|
||||
PFNGLUNIFORM1IPROC uniform_1i = nullptr;
|
||||
PFNGLUNIFORM2FPROC uniform_2f = nullptr;
|
||||
PFNGLUNIFORM4FPROC uniform_4f = nullptr;
|
||||
PFNGLUSEPROGRAMPROC use_program = nullptr;
|
||||
PFNGLVERTEXATTRIBPOINTERPROC vertex_attrib_pointer = nullptr;
|
||||
|
||||
@@ -116,7 +135,10 @@ struct OpenGlFunctions {
|
||||
"glGetVertexAttribPointerv") &&
|
||||
LoadOpenGlFunction(&link_program, "glLinkProgram") &&
|
||||
LoadOpenGlFunction(&shader_source, "glShaderSource") &&
|
||||
LoadOpenGlFunction(&uniform_1f, "glUniform1f") &&
|
||||
LoadOpenGlFunction(&uniform_1i, "glUniform1i") &&
|
||||
LoadOpenGlFunction(&uniform_2f, "glUniform2f") &&
|
||||
LoadOpenGlFunction(&uniform_4f, "glUniform4f") &&
|
||||
LoadOpenGlFunction(&use_program, "glUseProgram") &&
|
||||
LoadOpenGlFunction(&vertex_attrib_pointer, "glVertexAttribPointer");
|
||||
}
|
||||
@@ -205,7 +227,7 @@ void RestoreVertexAttrib(const OpenGlFunctions &gl, GLuint index,
|
||||
|
||||
} // namespace
|
||||
|
||||
struct WindowsOpenGlVideoRenderer::Impl {
|
||||
struct OpenGlVideoRenderer::Impl {
|
||||
OpenGlFunctions gl;
|
||||
std::shared_ptr<SharedFrameState> frames =
|
||||
std::make_shared<SharedFrameState>();
|
||||
@@ -218,6 +240,10 @@ struct WindowsOpenGlVideoRenderer::Impl {
|
||||
GLint texcoord_location = -1;
|
||||
GLint y_texture_location = -1;
|
||||
GLint uv_texture_location = -1;
|
||||
GLint target_size_location = -1;
|
||||
GLint video_rect_location = -1;
|
||||
GLint corner_radius_location = -1;
|
||||
GLint video_enabled_location = -1;
|
||||
int texture_width = 0;
|
||||
int texture_height = 0;
|
||||
std::string uploaded_stream;
|
||||
@@ -232,6 +258,10 @@ struct WindowsOpenGlVideoRenderer::Impl {
|
||||
texcoord_location = -1;
|
||||
y_texture_location = -1;
|
||||
uv_texture_location = -1;
|
||||
target_size_location = -1;
|
||||
video_rect_location = -1;
|
||||
corner_radius_location = -1;
|
||||
video_enabled_location = -1;
|
||||
texture_width = 0;
|
||||
texture_height = 0;
|
||||
uploaded_stream.clear();
|
||||
@@ -239,12 +269,12 @@ struct WindowsOpenGlVideoRenderer::Impl {
|
||||
}
|
||||
};
|
||||
|
||||
WindowsOpenGlVideoRenderer::WindowsOpenGlVideoRenderer()
|
||||
OpenGlVideoRenderer::OpenGlVideoRenderer()
|
||||
: impl_(std::make_unique<Impl>()) {}
|
||||
|
||||
WindowsOpenGlVideoRenderer::~WindowsOpenGlVideoRenderer() = default;
|
||||
OpenGlVideoRenderer::~OpenGlVideoRenderer() = default;
|
||||
|
||||
bool WindowsOpenGlVideoRenderer::Setup() {
|
||||
bool OpenGlVideoRenderer::Setup() {
|
||||
if (IsReady()) {
|
||||
return true;
|
||||
}
|
||||
@@ -274,15 +304,41 @@ precision mediump float;
|
||||
#endif
|
||||
uniform sampler2D y_texture;
|
||||
uniform sampler2D uv_texture;
|
||||
uniform vec2 target_size;
|
||||
uniform vec4 video_rect;
|
||||
uniform float corner_radius;
|
||||
uniform float video_enabled;
|
||||
varying vec2 video_texcoord;
|
||||
void main() {
|
||||
float y = 1.16438356 *
|
||||
(texture2D(y_texture, video_texcoord).r - 16.0 / 255.0);
|
||||
vec2 uv = texture2D(uv_texture, video_texcoord).ra - vec2(0.5, 0.5);
|
||||
vec3 rgb = vec3(y + 1.59602678 * uv.y,
|
||||
y - 0.39176229 * uv.x - 0.81296764 * uv.y,
|
||||
y + 2.01723214 * uv.x);
|
||||
gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0);
|
||||
vec2 surface_point = video_texcoord * target_size;
|
||||
vec3 rgb = vec3(0.0);
|
||||
if (video_enabled > 0.5 &&
|
||||
surface_point.x >= video_rect.x &&
|
||||
surface_point.x <= video_rect.x + video_rect.z &&
|
||||
surface_point.y >= video_rect.y &&
|
||||
surface_point.y <= video_rect.y + video_rect.w) {
|
||||
vec2 frame_texcoord =
|
||||
(surface_point - video_rect.xy) / video_rect.zw;
|
||||
float y = 1.16438356 *
|
||||
(texture2D(y_texture, frame_texcoord).r - 16.0 / 255.0);
|
||||
vec2 uv =
|
||||
texture2D(uv_texture, frame_texcoord).ra - vec2(0.5, 0.5);
|
||||
rgb = clamp(vec3(y + 1.59602678 * uv.y,
|
||||
y - 0.39176229 * uv.x - 0.81296764 * uv.y,
|
||||
y + 2.01723214 * uv.x),
|
||||
0.0, 1.0);
|
||||
}
|
||||
float coverage = 1.0;
|
||||
if (corner_radius > 0.0) {
|
||||
vec2 half_size = target_size * 0.5;
|
||||
vec2 corner = abs(surface_point - half_size) -
|
||||
(half_size - vec2(corner_radius));
|
||||
float distance_to_edge =
|
||||
length(max(corner, vec2(0.0))) +
|
||||
min(max(corner.x, corner.y), 0.0) - corner_radius;
|
||||
coverage = clamp(0.5 - distance_to_edge, 0.0, 1.0);
|
||||
}
|
||||
gl_FragColor = vec4(rgb * coverage, coverage);
|
||||
}
|
||||
)glsl";
|
||||
static constexpr char kDesktopVertexShader[] = R"glsl(
|
||||
@@ -299,15 +355,41 @@ void main() {
|
||||
#version 110
|
||||
uniform sampler2D y_texture;
|
||||
uniform sampler2D uv_texture;
|
||||
uniform vec2 target_size;
|
||||
uniform vec4 video_rect;
|
||||
uniform float corner_radius;
|
||||
uniform float video_enabled;
|
||||
varying vec2 video_texcoord;
|
||||
void main() {
|
||||
float y = 1.16438356 *
|
||||
(texture2D(y_texture, video_texcoord).r - 16.0 / 255.0);
|
||||
vec2 uv = texture2D(uv_texture, video_texcoord).ra - vec2(0.5, 0.5);
|
||||
vec3 rgb = vec3(y + 1.59602678 * uv.y,
|
||||
y - 0.39176229 * uv.x - 0.81296764 * uv.y,
|
||||
y + 2.01723214 * uv.x);
|
||||
gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0);
|
||||
vec2 surface_point = video_texcoord * target_size;
|
||||
vec3 rgb = vec3(0.0);
|
||||
if (video_enabled > 0.5 &&
|
||||
surface_point.x >= video_rect.x &&
|
||||
surface_point.x <= video_rect.x + video_rect.z &&
|
||||
surface_point.y >= video_rect.y &&
|
||||
surface_point.y <= video_rect.y + video_rect.w) {
|
||||
vec2 frame_texcoord =
|
||||
(surface_point - video_rect.xy) / video_rect.zw;
|
||||
float y = 1.16438356 *
|
||||
(texture2D(y_texture, frame_texcoord).r - 16.0 / 255.0);
|
||||
vec2 uv =
|
||||
texture2D(uv_texture, frame_texcoord).ra - vec2(0.5, 0.5);
|
||||
rgb = clamp(vec3(y + 1.59602678 * uv.y,
|
||||
y - 0.39176229 * uv.x - 0.81296764 * uv.y,
|
||||
y + 2.01723214 * uv.x),
|
||||
0.0, 1.0);
|
||||
}
|
||||
float coverage = 1.0;
|
||||
if (corner_radius > 0.0) {
|
||||
vec2 half_size = target_size * 0.5;
|
||||
vec2 corner = abs(surface_point - half_size) -
|
||||
(half_size - vec2(corner_radius));
|
||||
float distance_to_edge =
|
||||
length(max(corner, vec2(0.0))) +
|
||||
min(max(corner.x, corner.y), 0.0) - corner_radius;
|
||||
coverage = clamp(0.5 - distance_to_edge, 0.0, 1.0);
|
||||
}
|
||||
gl_FragColor = vec4(rgb * coverage, coverage);
|
||||
}
|
||||
)glsl";
|
||||
|
||||
@@ -363,8 +445,19 @@ void main() {
|
||||
impl_->gl.get_uniform_location(impl_->program, "y_texture");
|
||||
impl_->uv_texture_location =
|
||||
impl_->gl.get_uniform_location(impl_->program, "uv_texture");
|
||||
impl_->target_size_location =
|
||||
impl_->gl.get_uniform_location(impl_->program, "target_size");
|
||||
impl_->video_rect_location =
|
||||
impl_->gl.get_uniform_location(impl_->program, "video_rect");
|
||||
impl_->corner_radius_location =
|
||||
impl_->gl.get_uniform_location(impl_->program, "corner_radius");
|
||||
impl_->video_enabled_location =
|
||||
impl_->gl.get_uniform_location(impl_->program, "video_enabled");
|
||||
if (impl_->position_location < 0 || impl_->texcoord_location < 0 ||
|
||||
impl_->y_texture_location < 0 || impl_->uv_texture_location < 0) {
|
||||
impl_->y_texture_location < 0 || impl_->uv_texture_location < 0 ||
|
||||
impl_->target_size_location < 0 || impl_->video_rect_location < 0 ||
|
||||
impl_->corner_radius_location < 0 ||
|
||||
impl_->video_enabled_location < 0) {
|
||||
LOG_ERROR("OpenGL NV12 program is missing required shader bindings");
|
||||
impl_->gl.delete_program(impl_->program);
|
||||
impl_->ResetGlHandles();
|
||||
@@ -429,7 +522,7 @@ void main() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowsOpenGlVideoRenderer::Teardown() {
|
||||
void OpenGlVideoRenderer::Teardown() {
|
||||
impl_->ready.store(false, std::memory_order_release);
|
||||
if (impl_->y_texture != 0)
|
||||
glDeleteTextures(1, &impl_->y_texture);
|
||||
@@ -444,11 +537,13 @@ void WindowsOpenGlVideoRenderer::Teardown() {
|
||||
impl_->ResetGlHandles();
|
||||
}
|
||||
|
||||
bool WindowsOpenGlVideoRenderer::IsReady() const {
|
||||
bool OpenGlVideoRenderer::IsReady() const {
|
||||
return impl_->ready.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool WindowsOpenGlVideoRenderer::SetSelectedStream(std::string remote_id) {
|
||||
bool OpenGlVideoRenderer::IsActive() const { return IsReady(); }
|
||||
|
||||
bool OpenGlVideoRenderer::SetSelectedStream(std::string remote_id) {
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
if (impl_->frames->selected_stream == remote_id) {
|
||||
return false;
|
||||
@@ -463,7 +558,7 @@ bool WindowsOpenGlVideoRenderer::SetSelectedStream(std::string remote_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowsOpenGlVideoRenderer::DiscardStream(std::string_view remote_id) {
|
||||
void OpenGlVideoRenderer::DiscardStream(std::string_view remote_id) {
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
for (auto &slot : impl_->frames->slots) {
|
||||
if (slot.remote_id == remote_id && slot.use != SlotUse::uploading) {
|
||||
@@ -477,22 +572,22 @@ void WindowsOpenGlVideoRenderer::DiscardStream(std::string_view remote_id) {
|
||||
}
|
||||
}
|
||||
|
||||
WindowsOpenGlVideoRenderer::SubmitResult
|
||||
WindowsOpenGlVideoRenderer::SubmitNv12(std::string_view remote_id,
|
||||
OpenGlVideoRenderer::SubmitResult
|
||||
OpenGlVideoRenderer::SubmitNv12(std::string_view remote_id,
|
||||
const uint8_t *data, size_t size,
|
||||
int width, int height) {
|
||||
return SubmitNv12Internal(remote_id, data, size, width, height, true);
|
||||
}
|
||||
|
||||
WindowsOpenGlVideoRenderer::SubmitResult
|
||||
WindowsOpenGlVideoRenderer::SubmitCachedNv12(std::string_view remote_id,
|
||||
OpenGlVideoRenderer::SubmitResult
|
||||
OpenGlVideoRenderer::SubmitCachedNv12(std::string_view remote_id,
|
||||
const uint8_t *data, size_t size,
|
||||
int width, int height) {
|
||||
return SubmitNv12Internal(remote_id, data, size, width, height, false);
|
||||
}
|
||||
|
||||
WindowsOpenGlVideoRenderer::SubmitResult
|
||||
WindowsOpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id,
|
||||
OpenGlVideoRenderer::SubmitResult
|
||||
OpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id,
|
||||
const uint8_t *data, size_t size,
|
||||
int width, int height,
|
||||
bool replace_pending) {
|
||||
@@ -562,10 +657,11 @@ WindowsOpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id,
|
||||
return SubmitResult::submitted;
|
||||
}
|
||||
|
||||
WindowsOpenGlVideoRenderer::RenderOutcome
|
||||
WindowsOpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
int target_width, int target_height,
|
||||
int top_inset_pixels) {
|
||||
OpenGlVideoRenderer::RenderOutcome
|
||||
OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
int target_width, int target_height,
|
||||
int top_inset_pixels,
|
||||
int corner_radius_pixels) {
|
||||
if (!IsReady() || target_width <= 0 || target_height <= 0) {
|
||||
return {RenderResult::failed};
|
||||
}
|
||||
@@ -643,7 +739,10 @@ WindowsOpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glViewport(0, 0, target_width, target_height);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
// The Slint items above this underlay are transparent around the custom
|
||||
// window corners. Keep those pixels transparent so the compositor can show
|
||||
// the desktop instead of an opaque black framebuffer clear.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
bool upload_succeeded = true;
|
||||
@@ -696,60 +795,77 @@ WindowsOpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
}
|
||||
}
|
||||
|
||||
RenderOutcome outcome{RenderResult::empty};
|
||||
if (upload_succeeded && impl_->uploaded_stream == remote_id &&
|
||||
impl_->texture_width > 0 && impl_->texture_height > 0) {
|
||||
const int content_y = std::clamp(top_inset_pixels, 0, target_height);
|
||||
const int content_height = target_height - content_y;
|
||||
const bool has_video =
|
||||
upload_succeeded && impl_->uploaded_stream == remote_id &&
|
||||
impl_->texture_width > 0 && impl_->texture_height > 0 &&
|
||||
content_height > 0;
|
||||
int video_x = 0;
|
||||
int video_y = content_y;
|
||||
int video_width = target_width;
|
||||
int video_height = content_height;
|
||||
if (has_video) {
|
||||
source_width = impl_->texture_width;
|
||||
source_height = impl_->texture_height;
|
||||
sequence = impl_->uploaded_sequence;
|
||||
const int video_height = std::max(
|
||||
0, target_height - std::clamp(top_inset_pixels, 0, target_height));
|
||||
if (video_height > 0) {
|
||||
const double source_aspect =
|
||||
static_cast<double>(source_width) / source_height;
|
||||
const double target_aspect =
|
||||
static_cast<double>(target_width) / video_height;
|
||||
int viewport_width = target_width;
|
||||
int viewport_height = video_height;
|
||||
int viewport_x = 0;
|
||||
int viewport_y = 0;
|
||||
if (source_aspect > target_aspect) {
|
||||
viewport_height =
|
||||
std::max(1, static_cast<int>(target_width / source_aspect + 0.5));
|
||||
viewport_y = (video_height - viewport_height) / 2;
|
||||
} else {
|
||||
viewport_width =
|
||||
std::max(1, static_cast<int>(video_height * source_aspect + 0.5));
|
||||
viewport_x = (target_width - viewport_width) / 2;
|
||||
}
|
||||
|
||||
glViewport(viewport_x, viewport_y, viewport_width, viewport_height);
|
||||
impl_->gl.use_program(impl_->program);
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->y_texture);
|
||||
impl_->gl.uniform_1i(impl_->y_texture_location, 0);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->uv_texture);
|
||||
impl_->gl.uniform_1i(impl_->uv_texture_location, 1);
|
||||
impl_->gl.bind_buffer(GL_ARRAY_BUFFER, impl_->vertex_buffer);
|
||||
impl_->gl.vertex_attrib_pointer(
|
||||
static_cast<GLuint>(impl_->position_location), 2, GL_FLOAT, GL_FALSE,
|
||||
4 * static_cast<GLsizei>(sizeof(GLfloat)), nullptr);
|
||||
impl_->gl.vertex_attrib_pointer(
|
||||
static_cast<GLuint>(impl_->texcoord_location), 2, GL_FLOAT, GL_FALSE,
|
||||
4 * static_cast<GLsizei>(sizeof(GLfloat)),
|
||||
reinterpret_cast<const void *>(2 * sizeof(GLfloat)));
|
||||
impl_->gl.enable_vertex_attrib_array(
|
||||
static_cast<GLuint>(impl_->position_location));
|
||||
impl_->gl.enable_vertex_attrib_array(
|
||||
static_cast<GLuint>(impl_->texcoord_location));
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
outcome = {glGetError() == GL_NO_ERROR ? RenderResult::rendered
|
||||
: RenderResult::failed,
|
||||
source_width, source_height, sequence};
|
||||
const double source_aspect =
|
||||
static_cast<double>(source_width) / source_height;
|
||||
const double target_aspect =
|
||||
static_cast<double>(target_width) / content_height;
|
||||
if (source_aspect > target_aspect) {
|
||||
video_height =
|
||||
std::max(1, static_cast<int>(target_width / source_aspect + 0.5));
|
||||
video_y = content_y + (content_height - video_height) / 2;
|
||||
} else {
|
||||
video_width =
|
||||
std::max(1, static_cast<int>(content_height * source_aspect + 0.5));
|
||||
video_x = (target_width - video_width) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
impl_->gl.use_program(impl_->program);
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->y_texture);
|
||||
impl_->gl.uniform_1i(impl_->y_texture_location, 0);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->uv_texture);
|
||||
impl_->gl.uniform_1i(impl_->uv_texture_location, 1);
|
||||
impl_->gl.uniform_2f(impl_->target_size_location,
|
||||
static_cast<GLfloat>(target_width),
|
||||
static_cast<GLfloat>(target_height));
|
||||
impl_->gl.uniform_4f(
|
||||
impl_->video_rect_location, static_cast<GLfloat>(video_x),
|
||||
static_cast<GLfloat>(video_y), static_cast<GLfloat>(video_width),
|
||||
static_cast<GLfloat>(video_height));
|
||||
impl_->gl.uniform_1f(
|
||||
impl_->corner_radius_location,
|
||||
static_cast<GLfloat>(std::clamp(
|
||||
corner_radius_pixels, 0, std::min(target_width, target_height) / 2)));
|
||||
impl_->gl.uniform_1f(impl_->video_enabled_location, has_video ? 1.0f : 0.0f);
|
||||
impl_->gl.bind_buffer(GL_ARRAY_BUFFER, impl_->vertex_buffer);
|
||||
impl_->gl.vertex_attrib_pointer(
|
||||
static_cast<GLuint>(impl_->position_location), 2, GL_FLOAT, GL_FALSE,
|
||||
4 * static_cast<GLsizei>(sizeof(GLfloat)), nullptr);
|
||||
impl_->gl.vertex_attrib_pointer(
|
||||
static_cast<GLuint>(impl_->texcoord_location), 2, GL_FLOAT, GL_FALSE,
|
||||
4 * static_cast<GLsizei>(sizeof(GLfloat)),
|
||||
reinterpret_cast<const void *>(2 * sizeof(GLfloat)));
|
||||
impl_->gl.enable_vertex_attrib_array(
|
||||
static_cast<GLuint>(impl_->position_location));
|
||||
impl_->gl.enable_vertex_attrib_array(
|
||||
static_cast<GLuint>(impl_->texcoord_location));
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
const GLenum draw_error = glGetError();
|
||||
RenderOutcome outcome{
|
||||
!upload_succeeded || draw_error != GL_NO_ERROR
|
||||
? RenderResult::failed
|
||||
: has_video ? RenderResult::rendered : RenderResult::empty,
|
||||
has_video ? source_width : 0, has_video ? source_height : 0,
|
||||
has_video ? sequence : 0};
|
||||
|
||||
RestoreVertexAttrib(impl_->gl, static_cast<GLuint>(impl_->position_location),
|
||||
position_state);
|
||||
RestoreVertexAttrib(impl_->gl, static_cast<GLuint>(impl_->texcoord_location),
|
||||
@@ -786,7 +902,7 @@ WindowsOpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
return outcome;
|
||||
}
|
||||
|
||||
bool WindowsOpenGlVideoRenderer::CopyLatestNv12(
|
||||
bool OpenGlVideoRenderer::CopyLatestNv12(
|
||||
std::string_view remote_id, std::vector<unsigned char> *output, int *width,
|
||||
int *height) const {
|
||||
if (!output || !width || !height) {
|
||||
@@ -0,0 +1,70 @@
|
||||
#ifndef CROSSDESK_GUI_PLATFORM_OPENGL_VIDEO_RENDERER_H_
|
||||
#define CROSSDESK_GUI_PLATFORM_OPENGL_VIDEO_RENDERER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "platform/video_renderer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// NV12 underlay for Slint's FemtoVG OpenGL renderer on Windows and Linux.
|
||||
// MiniRTC submits tightly packed CPU NV12 frames from its decode callback
|
||||
// thread. The Slint UI thread uploads the newest frame as Y and UV textures and
|
||||
// performs color conversion and scaling in a fragment shader before Slint
|
||||
// draws its overlay.
|
||||
class OpenGlVideoRenderer final : public VideoRenderer {
|
||||
public:
|
||||
OpenGlVideoRenderer();
|
||||
~OpenGlVideoRenderer() override;
|
||||
|
||||
OpenGlVideoRenderer(const OpenGlVideoRenderer &) = delete;
|
||||
OpenGlVideoRenderer &operator=(const OpenGlVideoRenderer &) = delete;
|
||||
|
||||
// These methods must run while Slint's OpenGL context is current, from the
|
||||
// RenderingSetup and RenderingTeardown notifier states respectively.
|
||||
bool Setup();
|
||||
void Teardown();
|
||||
bool IsReady() const override;
|
||||
bool IsActive() const override;
|
||||
|
||||
// Only the selected stream is queued for upload. Frames for other tabs are
|
||||
// retained by GuiRuntime at a throttled rate for thumbnail/tab restoration.
|
||||
bool SetSelectedStream(std::string remote_id) override;
|
||||
void DiscardStream(std::string_view remote_id) override;
|
||||
|
||||
// Thread-safe; called from MiniRTC's decode callback thread.
|
||||
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t *data,
|
||||
size_t size, int width, int height) override;
|
||||
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
|
||||
size_t size, int width, int height) override;
|
||||
|
||||
// Draws below Slint while its OpenGL context is current. All dimensions are
|
||||
// physical pixels in the window client area. corner_radius_pixels clips the
|
||||
// underlay to Slint's custom rounded window surface.
|
||||
RenderOutcome RenderLatest(std::string_view remote_id, int target_width,
|
||||
int target_height, int top_inset_pixels,
|
||||
int corner_radius_pixels);
|
||||
|
||||
// Used during disconnect cleanup so thumbnail generation does not require a
|
||||
// second per-frame CPU copy during normal playback.
|
||||
bool CopyLatestNv12(std::string_view remote_id,
|
||||
std::vector<unsigned char> *output, int *width,
|
||||
int *height) const override;
|
||||
|
||||
private:
|
||||
SubmitResult SubmitNv12Internal(std::string_view remote_id,
|
||||
const uint8_t *data, size_t size, int width,
|
||||
int height, bool replace_pending);
|
||||
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_PLATFORM_OPENGL_VIDEO_RENDERER_H_
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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_) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#ifndef CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_
|
||||
#define CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Platform-independent contract for the native stream video renderer.
|
||||
//
|
||||
// Frame submission and snapshot methods may be called from MiniRTC callback
|
||||
// threads. Surface setup, rendering and teardown remain backend-specific and
|
||||
// are driven by the Slint presentation module on the UI thread.
|
||||
class VideoRenderer {
|
||||
public:
|
||||
enum class SubmitResult {
|
||||
submitted,
|
||||
not_selected,
|
||||
dropped,
|
||||
failed,
|
||||
};
|
||||
|
||||
enum class RenderResult {
|
||||
rendered,
|
||||
idle,
|
||||
empty,
|
||||
failed,
|
||||
};
|
||||
|
||||
struct RenderOutcome {
|
||||
RenderResult result = RenderResult::failed;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
virtual ~VideoRenderer() = default;
|
||||
|
||||
virtual bool IsReady() const = 0;
|
||||
|
||||
// True only while the renderer can accept and present native frames. Metal
|
||||
// additionally requires attachment to an AppKit view; OpenGL requires an
|
||||
// active Slint rendering context.
|
||||
virtual bool IsActive() const = 0;
|
||||
|
||||
virtual bool SetSelectedStream(std::string remote_id) = 0;
|
||||
virtual void DiscardStream(std::string_view remote_id) = 0;
|
||||
|
||||
virtual SubmitResult SubmitNv12(std::string_view remote_id,
|
||||
const uint8_t* data, size_t size, int width,
|
||||
int height) = 0;
|
||||
virtual SubmitResult SubmitCachedNv12(std::string_view remote_id,
|
||||
const uint8_t* data, size_t size,
|
||||
int width, int height) = 0;
|
||||
|
||||
virtual bool CopyLatestNv12(std::string_view remote_id,
|
||||
std::vector<unsigned char>* output, int* width,
|
||||
int* height) const = 0;
|
||||
};
|
||||
|
||||
// Exactly one factory implementation is linked for the target platform.
|
||||
std::unique_ptr<VideoRenderer> CreateVideoRenderer();
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_
|
||||
@@ -0,0 +1,13 @@
|
||||
#include "platform/video_renderer.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "platform/metal_video_renderer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
std::unique_ptr<VideoRenderer> CreateVideoRenderer() {
|
||||
return std::make_unique<MacMetalVideoRenderer>();
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,12 @@
|
||||
#include <memory>
|
||||
|
||||
#include "platform/opengl_video_renderer.h"
|
||||
#include "platform/video_renderer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
std::unique_ptr<VideoRenderer> CreateVideoRenderer() {
|
||||
return std::make_unique<OpenGlVideoRenderer>();
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,86 +0,0 @@
|
||||
#ifndef CROSSDESK_GUI_PLATFORM_WINDOWS_OPENGL_VIDEO_RENDERER_H_
|
||||
#define CROSSDESK_GUI_PLATFORM_WINDOWS_OPENGL_VIDEO_RENDERER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Windows NV12 underlay for Slint's FemtoVG OpenGL renderer. MiniRTC submits
|
||||
// tightly packed CPU NV12 frames from its decode callback thread. The Slint UI
|
||||
// thread uploads the newest frame as Y and UV textures and performs color
|
||||
// conversion and scaling in a fragment shader before Slint draws its overlay.
|
||||
class WindowsOpenGlVideoRenderer {
|
||||
public:
|
||||
enum class SubmitResult {
|
||||
submitted,
|
||||
not_selected,
|
||||
dropped,
|
||||
failed,
|
||||
};
|
||||
|
||||
enum class RenderResult {
|
||||
rendered,
|
||||
idle,
|
||||
empty,
|
||||
failed,
|
||||
};
|
||||
|
||||
struct RenderOutcome {
|
||||
RenderResult result = RenderResult::failed;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
WindowsOpenGlVideoRenderer();
|
||||
~WindowsOpenGlVideoRenderer();
|
||||
|
||||
WindowsOpenGlVideoRenderer(const WindowsOpenGlVideoRenderer &) = delete;
|
||||
WindowsOpenGlVideoRenderer &
|
||||
operator=(const WindowsOpenGlVideoRenderer &) = delete;
|
||||
|
||||
// These methods must run while Slint's OpenGL context is current, from the
|
||||
// RenderingSetup and RenderingTeardown notifier states respectively.
|
||||
bool Setup();
|
||||
void Teardown();
|
||||
bool IsReady() const;
|
||||
|
||||
// Only the selected stream is queued for upload. Frames for other tabs are
|
||||
// retained by GuiRuntime at a throttled rate for thumbnail/tab restoration.
|
||||
bool SetSelectedStream(std::string remote_id);
|
||||
void DiscardStream(std::string_view remote_id);
|
||||
|
||||
// Thread-safe; called from MiniRTC's decode callback thread.
|
||||
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t *data,
|
||||
size_t size, int width, int height);
|
||||
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
|
||||
size_t size, int width, int height);
|
||||
|
||||
// Draws below Slint while its OpenGL context is current. target dimensions
|
||||
// and top_inset_pixels are physical pixels in the window client area.
|
||||
RenderOutcome RenderLatest(std::string_view remote_id, int target_width,
|
||||
int target_height, int top_inset_pixels);
|
||||
|
||||
// Used during disconnect cleanup so thumbnail generation does not require a
|
||||
// second per-frame CPU copy during normal playback.
|
||||
bool CopyLatestNv12(std::string_view remote_id,
|
||||
std::vector<unsigned char> *output, int *width,
|
||||
int *height) const;
|
||||
|
||||
private:
|
||||
SubmitResult SubmitNv12Internal(std::string_view remote_id,
|
||||
const uint8_t *data, size_t size, int width,
|
||||
int height, bool replace_pending);
|
||||
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_PLATFORM_WINDOWS_OPENGL_VIDEO_RENDERER_H_
|
||||
@@ -0,0 +1,502 @@
|
||||
#include "rendering/slint_video_presenter.h"
|
||||
|
||||
#include <libyuv.h>
|
||||
#include <slint.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include "nv12_scaler.h"
|
||||
#include "platform/video_renderer.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#else
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <GL/gl.h>
|
||||
|
||||
#include "platform/opengl_video_renderer.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
constexpr GLint kGlClampToEdge = 0x812F;
|
||||
constexpr float kStreamWindowCornerRadius = 12.0f;
|
||||
#else
|
||||
constexpr int kMetalAttachmentAttemptLimit = 30;
|
||||
#endif
|
||||
|
||||
struct VideoRenderSize {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
bool operator==(const VideoRenderSize&) const = default;
|
||||
};
|
||||
|
||||
VideoRenderSize FitVideoToRenderArea(int source_width, int source_height,
|
||||
int area_width, int area_height) {
|
||||
if (source_width <= 0 || source_height <= 0 || area_width <= 0 ||
|
||||
area_height <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const double scale =
|
||||
std::min({1.0, static_cast<double>(area_width) / source_width,
|
||||
static_cast<double>(area_height) / source_height});
|
||||
int width = std::max(2, static_cast<int>(std::floor(source_width * scale)));
|
||||
int height = std::max(2, static_cast<int>(std::floor(source_height * scale)));
|
||||
|
||||
width &= ~1;
|
||||
height &= ~1;
|
||||
return {std::min(width, source_width & ~1),
|
||||
std::min(height, source_height & ~1)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct SlintVideoPresenter::Impl {
|
||||
explicit Impl(VideoRenderer& renderer) : renderer(renderer) {}
|
||||
|
||||
VideoRenderer& renderer;
|
||||
slint::ComponentHandle<ui::StreamWindow>* stream = nullptr;
|
||||
StateProvider state_provider;
|
||||
DirtyCallback dirty_callback;
|
||||
std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
|
||||
std::unordered_map<std::string, uint64_t> seeded_frame_sequence;
|
||||
std::vector<uint8_t> scaled_video_frame;
|
||||
std::vector<uint8_t> scaled_video_frame_scratch;
|
||||
|
||||
#if defined(__APPLE__)
|
||||
int attachment_attempts = 0;
|
||||
bool skip_attachment_once = false;
|
||||
#else
|
||||
std::mutex gl_mutex;
|
||||
std::vector<uint8_t> gl_conversion_frame;
|
||||
std::vector<uint8_t> gl_pending_frame;
|
||||
uint32_t gl_texture = 0;
|
||||
VideoRenderSize gl_texture_size;
|
||||
VideoRenderSize gl_image_size;
|
||||
VideoRenderSize gl_pending_size;
|
||||
bool gl_pending_dirty = false;
|
||||
#endif
|
||||
|
||||
SurfaceState CurrentState() const {
|
||||
return state_provider ? state_provider() : SurfaceState{};
|
||||
}
|
||||
|
||||
void MarkDirty() const {
|
||||
if (dirty_callback) {
|
||||
dirty_callback();
|
||||
}
|
||||
}
|
||||
|
||||
bool MarkPresented(const std::string& remote_id, uint64_t sequence) {
|
||||
if (remote_id.empty() || sequence == 0 ||
|
||||
displayed_frame_sequence[remote_id] == sequence) {
|
||||
return false;
|
||||
}
|
||||
displayed_frame_sequence[remote_id] = sequence;
|
||||
return true;
|
||||
}
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
void ConfigureOpenGlNotifier() {
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto error = (*stream)->window().set_rendering_notifier(
|
||||
[this](slint::RenderingState state, slint::GraphicsAPI graphics_api) {
|
||||
if (graphics_api != slint::GraphicsAPI::NativeOpenGL) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(gl_mutex);
|
||||
auto* opengl_renderer =
|
||||
dynamic_cast<OpenGlVideoRenderer*>(&renderer);
|
||||
if (state == slint::RenderingState::RenderingSetup) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
if (texture == 0) {
|
||||
return;
|
||||
}
|
||||
GLint previous_texture = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, kGlClampToEdge);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, kGlClampToEdge);
|
||||
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
|
||||
gl_texture = texture;
|
||||
if (opengl_renderer && opengl_renderer->Setup()) {
|
||||
const std::string selected_stream =
|
||||
CurrentState().selected_stream;
|
||||
opengl_renderer->SetSelectedStream(selected_stream);
|
||||
seeded_frame_sequence.erase(selected_stream);
|
||||
MarkDirty();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == slint::RenderingState::BeforeRendering && stream &&
|
||||
opengl_renderer && opengl_renderer->IsReady() &&
|
||||
(*stream)->get_native_video_enabled()) {
|
||||
const SurfaceState surface = CurrentState();
|
||||
const auto window_size = (*stream)->window().size();
|
||||
const float scale_factor =
|
||||
std::max(1.0f, (*stream)->window().scale_factor());
|
||||
const int top_inset =
|
||||
!surface.fullscreen && surface.tab_count > 1
|
||||
? static_cast<int>(std::lround(30.0f * scale_factor))
|
||||
: 0;
|
||||
const bool rounded_window = (*stream)->get_custom_titlebar() &&
|
||||
!surface.fullscreen &&
|
||||
!(*stream)->window().is_maximized();
|
||||
const int corner_radius =
|
||||
rounded_window ? static_cast<int>(std::lround(
|
||||
kStreamWindowCornerRadius * scale_factor))
|
||||
: 0;
|
||||
opengl_renderer->RenderLatest(
|
||||
surface.selected_stream, static_cast<int>(window_size.width),
|
||||
static_cast<int>(window_size.height), top_inset, corner_radius);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == slint::RenderingState::BeforeRendering &&
|
||||
gl_texture != 0 && gl_pending_dirty &&
|
||||
!gl_pending_frame.empty()) {
|
||||
GLint previous_texture = 0;
|
||||
GLint previous_unpack_alignment = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
||||
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
|
||||
glBindTexture(GL_TEXTURE_2D, gl_texture);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
if (gl_texture_size != gl_pending_size) {
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, gl_pending_size.width,
|
||||
gl_pending_size.height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
gl_pending_frame.data());
|
||||
gl_texture_size = gl_pending_size;
|
||||
} else {
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, gl_pending_size.width,
|
||||
gl_pending_size.height, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
gl_pending_frame.data());
|
||||
}
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
|
||||
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
|
||||
gl_pending_dirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == slint::RenderingState::RenderingTeardown) {
|
||||
if (opengl_renderer) {
|
||||
opengl_renderer->Teardown();
|
||||
}
|
||||
if (gl_texture != 0) {
|
||||
const GLuint texture = gl_texture;
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
gl_texture = 0;
|
||||
gl_texture_size = {};
|
||||
gl_image_size = {};
|
||||
gl_pending_size = {};
|
||||
gl_pending_dirty = false;
|
||||
gl_pending_frame.clear();
|
||||
}
|
||||
});
|
||||
|
||||
if (error.has_value()) {
|
||||
LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers");
|
||||
} else {
|
||||
renderer.SetSelectedStream(CurrentState().selected_stream);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
SlintVideoPresenter::SlintVideoPresenter(VideoRenderer& renderer)
|
||||
: impl_(std::make_unique<Impl>(renderer)) {}
|
||||
|
||||
SlintVideoPresenter::~SlintVideoPresenter() = default;
|
||||
|
||||
void SlintVideoPresenter::PrepareWindow(
|
||||
slint::ComponentHandle<ui::StreamWindow>& stream,
|
||||
StateProvider state_provider, DirtyCallback dirty_callback) {
|
||||
impl_->stream = &stream;
|
||||
impl_->state_provider = std::move(state_provider);
|
||||
impl_->dirty_callback = std::move(dirty_callback);
|
||||
#if defined(__APPLE__)
|
||||
stream->set_native_video_enabled(false);
|
||||
impl_->attachment_attempts = kMetalAttachmentAttemptLimit;
|
||||
impl_->skip_attachment_once = true;
|
||||
#else
|
||||
impl_->ConfigureOpenGlNotifier();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SlintVideoPresenter::EnsureAttached() {
|
||||
#if defined(__APPLE__)
|
||||
if (!impl_->stream || impl_->attachment_attempts <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (impl_->skip_attachment_once) {
|
||||
impl_->skip_attachment_once = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* metal_renderer =
|
||||
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
|
||||
if (!metal_renderer) {
|
||||
impl_->attachment_attempts = 0;
|
||||
(*impl_->stream)->set_native_video_enabled(false);
|
||||
return false;
|
||||
}
|
||||
if (!metal_renderer->IsReady()) {
|
||||
impl_->attachment_attempts = 0;
|
||||
(*impl_->stream)->set_native_video_enabled(false);
|
||||
return false;
|
||||
}
|
||||
if (metal_renderer->IsAttached()) {
|
||||
impl_->attachment_attempts = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (void* view = (*impl_->stream)->window().appkit_view()) {
|
||||
(*impl_->stream)->set_native_video_enabled(true);
|
||||
if (metal_renderer->Attach(view)) {
|
||||
impl_->attachment_attempts = 0;
|
||||
const SurfaceState surface = impl_->CurrentState();
|
||||
impl_->displayed_frame_sequence.erase(surface.selected_stream);
|
||||
impl_->seeded_frame_sequence.erase(surface.selected_stream);
|
||||
if (metal_renderer->SetSelectedStream(surface.selected_stream)) {
|
||||
impl_->MarkDirty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
(*impl_->stream)->set_native_video_enabled(false);
|
||||
}
|
||||
|
||||
if (--impl_->attachment_attempts == 0) {
|
||||
(*impl_->stream)->set_native_video_enabled(false);
|
||||
LOG_WARN("Unable to attach the Metal video surface; using CPU rendering");
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SlintVideoPresenter::NeedsRedraw() const {
|
||||
#if defined(__APPLE__)
|
||||
const auto* metal_renderer =
|
||||
dynamic_cast<const MacMetalVideoRenderer*>(&impl_->renderer);
|
||||
return metal_renderer && metal_renderer->IsActive() &&
|
||||
metal_renderer->NeedsSurfaceRedraw();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SlintVideoPresenter::SelectStream(std::string remote_id) {
|
||||
const bool changed = impl_->renderer.SetSelectedStream(remote_id);
|
||||
if (changed && !remote_id.empty()) {
|
||||
impl_->seeded_frame_sequence.erase(remote_id);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
|
||||
const Frame& frame) {
|
||||
PresentResult result;
|
||||
if (!impl_->stream || frame.remote_id.empty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (impl_->renderer.IsActive() && frame.nv12 && frame.width > 0 &&
|
||||
frame.height > 0 && frame.sequence > 0 &&
|
||||
impl_->seeded_frame_sequence[frame.remote_id] != frame.sequence) {
|
||||
const auto submit_result = impl_->renderer.SubmitCachedNv12(
|
||||
frame.remote_id, frame.nv12->data(), frame.nv12->size(), frame.width,
|
||||
frame.height);
|
||||
if (submit_result == VideoRenderer::SubmitResult::submitted ||
|
||||
submit_result == VideoRenderer::SubmitResult::dropped) {
|
||||
impl_->seeded_frame_sequence[frame.remote_id] = frame.sequence;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
auto* metal_renderer =
|
||||
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
|
||||
if (metal_renderer && metal_renderer->IsActive()) {
|
||||
const SurfaceState surface = impl_->CurrentState();
|
||||
const double top_inset =
|
||||
!surface.fullscreen && surface.tab_count > 1 ? 30.0 : 0.0;
|
||||
const auto outcome = metal_renderer->RenderLatest(
|
||||
frame.remote_id, top_inset, !surface.fullscreen);
|
||||
if (outcome.result == VideoRenderer::RenderResult::rendered) {
|
||||
(*impl_->stream)->set_has_frame(true);
|
||||
(*impl_->stream)->set_receiving_text("");
|
||||
result.width = outcome.width;
|
||||
result.height = outcome.height;
|
||||
result.new_frame =
|
||||
impl_->MarkPresented(frame.remote_id, outcome.sequence);
|
||||
} else if (outcome.result == VideoRenderer::RenderResult::empty) {
|
||||
(*impl_->stream)->set_has_frame(false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#else
|
||||
auto* opengl_renderer =
|
||||
dynamic_cast<OpenGlVideoRenderer*>(&impl_->renderer);
|
||||
if (opengl_renderer && opengl_renderer->IsReady()) {
|
||||
opengl_renderer->SetSelectedStream(frame.remote_id);
|
||||
if (!(*impl_->stream)->get_native_video_enabled()) {
|
||||
(*impl_->stream)->set_native_video_enabled(true);
|
||||
}
|
||||
|
||||
const bool has_frame =
|
||||
frame.width > 0 && frame.height > 0 && frame.sequence > 0;
|
||||
(*impl_->stream)->set_has_frame(has_frame);
|
||||
if (has_frame) {
|
||||
(*impl_->stream)->set_receiving_text("");
|
||||
result.width = frame.width;
|
||||
result.height = frame.height;
|
||||
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||
}
|
||||
(*impl_->stream)->window().request_redraw();
|
||||
return result;
|
||||
}
|
||||
if ((*impl_->stream)->get_native_video_enabled()) {
|
||||
(*impl_->stream)->set_native_video_enabled(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
const size_t nv12_size =
|
||||
static_cast<size_t>(frame.width) * frame.height * 3 / 2;
|
||||
if (!frame.nv12 || frame.width <= 0 || frame.height <= 0 ||
|
||||
frame.nv12->size() < nv12_size) {
|
||||
(*impl_->stream)->set_has_frame(false);
|
||||
return result;
|
||||
}
|
||||
if (impl_->displayed_frame_sequence[frame.remote_id] == frame.sequence) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto window_size = (*impl_->stream)->window().size();
|
||||
const VideoRenderSize render_size = FitVideoToRenderArea(
|
||||
frame.width, frame.height, static_cast<int>(window_size.width),
|
||||
static_cast<int>(window_size.height));
|
||||
const uint8_t* y_plane = frame.nv12->data();
|
||||
const uint8_t* uv_plane =
|
||||
frame.nv12->data() + static_cast<size_t>(frame.width) * frame.height;
|
||||
int output_width = frame.width;
|
||||
int output_height = frame.height;
|
||||
|
||||
if (render_size.width > 0 && render_size.height > 0 &&
|
||||
(render_size.width != frame.width ||
|
||||
render_size.height != frame.height)) {
|
||||
const size_t scaled_nv12_size =
|
||||
static_cast<size_t>(render_size.width) * render_size.height * 3 / 2;
|
||||
impl_->scaled_video_frame.resize(scaled_nv12_size);
|
||||
uint8_t* scaled_y = impl_->scaled_video_frame.data();
|
||||
uint8_t* scaled_uv =
|
||||
scaled_y + static_cast<size_t>(render_size.width) * render_size.height;
|
||||
if (ScaleNv12ViaI420(y_plane, frame.width, uv_plane, frame.width,
|
||||
frame.width, frame.height, scaled_y, render_size.width,
|
||||
scaled_uv, render_size.width, render_size.width,
|
||||
render_size.height, libyuv::kFilterBox,
|
||||
&impl_->scaled_video_frame_scratch) == 0) {
|
||||
y_plane = scaled_y;
|
||||
uv_plane = scaled_uv;
|
||||
output_width = render_size.width;
|
||||
output_height = render_size.height;
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
uint32_t texture_id = 0;
|
||||
{
|
||||
std::lock_guard lock(impl_->gl_mutex);
|
||||
texture_id = impl_->gl_texture;
|
||||
}
|
||||
if (texture_id != 0) {
|
||||
impl_->gl_conversion_frame.resize(static_cast<size_t>(output_width) *
|
||||
output_height * 4);
|
||||
if (libyuv::NV12ToABGR(y_plane, output_width, uv_plane, output_width,
|
||||
impl_->gl_conversion_frame.data(), output_width * 4,
|
||||
output_width, output_height) != 0) {
|
||||
return result;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(impl_->gl_mutex);
|
||||
if (impl_->gl_texture == 0) {
|
||||
return result;
|
||||
}
|
||||
texture_id = impl_->gl_texture;
|
||||
impl_->gl_pending_frame.swap(impl_->gl_conversion_frame);
|
||||
impl_->gl_pending_size = {output_width, output_height};
|
||||
impl_->gl_pending_dirty = true;
|
||||
}
|
||||
const VideoRenderSize output_size{output_width, output_height};
|
||||
if (impl_->gl_image_size != output_size) {
|
||||
(*impl_->stream)
|
||||
->set_frame(slint::Image::create_from_borrowed_gl_2d_rgba_texture(
|
||||
texture_id,
|
||||
slint::Size<uint32_t>{static_cast<uint32_t>(output_width),
|
||||
static_cast<uint32_t>(output_height)}));
|
||||
impl_->gl_image_size = output_size;
|
||||
}
|
||||
(*impl_->stream)->set_has_frame(true);
|
||||
(*impl_->stream)->set_receiving_text("");
|
||||
(*impl_->stream)->window().request_redraw();
|
||||
result.width = frame.width;
|
||||
result.height = frame.height;
|
||||
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(output_width,
|
||||
output_height);
|
||||
if (libyuv::NV12ToRAW(y_plane, output_width, uv_plane, output_width,
|
||||
reinterpret_cast<uint8_t*>(pixels.begin()),
|
||||
output_width * 3, output_width, output_height) != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
(*impl_->stream)->set_frame(slint::Image(std::move(pixels)));
|
||||
(*impl_->stream)->set_has_frame(true);
|
||||
(*impl_->stream)->set_receiving_text("");
|
||||
result.width = frame.width;
|
||||
result.height = frame.height;
|
||||
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||
return result;
|
||||
}
|
||||
|
||||
void SlintVideoPresenter::ForgetStream(const std::string& remote_id) {
|
||||
impl_->displayed_frame_sequence.erase(remote_id);
|
||||
impl_->seeded_frame_sequence.erase(remote_id);
|
||||
}
|
||||
|
||||
void SlintVideoPresenter::Detach() {
|
||||
impl_->renderer.SetSelectedStream({});
|
||||
#if defined(__APPLE__)
|
||||
if (auto* metal_renderer =
|
||||
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer)) {
|
||||
metal_renderer->Detach();
|
||||
}
|
||||
impl_->attachment_attempts = 0;
|
||||
impl_->skip_attachment_once = false;
|
||||
#endif
|
||||
impl_->stream = nullptr;
|
||||
impl_->state_provider = {};
|
||||
impl_->dirty_callback = {};
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,75 @@
|
||||
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "crossdesk_ui.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class VideoRenderer;
|
||||
|
||||
// Owns the bridge between decoded NV12 frames, the selected native graphics
|
||||
// backend and Slint's StreamWindow. Platform macros and graphics-API lifecycle
|
||||
// details are intentionally contained in the implementation file.
|
||||
class SlintVideoPresenter {
|
||||
public:
|
||||
struct SurfaceState {
|
||||
std::string selected_stream;
|
||||
bool fullscreen = false;
|
||||
size_t tab_count = 0;
|
||||
};
|
||||
|
||||
struct Frame {
|
||||
std::string remote_id;
|
||||
std::shared_ptr<std::vector<unsigned char>> nv12;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
struct PresentResult {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool new_frame = false;
|
||||
};
|
||||
|
||||
using StateProvider = std::function<SurfaceState()>;
|
||||
using DirtyCallback = std::function<void()>;
|
||||
|
||||
explicit SlintVideoPresenter(VideoRenderer& renderer);
|
||||
~SlintVideoPresenter();
|
||||
|
||||
SlintVideoPresenter(const SlintVideoPresenter&) = delete;
|
||||
SlintVideoPresenter& operator=(const SlintVideoPresenter&) = delete;
|
||||
|
||||
// Called before the window is shown. OpenGL installs Slint's rendering
|
||||
// notifier here; macOS deliberately defers native view attachment until a
|
||||
// later UI pass, after AppKit has committed the ordinary window style.
|
||||
void PrepareWindow(slint::ComponentHandle<ui::StreamWindow>& stream,
|
||||
StateProvider state_provider,
|
||||
DirtyCallback dirty_callback);
|
||||
|
||||
// Returns true only when a deferred native surface became attached during
|
||||
// this call, allowing the caller to seed it from a cached frame once.
|
||||
bool EnsureAttached();
|
||||
bool NeedsRedraw() const;
|
||||
|
||||
bool SelectStream(std::string remote_id);
|
||||
PresentResult Present(const Frame& frame);
|
||||
void ForgetStream(const std::string& remote_id);
|
||||
void Detach();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
@@ -11,12 +11,7 @@
|
||||
#include "display_stream_id.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#if defined(_WIN32)
|
||||
#include "platform/windows_opengl_video_renderer.h"
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
#include "platform/video_renderer.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
@@ -229,21 +224,14 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
if ((!frame_snapshot || frame_snapshot->empty()) &&
|
||||
props->thumbnail_frame_ && !props->thumbnail_frame_->empty()) {
|
||||
frame_snapshot = props->thumbnail_frame_;
|
||||
video_width = props->thumbnail_width_;
|
||||
video_height = props->thumbnail_height_;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
#if defined(_WIN32)
|
||||
auto* native_renderer = windows_opengl_video_renderer_.get();
|
||||
#else
|
||||
auto* native_renderer = mac_metal_video_renderer_.get();
|
||||
#endif
|
||||
auto* native_renderer = video_renderer_.get();
|
||||
if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) {
|
||||
auto native_snapshot = std::make_shared<std::vector<unsigned char>>();
|
||||
if (native_renderer->CopyLatestNv12(props->remote_id_,
|
||||
@@ -252,7 +240,6 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
frame_snapshot = std::move(native_snapshot);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
||||
video_height > 0) {
|
||||
@@ -276,12 +263,10 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
if (native_renderer) {
|
||||
native_renderer->DiscardStream(props->remote_id_);
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (props->peer_) {
|
||||
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
|
||||
@@ -343,18 +328,21 @@ void GuiRuntime::ResetRemoteSessionResources(
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
props->thumbnail_frame_.reset();
|
||||
props->thumbnail_width_ = 0;
|
||||
props->thumbnail_height_ = 0;
|
||||
props->background_snapshot_time_ = {};
|
||||
#endif
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
@@ -12,12 +12,7 @@
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "localization.h"
|
||||
#if defined(_WIN32)
|
||||
#include "platform/windows_opengl_video_renderer.h"
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
#include "platform/video_renderer.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -15,12 +15,7 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
#if defined(_WIN32)
|
||||
class WindowsOpenGlVideoRenderer;
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
class MacMetalVideoRenderer;
|
||||
#endif
|
||||
class VideoRenderer;
|
||||
|
||||
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
|
||||
// state, but no window lifecycle, ImGui view, or transport callback methods.
|
||||
@@ -90,12 +85,7 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
KeyboardController keyboard_;
|
||||
PeerEventHandler peer_events_;
|
||||
std::atomic<bool> video_frame_dirty_{false};
|
||||
#if defined(_WIN32)
|
||||
std::unique_ptr<WindowsOpenGlVideoRenderer> windows_opengl_video_renderer_;
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
std::unique_ptr<MacMetalVideoRenderer> mac_metal_video_renderer_;
|
||||
#endif
|
||||
std::unique_ptr<VideoRenderer> video_renderer_;
|
||||
|
||||
private:
|
||||
friend class ClipboardController;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -16,12 +16,7 @@
|
||||
#include "file_transfer.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#if defined(_WIN32)
|
||||
#include "platform/windows_opengl_video_renderer.h"
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
#include "platform/video_renderer.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "runtime/remote_action_codec.h"
|
||||
@@ -254,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 =
|
||||
@@ -318,63 +318,52 @@ 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;
|
||||
}
|
||||
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
|
||||
int native_snapshot_width = 0;
|
||||
int native_snapshot_height = 0;
|
||||
bool needs_native_snapshot = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
needs_native_snapshot = !props->thumbnail_frame_ ||
|
||||
props->thumbnail_frame_->empty();
|
||||
needs_native_snapshot =
|
||||
!props->thumbnail_frame_ || props->thumbnail_frame_->empty();
|
||||
}
|
||||
if (needs_native_snapshot) {
|
||||
#if defined(_WIN32)
|
||||
auto* native_renderer =
|
||||
runtime->windows_opengl_video_renderer_.get();
|
||||
#else
|
||||
auto* native_renderer = runtime->mac_metal_video_renderer_.get();
|
||||
#endif
|
||||
auto* native_renderer = runtime->video_renderer_.get();
|
||||
auto snapshot = std::make_shared<std::vector<unsigned char>>();
|
||||
if (native_renderer && native_renderer->CopyLatestNv12(
|
||||
remote_id, snapshot.get(),
|
||||
&native_snapshot_width,
|
||||
&native_snapshot_height)) {
|
||||
if (native_renderer &&
|
||||
native_renderer->CopyLatestNv12(remote_id, snapshot.get(),
|
||||
&native_snapshot_width,
|
||||
&native_snapshot_height)) {
|
||||
native_snapshot = std::move(snapshot);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
if (native_snapshot &&
|
||||
(!props->thumbnail_frame_ || props->thumbnail_frame_->empty())) {
|
||||
props->thumbnail_frame_ = std::move(native_snapshot);
|
||||
props->thumbnail_width_ = native_snapshot_width;
|
||||
props->thumbnail_height_ = native_snapshot_height;
|
||||
}
|
||||
#endif
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
props->render_rect_dirty_ = true;
|
||||
props->stream_cleanup_pending_ = true;
|
||||
}
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
#if defined(_WIN32)
|
||||
auto* native_renderer = runtime->windows_opengl_video_renderer_.get();
|
||||
#else
|
||||
auto* native_renderer = runtime->mac_metal_video_renderer_.get();
|
||||
#endif
|
||||
auto* native_renderer = runtime->video_renderer_.get();
|
||||
if (native_renderer) {
|
||||
native_renderer->DiscardStream(remote_id);
|
||||
runtime->video_frame_dirty_.store(true,
|
||||
std::memory_order_release);
|
||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
#endif
|
||||
|
||||
runtime->focus_on_stream_window_ = false;
|
||||
|
||||
|
||||
@@ -1,36 +1,26 @@
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "platform/video_renderer.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
#include <chrono>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include "platform/windows_opengl_video_renderer.h"
|
||||
#else
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
#endif
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
namespace crossdesk {
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
namespace {
|
||||
|
||||
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
const XVideoFrame *video_frame, const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size, void *user_data) {
|
||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
||||
const XVideoFrame* video_frame, const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
@@ -41,32 +31,20 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
runtime->remote_sessions_.end()) {
|
||||
return;
|
||||
}
|
||||
GuiRuntime::RemoteSession *props =
|
||||
GuiRuntime::RemoteSession* props =
|
||||
runtime->remote_sessions_.find(remote_id)->second.get();
|
||||
|
||||
if (props->connection_established_) {
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
bool background_snapshot_only = false;
|
||||
#if defined(_WIN32)
|
||||
auto* native_renderer = runtime->windows_opengl_video_renderer_.get();
|
||||
using NativeVideoRenderer = WindowsOpenGlVideoRenderer;
|
||||
#else
|
||||
auto* native_renderer = runtime->mac_metal_video_renderer_.get();
|
||||
using NativeVideoRenderer = MacMetalVideoRenderer;
|
||||
#endif
|
||||
if (native_renderer && native_renderer->IsReady()
|
||||
#if defined(__APPLE__)
|
||||
&& native_renderer->IsAttached()
|
||||
#endif
|
||||
) {
|
||||
auto* native_renderer = runtime->video_renderer_.get();
|
||||
if (native_renderer && native_renderer->IsActive()) {
|
||||
const auto submit_result = native_renderer->SubmitNv12(
|
||||
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
|
||||
video_frame->size, video_frame->width, video_frame->height);
|
||||
if (submit_result == NativeVideoRenderer::SubmitResult::submitted) {
|
||||
if (submit_result == VideoRenderer::SubmitResult::submitted) {
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
const bool size_changed =
|
||||
(props->video_width_ != video_frame->width) ||
|
||||
(props->video_height_ != video_frame->height);
|
||||
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||
(props->video_height_ != video_frame->height);
|
||||
if (size_changed) {
|
||||
props->render_rect_dirty_ = true;
|
||||
}
|
||||
@@ -86,22 +64,19 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
if (submit_result == NativeVideoRenderer::SubmitResult::dropped ||
|
||||
submit_result == NativeVideoRenderer::SubmitResult::failed) {
|
||||
if (submit_result == VideoRenderer::SubmitResult::dropped ||
|
||||
submit_result == VideoRenderer::SubmitResult::failed) {
|
||||
// Keep presenting the last native frame. A later decoded frame can
|
||||
// reuse the renderer without changing ownership mid-window.
|
||||
props->streaming_ = true;
|
||||
return;
|
||||
}
|
||||
if (submit_result ==
|
||||
NativeVideoRenderer::SubmitResult::not_selected) {
|
||||
if (submit_result == VideoRenderer::SubmitResult::not_selected) {
|
||||
background_snapshot_only = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (background_snapshot_only && props->thumbnail_frame_ &&
|
||||
!props->thumbnail_frame_->empty() &&
|
||||
@@ -112,7 +87,6 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
props->streaming_ = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Allocate a third buffer only while the UI still owns the old snapshot.
|
||||
if (!props->back_frame_ || props->back_frame_.use_count() != 1) {
|
||||
@@ -137,32 +111,28 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
props->video_size_ = video_frame->size;
|
||||
|
||||
props->front_frame_.swap(props->back_frame_);
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
props->thumbnail_frame_ = props->front_frame_;
|
||||
props->thumbnail_width_ = video_frame->width;
|
||||
props->thumbnail_height_ = video_frame->height;
|
||||
if (background_snapshot_only) {
|
||||
props->background_snapshot_time_ = now;
|
||||
}
|
||||
#endif
|
||||
++props->video_frame_sequence_;
|
||||
}
|
||||
|
||||
props->streaming_ = true;
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
if (background_snapshot_only) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
void PeerEventHandler::OnReceiveAudioBuffer(
|
||||
const char *data, size_t size, const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size, void *user_data) {
|
||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
||||
const char* data, size_t size, const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
@@ -172,4 +142,4 @@ void PeerEventHandler::OnReceiveAudioBuffer(
|
||||
runtime->devices_.PushAudio(data, size);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "display_info.h"
|
||||
#include "minirtc.h"
|
||||
|
||||
@@ -110,14 +111,12 @@ struct RemoteSession {
|
||||
std::mutex video_frame_mutex_;
|
||||
std::shared_ptr<std::vector<unsigned char>> front_frame_;
|
||||
std::shared_ptr<std::vector<unsigned char>> back_frame_;
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
// Retains the most recent background CPU frame across disconnect cleanup
|
||||
// so closing an unselected tab can still update its recent-item thumbnail.
|
||||
std::shared_ptr<std::vector<unsigned char>> thumbnail_frame_;
|
||||
int thumbnail_width_ = 0;
|
||||
int thumbnail_height_ = 0;
|
||||
std::chrono::steady_clock::time_point background_snapshot_time_;
|
||||
#endif
|
||||
bool render_rect_dirty_ = false;
|
||||
bool stream_cleanup_pending_ = false;
|
||||
float mouse_pos_x_ = 0;
|
||||
@@ -150,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;
|
||||
|
||||
@@ -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] = "";
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
Submodule submodules/minirtc updated: ad2614b3a1...3aa5fa5ed0
@@ -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
@@ -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
|
||||
|
||||
+8
-8
@@ -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")
|
||||
@@ -270,6 +265,7 @@ function setup_targets()
|
||||
"device_controller", "thumbnail", "version_checker", "tools")
|
||||
add_files("src/gui/render.cpp", "src/gui/application/gui_application.cpp",
|
||||
"src/gui/application/portable_service_integration.cpp",
|
||||
"src/gui/rendering/*.cpp",
|
||||
"src/gui/runtime/*.cpp",
|
||||
"src/gui/features/devices/*.cpp", "src/gui/features/input/*.cpp",
|
||||
"src/gui/features/clipboard/*.cpp", "src/gui/features/file_transfer/*.cpp",
|
||||
@@ -279,15 +275,19 @@ function setup_targets()
|
||||
add_cxxflags("/bigobj")
|
||||
add_links("opengl32")
|
||||
add_files("src/gui/platform/tray/win_tray.cpp",
|
||||
"src/gui/platform/windows_opengl_video_renderer.cpp")
|
||||
"src/gui/platform/opengl_video_renderer.cpp",
|
||||
"src/gui/platform/video_renderer_factory_opengl.cpp")
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
elseif is_os("macosx") then
|
||||
add_files("src/gui/runtime/*.mm", "src/gui/platform/tray/*.mm",
|
||||
"src/gui/platform/window_drag_mac.mm",
|
||||
"src/gui/platform/metal_video_renderer.mm")
|
||||
"src/gui/platform/metal_video_renderer.mm",
|
||||
"src/gui/platform/video_renderer_factory_metal.mm")
|
||||
elseif is_os("linux") then
|
||||
add_links("GL")
|
||||
add_files("src/gui/platform/tray/linux_tray.cpp")
|
||||
add_files("src/gui/platform/tray/linux_tray.cpp",
|
||||
"src/gui/platform/opengl_video_renderer.cpp",
|
||||
"src/gui/platform/video_renderer_factory_opengl.cpp")
|
||||
end
|
||||
|
||||
if is_os("windows") then
|
||||
|
||||
Reference in New Issue
Block a user