mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-26 13:29:31 +08:00
[feat] add native Metal video rendering for Slint on macOS
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
#include "crossdesk_ui.h"
|
||||
#include "fa_solid_900.h"
|
||||
#include "ui/ui_localization.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <charconv>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -36,9 +39,309 @@ bool RegisterFontAwesome(slint::Window &window) {
|
||||
.has_value();
|
||||
}
|
||||
|
||||
struct CaptureOptions {
|
||||
bool enabled = false;
|
||||
std::string page;
|
||||
int language = 0;
|
||||
std::string snapshot_path;
|
||||
};
|
||||
|
||||
// Compatibility entry point for the former application-level debug capture
|
||||
// mode. CROSSDESK_UI_CAPTURE keeps the interactive workflow, while
|
||||
// CROSSDESK_UI_CAPTURE_SNAPSHOT writes a deterministic PPM and exits so every
|
||||
// page can be exercised in automation. The legacy /tmp overrides still win.
|
||||
std::string ReadFirstLine(const char *path) {
|
||||
std::ifstream input(path);
|
||||
std::string value;
|
||||
std::getline(input, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
CaptureOptions LoadCaptureOptions() {
|
||||
CaptureOptions options;
|
||||
if (const char *capture = std::getenv("CROSSDESK_UI_CAPTURE")) {
|
||||
options.enabled = std::string_view(capture) != "0";
|
||||
}
|
||||
if (!options.enabled) {
|
||||
return options;
|
||||
}
|
||||
|
||||
if (const char *page = std::getenv("CROSSDESK_UI_CAPTURE_PAGE")) {
|
||||
options.page = page;
|
||||
}
|
||||
if (const std::string page_override =
|
||||
ReadFirstLine("/tmp/crossdesk-ui-capture-page");
|
||||
!page_override.empty()) {
|
||||
options.page = page_override;
|
||||
}
|
||||
|
||||
std::string language_value =
|
||||
ReadFirstLine("/tmp/crossdesk-ui-capture-language");
|
||||
if (language_value.empty()) {
|
||||
if (const char *language =
|
||||
std::getenv("CROSSDESK_UI_CAPTURE_LANGUAGE")) {
|
||||
language_value = language;
|
||||
}
|
||||
}
|
||||
if (!language_value.empty()) {
|
||||
int language = 0;
|
||||
const auto [end, error] = std::from_chars(
|
||||
language_value.data(), language_value.data() + language_value.size(),
|
||||
language);
|
||||
if (error == std::errc{} &&
|
||||
end == language_value.data() + language_value.size()) {
|
||||
options.language =
|
||||
crossdesk::localization::detail::ClampLanguageIndex(language);
|
||||
}
|
||||
}
|
||||
if (const char *snapshot =
|
||||
std::getenv("CROSSDESK_UI_CAPTURE_SNAPSHOT")) {
|
||||
options.snapshot_path = snapshot;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
bool HasNonEmptyEnvironmentVariable(const char *name) {
|
||||
const char *value = std::getenv(name);
|
||||
return value != nullptr && value[0] != '\0';
|
||||
}
|
||||
|
||||
void ResetMainCaptureState(
|
||||
const slint::ComponentHandle<crossdesk::ui::MainWindow> &window) {
|
||||
window->set_settings_open(false);
|
||||
window->set_self_host_settings_open(false);
|
||||
window->set_about_open(false);
|
||||
window->set_update_open(false);
|
||||
window->set_update_available(false);
|
||||
window->set_reset_password_open(false);
|
||||
window->set_reset_password_invalid(false);
|
||||
window->set_new_password_input("");
|
||||
window->set_alias_open(false);
|
||||
window->set_alias_input("");
|
||||
window->set_delete_open(false);
|
||||
window->set_offline_warning("");
|
||||
window->set_connection_dialog_open(false);
|
||||
window->set_connection_status_text("");
|
||||
window->set_connection_pending(false);
|
||||
window->set_connection_password_required(false);
|
||||
window->set_connection_validating(false);
|
||||
window->set_permission_dialog_open(false);
|
||||
window->set_screen_recording_granted(true);
|
||||
window->set_accessibility_granted(true);
|
||||
window->set_settings_session_active(false);
|
||||
window->set_portable_service_settings_visible(false);
|
||||
window->set_portable_service_dialog_open(false);
|
||||
window->set_portable_service_suppressed_notice_open(false);
|
||||
}
|
||||
|
||||
void ConfigureMainCapturePage(
|
||||
const slint::ComponentHandle<crossdesk::ui::MainWindow> &window,
|
||||
const CaptureOptions &options) {
|
||||
const int language = crossdesk::ui_localization::ApplyMainWindowStrings(
|
||||
window, options.language);
|
||||
crossdesk::ui_localization::ApplyStreamWindowStrings(window, language);
|
||||
window->set_language_index(language);
|
||||
ResetMainCaptureState(window);
|
||||
|
||||
#if _WIN32
|
||||
window->set_custom_titlebar(true);
|
||||
window->set_wayland_titlebar(false);
|
||||
#if CROSSDESK_PORTABLE
|
||||
window->set_portable_service_settings_visible(true);
|
||||
#endif
|
||||
#elif defined(__linux__)
|
||||
const bool has_x11 = HasNonEmptyEnvironmentVariable("DISPLAY");
|
||||
const bool use_xwayland =
|
||||
has_x11 && HasNonEmptyEnvironmentVariable("WAYLAND_DISPLAY");
|
||||
window->set_custom_titlebar(has_x11);
|
||||
window->set_wayland_titlebar(use_xwayland);
|
||||
#else
|
||||
window->set_custom_titlebar(false);
|
||||
window->set_wayland_titlebar(false);
|
||||
#endif
|
||||
|
||||
if (options.page == "settings") {
|
||||
window->set_settings_open(true);
|
||||
} else if (options.page == "self-hosted") {
|
||||
window->set_settings_open(true);
|
||||
window->set_self_host_settings_open(true);
|
||||
} else if (options.page == "about") {
|
||||
window->set_current_version("0.0.0");
|
||||
window->set_about_open(true);
|
||||
} else if (options.page == "update") {
|
||||
window->set_update_available(true);
|
||||
window->set_latest_version("v0.0.1");
|
||||
window->set_release_name("CrossDesk Preview");
|
||||
window->set_release_date("2026-08-25");
|
||||
window->set_update_open(true);
|
||||
} else if (options.page == "reset-password") {
|
||||
window->set_reset_password_open(true);
|
||||
} else if (options.page == "reset-password-invalid") {
|
||||
window->set_reset_password_open(true);
|
||||
window->set_reset_password_invalid(true);
|
||||
window->set_new_password_input("123");
|
||||
} else if (options.page == "alias") {
|
||||
window->set_alias_input("Office workstation");
|
||||
window->set_alias_open(true);
|
||||
} else if (options.page == "delete") {
|
||||
window->set_delete_open(true);
|
||||
} else if (options.page == "offline") {
|
||||
window->set_offline_warning(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::device_offline[language]));
|
||||
} else if (options.page == "connection") {
|
||||
window->set_connection_status_text(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::p2p_connecting[language]));
|
||||
window->set_connection_pending(true);
|
||||
window->set_connection_dialog_open(true);
|
||||
} else if (options.page == "connection-password") {
|
||||
window->set_connection_status_text(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::reinput_password[language]));
|
||||
window->set_connection_password_required(true);
|
||||
window->set_connection_dialog_open(true);
|
||||
} else if (options.page == "service") {
|
||||
window->set_portable_service_settings_visible(true);
|
||||
window->set_portable_service_dialog_open(true);
|
||||
} else if (options.page == "service-notice") {
|
||||
window->set_portable_service_settings_visible(true);
|
||||
window->set_portable_service_suppressed_notice_open(true);
|
||||
} else if (options.page == "permission") {
|
||||
window->set_permission_dialog_open(true);
|
||||
window->set_screen_recording_granted(false);
|
||||
window->set_accessibility_granted(false);
|
||||
}
|
||||
}
|
||||
|
||||
slint::Image CreateStreamPreviewImage() {
|
||||
constexpr int preview_width = 640;
|
||||
constexpr int preview_height = 360;
|
||||
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(preview_width,
|
||||
preview_height);
|
||||
auto *data = reinterpret_cast<uint8_t *>(pixels.begin());
|
||||
for (int y = 0; y < preview_height; ++y) {
|
||||
for (int x = 0; x < preview_width; ++x) {
|
||||
const bool border = x < 8 || y < 8 || x >= preview_width - 8 ||
|
||||
y >= preview_height - 8;
|
||||
const size_t offset = (static_cast<size_t>(y) * preview_width + x) * 3;
|
||||
data[offset] =
|
||||
border ? 240 : static_cast<uint8_t>(35 + 150 * x / preview_width);
|
||||
data[offset + 1] =
|
||||
border ? 70 : static_cast<uint8_t>(35 + 150 * y / preview_height);
|
||||
data[offset + 2] = border ? 70 : 90;
|
||||
}
|
||||
}
|
||||
return slint::Image(std::move(pixels));
|
||||
}
|
||||
|
||||
void ConfigureStreamCapturePage(
|
||||
const slint::ComponentHandle<crossdesk::ui::StreamWindow> &stream,
|
||||
int language) {
|
||||
language = crossdesk::ui_localization::ApplyStreamWindowStrings(
|
||||
stream, language);
|
||||
#if defined(__linux__)
|
||||
stream->set_custom_titlebar(
|
||||
HasNonEmptyEnvironmentVariable("DISPLAY") &&
|
||||
HasNonEmptyEnvironmentVariable("WAYLAND_DISPLAY"));
|
||||
#else
|
||||
stream->set_custom_titlebar(false);
|
||||
#endif
|
||||
stream->set_native_video_enabled(false);
|
||||
stream->set_window_maximized(false);
|
||||
stream->set_fullscreen_enabled(false);
|
||||
stream->set_stats_visible(false);
|
||||
stream->set_file_transfer_visible(false);
|
||||
stream->set_receiving_text("");
|
||||
stream->set_status_text("");
|
||||
|
||||
crossdesk::ui::StreamTab tab;
|
||||
tab.remote_id = "589173341";
|
||||
tab.title = "Mac";
|
||||
tab.connected = true;
|
||||
stream->set_tabs(
|
||||
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
|
||||
std::vector{tab}));
|
||||
stream->set_displays(
|
||||
std::make_shared<slint::VectorModel<slint::SharedString>>(
|
||||
std::vector{slint::SharedString("Display 1")}));
|
||||
stream->set_frame(CreateStreamPreviewImage());
|
||||
stream->set_has_frame(true);
|
||||
const float height = stream->get_custom_titlebar() ? 752.0f : 720.0f;
|
||||
stream->window().set_size(
|
||||
slint::LogicalSize(slint::Size<float>{1280.0f, height}));
|
||||
}
|
||||
|
||||
void ConfigureServerCapturePage(
|
||||
const slint::ComponentHandle<crossdesk::ui::ServerWindow> &server,
|
||||
int language) {
|
||||
language = crossdesk::localization::detail::ClampLanguageIndex(language);
|
||||
crossdesk::ui::ControllerEntry controller;
|
||||
controller.remote_id = "589173341";
|
||||
controller.display_name = "Mac";
|
||||
server->set_controllers(
|
||||
std::make_shared<slint::VectorModel<crossdesk::ui::ControllerEntry>>(
|
||||
std::vector{controller}));
|
||||
server->set_controller_names(
|
||||
std::make_shared<slint::VectorModel<slint::SharedString>>(
|
||||
std::vector{slint::SharedString("Mac")}));
|
||||
server->set_language_index(language);
|
||||
server->set_controller_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::controller[language]));
|
||||
server->set_connection_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::connection_status[language]));
|
||||
server->set_connection_status(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::p2p_connected[language]));
|
||||
server->set_file_transfer_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::file_transfer[language]));
|
||||
server->set_select_file_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::select_file[language]));
|
||||
server->set_file_transfer_visible(false);
|
||||
server->set_sending_file(false);
|
||||
const float width = language == 0 ? 250.0f : language == 1 ? 330.0f : 430.0f;
|
||||
server->window().set_size(
|
||||
slint::LogicalSize(slint::Size<float>{width, 150.0f}));
|
||||
}
|
||||
|
||||
int RunCaptureMode(
|
||||
const CaptureOptions &options,
|
||||
slint::ComponentHandle<crossdesk::ui::MainWindow> &window,
|
||||
slint::ComponentHandle<crossdesk::ui::StreamWindow> &stream,
|
||||
slint::ComponentHandle<crossdesk::ui::ServerWindow> &server) {
|
||||
ConfigureMainCapturePage(window, options);
|
||||
stream->hide();
|
||||
server->hide();
|
||||
|
||||
slint::Window *capture_window = &window->window();
|
||||
if (options.page == "stream") {
|
||||
ConfigureStreamCapturePage(stream, options.language);
|
||||
window->hide();
|
||||
stream->show();
|
||||
capture_window = &stream->window();
|
||||
} else if (options.page == "server") {
|
||||
ConfigureServerCapturePage(server, options.language);
|
||||
window->hide();
|
||||
server->show();
|
||||
capture_window = &server->window();
|
||||
} else {
|
||||
window->show();
|
||||
}
|
||||
|
||||
if (!options.snapshot_path.empty()) {
|
||||
return WriteWindowSnapshot(*capture_window, options.snapshot_path.c_str())
|
||||
? 0
|
||||
: 7;
|
||||
}
|
||||
|
||||
if (options.page == "stream" || options.page == "server") {
|
||||
slint::run_event_loop();
|
||||
} else {
|
||||
window->run();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const CaptureOptions capture_options = LoadCaptureOptions();
|
||||
auto window = crossdesk::ui::MainWindow::create();
|
||||
auto stream = crossdesk::ui::StreamWindow::create();
|
||||
auto server = crossdesk::ui::ServerWindow::create();
|
||||
@@ -143,10 +446,15 @@ int main() {
|
||||
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
|
||||
std::vector{tab}));
|
||||
stream->set_custom_titlebar(true);
|
||||
stream->set_native_video_enabled(true);
|
||||
stream->set_window_maximized(true);
|
||||
assert(stream->get_tabs()->row_count() == 1);
|
||||
assert(stream->get_custom_titlebar());
|
||||
assert(stream->get_native_video_enabled());
|
||||
assert(stream->get_window_maximized());
|
||||
// Keep snapshots on the CPU-rendered UI path; the native Metal video view
|
||||
// is intentionally outside Slint's component snapshot.
|
||||
stream->set_native_video_enabled(false);
|
||||
if (const char *snapshot_path =
|
||||
std::getenv("CROSSDESK_STREAM_UI_SNAPSHOT")) {
|
||||
stream->set_window_maximized(false);
|
||||
@@ -276,5 +584,9 @@ int main() {
|
||||
server->invoke_controller_selected(0);
|
||||
assert(controller_selected);
|
||||
|
||||
if (capture_options.enabled) {
|
||||
return RunCaptureMode(capture_options, window, stream, server);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user