mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-26 21:35:52 +08:00
[feat] add native Metal video rendering for Slint on macOS
This commit is contained in:
@@ -11,9 +11,9 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
@@ -36,11 +36,7 @@
|
||||
|
||||
#include "platform/tray/win_tray.h"
|
||||
#elif defined(__APPLE__)
|
||||
#ifndef GL_SILENCE_DEPRECATION
|
||||
#define GL_SILENCE_DEPRECATION
|
||||
#endif
|
||||
#include <OpenGL/gl.h>
|
||||
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#include "platform/tray/mac_tray.h"
|
||||
#include "platform/window_drag.h"
|
||||
#elif defined(__linux__)
|
||||
@@ -52,6 +48,7 @@
|
||||
#include "platform/tray/linux_tray.h"
|
||||
#endif
|
||||
#include "rd_log.h"
|
||||
#include "ui/ui_localization.h"
|
||||
#include "version_checker.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -59,7 +56,11 @@ namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
constexpr GLint kGlClampToEdge = 0x812F;
|
||||
#else
|
||||
constexpr int kMetalAttachmentAttemptLimit = 30;
|
||||
#endif
|
||||
|
||||
struct VideoRenderSize {
|
||||
int width = 0;
|
||||
@@ -864,6 +865,7 @@ struct GuiApplication::SlintUi {
|
||||
std::make_shared<slint::VectorModel<slint::SharedString>>();
|
||||
std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
|
||||
std::vector<uint8_t> scaled_video_frame;
|
||||
#if !defined(__APPLE__)
|
||||
std::mutex video_gl_mutex;
|
||||
std::vector<uint8_t> video_gl_conversion_frame;
|
||||
std::vector<uint8_t> video_gl_pending_frame;
|
||||
@@ -872,6 +874,7 @@ struct GuiApplication::SlintUi {
|
||||
VideoRenderSize video_gl_image_size;
|
||||
VideoRenderSize video_gl_pending_size;
|
||||
bool video_gl_pending_dirty = false;
|
||||
#endif
|
||||
std::vector<std::string> tab_order;
|
||||
std::vector<std::string> tab_ids;
|
||||
std::string tab_model_signature;
|
||||
@@ -886,11 +889,12 @@ 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;
|
||||
int localized_language = -1;
|
||||
bool capture_mode = false;
|
||||
std::string capture_page;
|
||||
std::chrono::steady_clock::time_point last_clipboard_poll{};
|
||||
slint::Timer video_timer;
|
||||
#if _WIN32
|
||||
@@ -972,18 +976,11 @@ int GuiApplication::Run() {
|
||||
|
||||
ui_->timer.start(slint::TimerMode::Repeated, 16ms, [this] { Tick(); });
|
||||
ScheduleNextVideoFrame();
|
||||
if (ui_->capture_mode &&
|
||||
(ui_->capture_page == "stream" || ui_->capture_page == "server")) {
|
||||
slint::run_event_loop();
|
||||
} else if (ui_->capture_mode) {
|
||||
ui_->main->run();
|
||||
} else {
|
||||
// The native tray icon is managed outside Slint, so Slint cannot count it
|
||||
// when deciding whether the last hidden window should stop the event loop.
|
||||
// Keep the loop alive until the tray's explicit Exit action requests quit.
|
||||
ui_->main->show();
|
||||
slint::run_event_loop(slint::EventLoopMode::RunUntilQuit);
|
||||
}
|
||||
// The native tray icon is managed outside Slint, so Slint cannot count it
|
||||
// when deciding whether the last hidden window should stop the event loop.
|
||||
// Keep the loop alive until the tray's explicit Exit action requests quit.
|
||||
ui_->main->show();
|
||||
slint::run_event_loop(slint::EventLoopMode::RunUntilQuit);
|
||||
Cleanup();
|
||||
return 0;
|
||||
}
|
||||
@@ -1046,12 +1043,34 @@ void GuiApplication::InitializeModules() {
|
||||
if (modules_inited_) {
|
||||
return;
|
||||
}
|
||||
#if 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
|
||||
devices_.Initialize();
|
||||
CreateConnectionPeer();
|
||||
modules_inited_ = true;
|
||||
}
|
||||
|
||||
void GuiApplication::InitializeUi() {
|
||||
#if defined(__APPLE__)
|
||||
// Slint's renderer must be selected before the first component creates the
|
||||
// process-wide backend. The macOS package contains Skia (Metal) and the
|
||||
// software fallback, but no OpenGL window renderer.
|
||||
const char* requested_slint_backend = std::getenv("SLINT_BACKEND");
|
||||
if (requested_slint_backend == nullptr ||
|
||||
requested_slint_backend[0] == '\0') {
|
||||
if (setenv("SLINT_BACKEND", "winit-skia", 1) == 0) {
|
||||
LOG_INFO("Using Slint winit-skia renderer with the macOS Metal backend");
|
||||
} else {
|
||||
LOG_WARN("Unable to select the Slint Metal backend through SLINT_BACKEND");
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Using requested Slint backend: {}", requested_slint_backend);
|
||||
}
|
||||
#endif
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
// Winit prefers Wayland whenever WAYLAND_DISPLAY/WAYLAND_SOCKET is present.
|
||||
// Hide them only while Slint selects its process-wide window backend, then
|
||||
@@ -1080,189 +1099,12 @@ void GuiApplication::InitializeUi() {
|
||||
ui_->release_note_blocks_model->set_vector(std::move(release_note_blocks));
|
||||
ui_->main->set_release_note_blocks(ui_->release_note_blocks_model);
|
||||
RegisterFontAwesome(ui_->main->window());
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
auto read_capture_override = [](const char* path) -> std::string {
|
||||
std::ifstream input(path);
|
||||
std::string value;
|
||||
std::getline(input, value);
|
||||
return value;
|
||||
};
|
||||
if (const char* capture = std::getenv("CROSSDESK_UI_CAPTURE")) {
|
||||
ui_->capture_mode = std::string_view(capture) != "0";
|
||||
}
|
||||
if (const char* page = std::getenv("CROSSDESK_UI_CAPTURE_PAGE")) {
|
||||
ui_->capture_page = page;
|
||||
}
|
||||
std::string capture_language_override;
|
||||
if (ui_->capture_mode) {
|
||||
if (const auto page =
|
||||
read_capture_override("/tmp/crossdesk-ui-capture-page");
|
||||
!page.empty()) {
|
||||
ui_->capture_page = page;
|
||||
}
|
||||
capture_language_override =
|
||||
read_capture_override("/tmp/crossdesk-ui-capture-language");
|
||||
}
|
||||
const char* language = capture_language_override.empty()
|
||||
? std::getenv("CROSSDESK_UI_CAPTURE_LANGUAGE")
|
||||
: capture_language_override.c_str();
|
||||
if (language) {
|
||||
int capture_language = 0;
|
||||
const auto [end, error] = std::from_chars(
|
||||
language, language + std::strlen(language), capture_language);
|
||||
if (error == std::errc{} && end == language + std::strlen(language)) {
|
||||
localization_language_index_ =
|
||||
localization::detail::ClampLanguageIndex(capture_language);
|
||||
language_button_value_ = localization_language_index_;
|
||||
localization_language_ =
|
||||
static_cast<ConfigCenter::LANGUAGE>(localization_language_index_);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
ui_->main->set_recent_connections(ui_->recent_model);
|
||||
ResetSettingsUi();
|
||||
BindMainCallbacks();
|
||||
InitializeSystemTray();
|
||||
UpdateLocalization();
|
||||
if (ui_->capture_mode) {
|
||||
const bool detected_update = update_available_;
|
||||
update_available_ = false;
|
||||
if (ui_->capture_page == "settings") {
|
||||
ui_->main->set_settings_open(true);
|
||||
} else if (ui_->capture_page == "self-hosted") {
|
||||
ui_->main->set_settings_open(true);
|
||||
ui_->main->set_self_host_settings_open(true);
|
||||
} else if (ui_->capture_page == "about") {
|
||||
update_available_ = detected_update;
|
||||
ui_->main->set_update_open(false);
|
||||
ui_->main->set_about_open(true);
|
||||
} else if (ui_->capture_page == "update") {
|
||||
update_available_ = detected_update;
|
||||
ui_->main->set_update_open(true);
|
||||
} else if (ui_->capture_page == "reset-password") {
|
||||
ui_->main->set_reset_password_open(true);
|
||||
} else if (ui_->capture_page == "reset-password-invalid") {
|
||||
ui_->main->set_reset_password_open(true);
|
||||
ui_->main->set_reset_password_invalid(true);
|
||||
ui_->main->set_new_password_input("123");
|
||||
} else if (ui_->capture_page == "alias") {
|
||||
ui_->main->set_alias_input("Office workstation");
|
||||
ui_->main->set_alias_open(true);
|
||||
} else if (ui_->capture_page == "delete") {
|
||||
ui_->main->set_delete_open(true);
|
||||
} else if (ui_->capture_page == "offline") {
|
||||
ui_->main->set_offline_warning(
|
||||
UiText(localization::device_offline[localization_language_index_]));
|
||||
} else if (ui_->capture_page == "connection") {
|
||||
ui_->main->set_connection_status_text(
|
||||
UiText(localization::p2p_connecting[localization_language_index_]));
|
||||
ui_->main->set_connection_pending(true);
|
||||
ui_->main->set_connection_dialog_open(true);
|
||||
} else if (ui_->capture_page == "connection-password") {
|
||||
ui_->main->set_connection_status_text(
|
||||
UiText(localization::reinput_password[localization_language_index_]));
|
||||
ui_->main->set_connection_password_required(true);
|
||||
ui_->main->set_connection_dialog_open(true);
|
||||
} else if (ui_->capture_page == "service") {
|
||||
ui_->main->set_portable_service_dialog_open(true);
|
||||
} else if (ui_->capture_page == "service-notice") {
|
||||
ui_->main->set_portable_service_suppressed_notice_open(true);
|
||||
}
|
||||
}
|
||||
SyncMainWindow();
|
||||
|
||||
if (ui_->capture_mode && ui_->capture_page == "stream") {
|
||||
ui_->stream.emplace(ui::StreamWindow::create());
|
||||
#if defined(__linux__)
|
||||
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
|
||||
#endif
|
||||
RegisterFontAwesome((*ui_->stream)->window());
|
||||
ConfigureStreamVideoRenderer();
|
||||
(*ui_->stream)->set_tabs(ui_->tab_model);
|
||||
(*ui_->stream)->set_displays(ui_->display_model);
|
||||
(*ui_->stream)->set_file_transfers(ui_->transfer_model);
|
||||
(*ui_->stream)->set_stats_rows(ui_->stats_model);
|
||||
ui::StreamTab preview_tab;
|
||||
preview_tab.remote_id = "589173341";
|
||||
preview_tab.title = "Mac";
|
||||
preview_tab.connected = true;
|
||||
ui_->tab_model->set_vector({preview_tab});
|
||||
ui_->display_model->set_vector({slint::SharedString("Display 1")});
|
||||
ui_->localized_language = -1;
|
||||
UpdateLocalization();
|
||||
(*ui_->stream)
|
||||
->set_status_text(
|
||||
UiText(localization::p2p_connected[localization_language_index_]));
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
// A bordered 16:9 frame makes stream scaling/cropping regressions visible
|
||||
// in the existing capture mode without requiring a live remote session.
|
||||
constexpr int preview_width = 640;
|
||||
constexpr int preview_height = 360;
|
||||
slint::SharedPixelBuffer<slint::Rgb8Pixel> preview_pixels(preview_width,
|
||||
preview_height);
|
||||
auto* preview_data = reinterpret_cast<uint8_t*>(preview_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;
|
||||
preview_data[offset] =
|
||||
border ? 240 : static_cast<uint8_t>(35 + 150 * x / preview_width);
|
||||
preview_data[offset + 1] =
|
||||
border ? 70 : static_cast<uint8_t>(35 + 150 * y / preview_height);
|
||||
preview_data[offset + 2] = border ? 70 : 90;
|
||||
}
|
||||
}
|
||||
(*ui_->stream)->set_frame(slint::Image(std::move(preview_pixels)));
|
||||
(*ui_->stream)->set_has_frame(true);
|
||||
(*ui_->stream)->set_status_text("");
|
||||
#endif
|
||||
BindStreamCallbacks();
|
||||
(*ui_->stream)->show();
|
||||
PositionWindowAtCenter(
|
||||
(*ui_->stream)->window(), ui_->main->window(),
|
||||
kStreamWindowLogicalWidth,
|
||||
StreamWindowLogicalHeight((*ui_->stream)->get_custom_titlebar()));
|
||||
ui_->main->hide();
|
||||
} else if (ui_->capture_mode && ui_->capture_page == "server") {
|
||||
ui_->server.emplace(ui::ServerWindow::create());
|
||||
RegisterFontAwesome((*ui_->server)->window());
|
||||
(*ui_->server)->set_controllers(ui_->controller_model);
|
||||
(*ui_->server)->set_controller_names(ui_->controller_name_model);
|
||||
(*ui_->server)->set_language_index(localization_language_index_);
|
||||
const float server_width =
|
||||
ServerWindowLogicalWidth(localization_language_index_);
|
||||
const float server_height = ServerWindowLogicalHeight(false);
|
||||
(*ui_->server)
|
||||
->window()
|
||||
.set_size(slint::LogicalSize(
|
||||
slint::Size<float>{server_width, server_height}));
|
||||
ui::ControllerEntry preview_controller;
|
||||
preview_controller.remote_id = "589173341";
|
||||
preview_controller.display_name = "Mac";
|
||||
ui_->controller_model->set_vector({preview_controller});
|
||||
ui_->controller_name_model->set_vector({slint::SharedString("Mac")});
|
||||
(*ui_->server)
|
||||
->set_controller_label(
|
||||
UiText(localization::controller[localization_language_index_]));
|
||||
(*ui_->server)
|
||||
->set_connection_label(UiText(
|
||||
localization::connection_status[localization_language_index_]));
|
||||
(*ui_->server)
|
||||
->set_connection_status(
|
||||
UiText(localization::p2p_connected[localization_language_index_]));
|
||||
(*ui_->server)
|
||||
->set_file_transfer_label(
|
||||
UiText(localization::file_transfer[localization_language_index_]));
|
||||
(*ui_->server)
|
||||
->set_select_file_label(
|
||||
UiText(localization::select_file[localization_language_index_]));
|
||||
BindServerCallbacks();
|
||||
(*ui_->server)->show();
|
||||
PositionWindowAtBottomRight((*ui_->server)->window(), server_width,
|
||||
server_height);
|
||||
ui_->main->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::InitializeSystemTray() {
|
||||
@@ -1806,7 +1648,9 @@ void GuiApplication::BindStreamCallbacks() {
|
||||
#endif
|
||||
fullscreen_button_pressed_ = enter_fullscreen;
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
#if defined(__APPLE__)
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
#else
|
||||
stream->set_fullscreen_enabled(enter_fullscreen);
|
||||
window.request_redraw();
|
||||
window.set_fullscreen(enter_fullscreen);
|
||||
@@ -1901,10 +1745,6 @@ void GuiApplication::Tick() {
|
||||
clipboard_.HandleLocalUpdate();
|
||||
}
|
||||
}
|
||||
if (ui_->capture_mode &&
|
||||
(ui_->capture_page == "stream" || ui_->capture_page == "server")) {
|
||||
return;
|
||||
}
|
||||
clipboard_.ApplyPendingRemoteText();
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (ui_->tray) {
|
||||
@@ -2073,147 +1913,10 @@ void GuiApplication::UpdateLocalization() {
|
||||
if (!ui_ || ui_->localized_language == language) {
|
||||
return;
|
||||
}
|
||||
auto& strings = ui_->main->global<ui::UiStrings>();
|
||||
strings.set_local_desktop(UiText(localization::local_desktop[language]));
|
||||
strings.set_remote_desktop(UiText(localization::remote_desktop[language]));
|
||||
strings.set_recent_connections(
|
||||
UiText(localization::recent_connections[language]));
|
||||
strings.set_local_id(UiText(localization::local_id[language]));
|
||||
strings.set_device_name(UiText(localization::device_name[language]));
|
||||
strings.set_remote_id(UiText(localization::remote_id[language]));
|
||||
strings.set_online(UiText(localization::online[language]));
|
||||
strings.set_offline(UiText(localization::offline[language]));
|
||||
strings.set_password(UiText(localization::password[language]));
|
||||
strings.set_copied(
|
||||
UiText(localization::local_id_copied_to_clipboard[language]));
|
||||
strings.set_connect(UiText(localization::connect[language]));
|
||||
strings.set_settings(UiText(localization::settings[language]));
|
||||
strings.set_about(UiText(localization::about[language]));
|
||||
strings.set_ok(UiText(localization::ok[language]));
|
||||
strings.set_cancel(UiText(localization::cancel[language]));
|
||||
strings.set_new_password(UiText(localization::new_password[language]));
|
||||
strings.set_invalid_password(
|
||||
UiText(localization::max_password_len[language]));
|
||||
strings.set_edit_alias(
|
||||
UiText(localization::input_connection_alias[language]));
|
||||
strings.set_delete_connection(
|
||||
UiText(localization::delete_connection[language]));
|
||||
strings.set_confirm_delete(
|
||||
UiText(localization::confirm_delete_connection[language]));
|
||||
strings.set_language(UiText(localization::language[language]));
|
||||
strings.set_video_quality(UiText(localization::video_quality[language]));
|
||||
strings.set_frame_rate(UiText(localization::video_frame_rate[language]));
|
||||
strings.set_codec(UiText(localization::video_encode_format[language]));
|
||||
strings.set_hardware_codec(
|
||||
UiText(localization::enable_hardware_video_codec[language]));
|
||||
strings.set_turn_relay(UiText(localization::enable_turn[language]));
|
||||
strings.set_srtp(UiText(localization::enable_srtp[language]));
|
||||
strings.set_self_hosted(
|
||||
UiText(localization::self_hosted_server_config[language]));
|
||||
strings.set_autostart(UiText(localization::enable_autostart[language]));
|
||||
strings.set_daemon(UiText(localization::enable_daemon[language]));
|
||||
strings.set_minimize_to_tray(
|
||||
UiText(localization::minimize_to_tray[language]));
|
||||
strings.set_file_save_path(
|
||||
UiText(localization::file_transfer_save_path[language]));
|
||||
strings.set_default_desktop(UiText(localization::default_desktop[language]));
|
||||
strings.set_server_host(
|
||||
UiText(localization::self_hosted_server_address[language]));
|
||||
strings.set_server_port(
|
||||
UiText(localization::self_hosted_server_port[language]));
|
||||
strings.set_coturn_port(
|
||||
UiText(localization::self_hosted_server_coturn_server_port[language]));
|
||||
strings.set_version(UiText(localization::version[language]));
|
||||
strings.set_signal_connected(
|
||||
UiText(localization::signal_connected[language]));
|
||||
strings.set_signal_disconnected(
|
||||
UiText(localization::signal_disconnected[language]));
|
||||
strings.set_tls_error(UiText(localization::signal_tls_cert_error[language]));
|
||||
strings.set_update_available(
|
||||
UiText(localization::new_version_available[language]));
|
||||
strings.set_release_notes(UiText(localization::release_notes[language]));
|
||||
strings.set_download(UiText(localization::update[language]));
|
||||
strings.set_input_password(UiText(localization::input_password[language]));
|
||||
strings.set_reinput_password(
|
||||
UiText(localization::reinput_password[language]));
|
||||
strings.set_remember_password(
|
||||
UiText(localization::remember_password[language]));
|
||||
strings.set_validate_password(
|
||||
UiText(localization::validate_password[language]));
|
||||
strings.set_request_permissions(
|
||||
UiText(localization::request_permissions[language]));
|
||||
strings.set_permission_required(
|
||||
UiText(localization::permission_required_message[language]));
|
||||
strings.set_screen_recording_permission(
|
||||
UiText(localization::screen_recording_permission[language]));
|
||||
strings.set_accessibility_permission(
|
||||
UiText(localization::accessibility_permission[language]));
|
||||
strings.set_service_setup_title(
|
||||
UiText(localization::windows_service_setup_title[language]));
|
||||
strings.set_service_setup_message(
|
||||
UiText(localization::windows_service_setup_message[language]));
|
||||
strings.set_service_settings_label(
|
||||
UiText(localization::windows_service_settings_label[language]));
|
||||
strings.set_install_service(
|
||||
UiText(localization::install_windows_service[language]));
|
||||
strings.set_service_installed(
|
||||
UiText(localization::windows_service_installed[language]));
|
||||
strings.set_do_not_remind(
|
||||
UiText(localization::do_not_remind_again[language]));
|
||||
strings.set_notification(UiText(localization::notification[language]));
|
||||
strings.set_service_suppressed_message(UiText(
|
||||
localization::windows_service_prompt_suppressed_message[language]));
|
||||
strings.set_quality_low(UiText(localization::video_quality_low[language]));
|
||||
strings.set_quality_medium(
|
||||
UiText(localization::video_quality_medium[language]));
|
||||
strings.set_quality_high(UiText(localization::video_quality_high[language]));
|
||||
strings.set_codec_h264(UiText(localization::h264[language]));
|
||||
strings.set_codec_av1(UiText(localization::av1[language]));
|
||||
strings.set_self_hosted_settings(
|
||||
UiText(localization::self_hosted_server_settings[language]));
|
||||
strings.set_access_website(UiText(localization::access_website[language]));
|
||||
strings.set_release_date_label(UiText(localization::release_date[language]));
|
||||
strings.set_later(UiText(localization::cancel[language]));
|
||||
const auto apply_stream_strings = [language](auto& stream_strings) {
|
||||
stream_strings.set_select_display(
|
||||
UiText(localization::select_display[language]));
|
||||
stream_strings.set_send_shortcut(
|
||||
UiText(localization::send_shortcut[language]));
|
||||
stream_strings.set_control_mouse(
|
||||
UiText(localization::control_mouse[language]));
|
||||
stream_strings.set_release_mouse(
|
||||
UiText(localization::release_mouse[language]));
|
||||
stream_strings.set_audio(UiText(localization::audio_capture[language]));
|
||||
stream_strings.set_mute(UiText(localization::mute[language]));
|
||||
stream_strings.set_select_file(UiText(localization::select_file[language]));
|
||||
stream_strings.set_show_stats(
|
||||
UiText(localization::show_net_traffic_stats[language]));
|
||||
stream_strings.set_hide_stats(
|
||||
UiText(localization::hide_net_traffic_stats[language]));
|
||||
stream_strings.set_fullscreen(UiText(localization::fullscreen[language]));
|
||||
stream_strings.set_exit_fullscreen(
|
||||
UiText(localization::exit_fullscreen[language]));
|
||||
stream_strings.set_disconnect(UiText(localization::disconnect[language]));
|
||||
stream_strings.set_file_transfer(
|
||||
UiText(localization::file_transfer_progress[language]));
|
||||
stream_strings.set_expand_control(
|
||||
UiText(localization::expand_control_bar[language]));
|
||||
stream_strings.set_collapse_control(
|
||||
UiText(localization::collapse_control_bar[language]));
|
||||
stream_strings.set_stats_in(UiText(localization::in[language]));
|
||||
stream_strings.set_stats_out(UiText(localization::out[language]));
|
||||
stream_strings.set_stats_loss_rate(
|
||||
UiText(localization::loss_rate[language]));
|
||||
stream_strings.set_stats_resolution(
|
||||
UiText(localization::resolution[language]));
|
||||
stream_strings.set_stats_connection_mode(
|
||||
UiText(localization::connection_mode[language]));
|
||||
};
|
||||
auto& main_stream_strings = ui_->main->global<ui::StreamStrings>();
|
||||
apply_stream_strings(main_stream_strings);
|
||||
ui_localization::ApplyMainWindowStrings(ui_->main, language);
|
||||
ui_localization::ApplyStreamWindowStrings(ui_->main, language);
|
||||
if (ui_->stream) {
|
||||
auto& active_stream_strings = (*ui_->stream)->global<ui::StreamStrings>();
|
||||
apply_stream_strings(active_stream_strings);
|
||||
ui_localization::ApplyStreamWindowStrings(*ui_->stream, language);
|
||||
}
|
||||
ui_->localized_language = language;
|
||||
}
|
||||
@@ -2306,13 +2009,6 @@ void GuiApplication::SyncMainWindow() {
|
||||
}
|
||||
|
||||
void GuiApplication::SyncConnectionDialog() {
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
if (ui_ && ui_->capture_mode &&
|
||||
(ui_->capture_page == "connection" ||
|
||||
ui_->capture_page == "connection-password")) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (!ui_ || !show_connection_status_window_) {
|
||||
if (ui_) {
|
||||
ui_->main->set_connection_dialog_open(false);
|
||||
@@ -2362,13 +2058,6 @@ void GuiApplication::SyncConnectionDialog() {
|
||||
}
|
||||
|
||||
void GuiApplication::SyncPlatformDialogs() {
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
if (ui_ && ui_->capture_mode &&
|
||||
(ui_->capture_page == "service" ||
|
||||
ui_->capture_page == "service-notice")) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
CheckPortableWindowsService();
|
||||
const PortableServiceInstallState state =
|
||||
@@ -2414,13 +2103,6 @@ void GuiApplication::SyncPlatformDialogs() {
|
||||
ui_->main->set_portable_service_suppressed_notice_open(false);
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
if (ui_->capture_mode) {
|
||||
const bool show_permission = ui_->capture_page == "permission";
|
||||
ui_->main->set_permission_dialog_open(show_permission);
|
||||
ui_->main->set_screen_recording_granted(!show_permission);
|
||||
ui_->main->set_accessibility_granted(!show_permission);
|
||||
return;
|
||||
}
|
||||
RefreshMacPermissionStatus(false);
|
||||
show_request_permission_window_ = !mac_screen_recording_permission_granted_ ||
|
||||
!mac_accessibility_permission_granted_;
|
||||
@@ -2505,6 +2187,9 @@ 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_);
|
||||
@@ -2516,7 +2201,15 @@ 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;
|
||||
@@ -2526,11 +2219,15 @@ 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;
|
||||
@@ -2543,12 +2240,30 @@ void GuiApplication::SyncStreamWindow() {
|
||||
(*ui_->stream)->set_srtp_enabled(enable_srtp_);
|
||||
#if defined(__APPLE__)
|
||||
if (ui_->stream_live_resize_configuration_attempts > 0) {
|
||||
if (ConfigureStreamWindowLiveResize()) {
|
||||
if (ConfigureStreamWindowLiveResize(
|
||||
(*ui_->stream)->window().appkit_view())) {
|
||||
ui_->stream_live_resize_configuration_attempts = 0;
|
||||
} else {
|
||||
--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_->stream_initial_position_attempts > 0) {
|
||||
if (PositionWindowAtCenter(
|
||||
@@ -2563,7 +2278,13 @@ void GuiApplication::SyncStreamWindow() {
|
||||
if (!has_sessions) {
|
||||
SetStreamKeyboardFocus(false);
|
||||
#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
|
||||
(*ui_->stream)->hide();
|
||||
ui_->stream.reset();
|
||||
@@ -2571,6 +2292,9 @@ void GuiApplication::SyncStreamWindow() {
|
||||
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();
|
||||
@@ -2620,6 +2344,10 @@ void GuiApplication::SyncStreamWindow() {
|
||||
if (ui_->tab_model_signature != tab_signature) {
|
||||
ui_->tab_model_signature = tab_signature;
|
||||
ui_->tab_model->set_vector(std::move(tabs));
|
||||
#if defined(__APPLE__)
|
||||
// Adding or removing a tab changes the native video's top inset.
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
#endif
|
||||
}
|
||||
|
||||
int selected_index = 0;
|
||||
@@ -2640,6 +2368,9 @@ void GuiApplication::SyncStreamWindow() {
|
||||
return;
|
||||
}
|
||||
const ConnectionStatus status = props->connection_status_.load();
|
||||
const bool waiting_for_frame =
|
||||
status == ConnectionStatus::Connected &&
|
||||
!(*ui_->stream)->get_has_frame();
|
||||
// Once the peer is connected, the frame-waiting hint replaces the
|
||||
// connection status. Keeping these states mutually exclusive also avoids a
|
||||
// transient overlap when both properties are synchronized in one tick.
|
||||
@@ -2650,7 +2381,7 @@ void GuiApplication::SyncStreamWindow() {
|
||||
: ConnectionStatusText(status, localization_language_index_)));
|
||||
(*ui_->stream)
|
||||
->set_receiving_text(UiText(
|
||||
status == ConnectionStatus::Connected
|
||||
waiting_for_frame
|
||||
? localization::receiving_screen[localization_language_index_]
|
||||
: std::string{}));
|
||||
(*ui_->stream)->set_mouse_control_enabled(props->control_mouse_);
|
||||
@@ -2788,6 +2519,51 @@ 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
|
||||
|
||||
std::shared_ptr<std::vector<unsigned char>> frame;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
@@ -2839,6 +2615,7 @@ void GuiApplication::SyncStreamVideoFrame() {
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
uint32_t texture_id = 0;
|
||||
{
|
||||
std::lock_guard lock(ui_->video_gl_mutex);
|
||||
@@ -2879,6 +2656,7 @@ void GuiApplication::SyncStreamVideoFrame() {
|
||||
++props->frame_count_;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(output_width,
|
||||
output_height);
|
||||
@@ -2897,6 +2675,41 @@ void GuiApplication::SyncStreamVideoFrame() {
|
||||
}
|
||||
|
||||
void GuiApplication::ConfigureStreamVideoRenderer() {
|
||||
#if defined(__APPLE__)
|
||||
if (!ui_ || !ui_->stream) {
|
||||
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;
|
||||
}
|
||||
@@ -2971,8 +2784,45 @@ void GuiApplication::ConfigureStreamVideoRenderer() {
|
||||
if (error.has_value()) {
|
||||
LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers");
|
||||
}
|
||||
#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;
|
||||
@@ -2986,9 +2836,21 @@ void GuiApplication::ScheduleNextVideoFrame() {
|
||||
}
|
||||
|
||||
if (now >= next_video_frame_time_) {
|
||||
if (video_frame_dirty_.exchange(false, std::memory_order_acq_rel)) {
|
||||
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();
|
||||
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);
|
||||
@@ -3262,24 +3124,50 @@ void GuiApplication::SelectStreamTab(int index) {
|
||||
return;
|
||||
}
|
||||
const std::string selected_remote_id = ui_->tab_ids[index];
|
||||
#if defined(__APPLE__)
|
||||
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;
|
||||
std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto& [id, props] : remote_sessions_) {
|
||||
if (props) {
|
||||
props->tab_selected_ = id == focused_remote_id_;
|
||||
}
|
||||
#if defined(__APPLE__)
|
||||
if (mac_metal_video_renderer_) {
|
||||
renderer_selection_changed =
|
||||
mac_metal_video_renderer_->SetSelectedStream(selected_remote_id);
|
||||
}
|
||||
const auto selected = remote_sessions_.find(selected_remote_id);
|
||||
start_keyboard_capturer_ = selected != remote_sessions_.end() &&
|
||||
selected->second &&
|
||||
selected->second->control_mouse_ &&
|
||||
selected->second->connection_status_.load() ==
|
||||
ConnectionStatus::Connected;
|
||||
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_);
|
||||
for (auto& [id, props] : remote_sessions_) {
|
||||
if (props) {
|
||||
props->tab_selected_ = id == focused_remote_id_;
|
||||
}
|
||||
}
|
||||
const auto selected = remote_sessions_.find(selected_remote_id);
|
||||
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;
|
||||
}
|
||||
#if defined(__APPLE__)
|
||||
if (renderer_selection_changed) {
|
||||
SubmitCachedFrameToMetal(selected_session);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) {
|
||||
@@ -3327,6 +3215,12 @@ void GuiApplication::CloseStreamTab(const std::string& remote_id) {
|
||||
if (focused_remote_id_ == remote_id) {
|
||||
focused_remote_id_.clear();
|
||||
controlled_remote_id_.clear();
|
||||
#if defined(__APPLE__)
|
||||
if (mac_metal_video_renderer_ &&
|
||||
mac_metal_video_renderer_->SetSelectedStream({})) {
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3520,6 +3414,15 @@ void GuiApplication::Cleanup() {
|
||||
WaitForThumbnailSaveTasks();
|
||||
devices_.DestroyAudioOutput();
|
||||
if (ui_->stream) {
|
||||
#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
|
||||
(*ui_->stream)->hide();
|
||||
ui_->stream.reset();
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ private:
|
||||
void SyncStreamVideoFrame();
|
||||
void ScheduleNextVideoFrame();
|
||||
void ConfigureStreamVideoRenderer();
|
||||
#if defined(__APPLE__)
|
||||
void SubmitCachedFrameToMetal(
|
||||
const std::shared_ptr<RemoteSession>& session);
|
||||
#endif
|
||||
void SyncStreamKeyboardFocus();
|
||||
void SetStreamKeyboardFocus(bool focused);
|
||||
void SyncServerWindow();
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#ifndef CROSSDESK_GUI_PLATFORM_METAL_VIDEO_RENDERER_H_
|
||||
#define CROSSDESK_GUI_PLATFORM_METAL_VIDEO_RENDERER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// macOS video compositor used by CrossDesk only. MiniRTC continues to expose
|
||||
// tightly packed CPU NV12 frames; this renderer performs the single required
|
||||
// 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 {
|
||||
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(const MacMetalVideoRenderer&) = delete;
|
||||
MacMetalVideoRenderer& operator=(const MacMetalVideoRenderer&) = delete;
|
||||
|
||||
bool IsReady() const;
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// Attaches a native CAMetalLayer-backed sibling below Slint's NSView and
|
||||
// restores the containing AppKit window's ordinary opaque titlebar.
|
||||
// |slint_view| is an NSView* kept opaque so this header remains valid C++.
|
||||
bool Attach(void* slint_view);
|
||||
void Detach();
|
||||
bool IsAttached() const;
|
||||
|
||||
// Cheap main-thread geometry check used by the UI frame timer. New decoded
|
||||
// frames are tracked by GuiRuntime's dirty flag; this covers native resize,
|
||||
// scale, stream selection, and retry work without a full session sync.
|
||||
bool NeedsSurfaceRedraw() const;
|
||||
|
||||
// Draws the newest queued frame for |remote_id|. top_inset_points reserves
|
||||
// Slint's tab strip above the native video surface. A rendered outcome
|
||||
// carries metadata from the exact slot submitted to the command buffer.
|
||||
RenderOutcome RenderLatest(std::string_view remote_id,
|
||||
double top_inset_points,
|
||||
bool native_titlebar_visible);
|
||||
|
||||
// Used only when a session closes, 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_METAL_VIDEO_RENDERER_H_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,14 @@ bool HideDisabledMainWindowZoomButton();
|
||||
|
||||
// Configures live resize and replaces the stream window's native Space
|
||||
// fullscreen action with an immediate, single-window fullscreen transition.
|
||||
bool ConfigureStreamWindowLiveResize();
|
||||
// |slint_view| is the current stream window's AppKit NSView*. Passing the
|
||||
// identity explicitly prevents a closing window with the same title from
|
||||
// being selected during a later connection.
|
||||
bool ConfigureStreamWindowLiveResize(void* slint_view);
|
||||
|
||||
// Drops the native stream-window reference if it belongs to |slint_view|.
|
||||
// This must run before the corresponding Slint component is destroyed.
|
||||
void UnregisterStreamWindow(void* slint_view);
|
||||
|
||||
// Returns whether the configured stream window is currently the active AppKit
|
||||
// key window.
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
NSWindow *stream_window = nil;
|
||||
__weak NSWindow *stream_window = nil;
|
||||
__weak NSView *stream_slint_view = nil;
|
||||
CrossDeskStreamFullscreenTarget *stream_fullscreen_target = nil;
|
||||
bool stream_fullscreen = false;
|
||||
NSRect stream_restore_frame = NSZeroRect;
|
||||
@@ -101,48 +102,63 @@ bool HideDisabledMainWindowZoomButton() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigureStreamWindowLiveResize() {
|
||||
bool ConfigureStreamWindowLiveResize(void *opaque_slint_view) {
|
||||
@autoreleasepool {
|
||||
for (NSWindow *window in [NSApp windows]) {
|
||||
if (![window.title isEqualToString:@"CrossDesk"]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSButton *zoom_button =
|
||||
[window standardWindowButton:NSWindowZoomButton];
|
||||
// The stream window is resizable and therefore keeps an enabled native
|
||||
// zoom button. The fixed-size main window's zoom button is disabled.
|
||||
if (zoom_button == nil || !zoom_button.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSView *content_view = window.contentView;
|
||||
if (content_view == nil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// During an ordinary edge resize, scale one cached frame and redraw once
|
||||
// the new window size has settled.
|
||||
window.preservesContentDuringLiveResize = YES;
|
||||
content_view.layerContentsRedrawPolicy =
|
||||
NSViewLayerContentsRedrawBeforeViewResize;
|
||||
content_view.layerContentsPlacement =
|
||||
NSViewLayerContentsPlacementScaleProportionallyToFit;
|
||||
stream_window = window;
|
||||
// Native Space fullscreen cross-fades an AppKit source snapshot over a
|
||||
// separately rendered destination window. Disable that path for the
|
||||
// stream window and route the green button to the immediate transition.
|
||||
window.collectionBehavior =
|
||||
(window.collectionBehavior &
|
||||
~(NSWindowCollectionBehaviorFullScreenPrimary |
|
||||
NSWindowCollectionBehaviorFullScreenAuxiliary |
|
||||
NSWindowCollectionBehaviorFullScreenAllowsTiling)) |
|
||||
NSWindowCollectionBehaviorFullScreenNone;
|
||||
InstallStreamFullscreenButton(window);
|
||||
content_view.needsDisplay = YES;
|
||||
return true;
|
||||
NSView *slint_view = (__bridge NSView *)opaque_slint_view;
|
||||
NSWindow *window = slint_view.window;
|
||||
if (window == nil) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
NSButton *zoom_button =
|
||||
[window standardWindowButton:NSWindowZoomButton];
|
||||
NSView *content_view = window.contentView;
|
||||
if (zoom_button == nil || !zoom_button.enabled || content_view == nil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// During an ordinary edge resize, scale one cached frame and redraw once
|
||||
// the new window size has settled.
|
||||
window.preservesContentDuringLiveResize = YES;
|
||||
content_view.layerContentsRedrawPolicy =
|
||||
NSViewLayerContentsRedrawBeforeViewResize;
|
||||
content_view.layerContentsPlacement =
|
||||
NSViewLayerContentsPlacementScaleProportionallyToFit;
|
||||
stream_window = window;
|
||||
stream_slint_view = slint_view;
|
||||
// Native Space fullscreen cross-fades an AppKit source snapshot over a
|
||||
// separately rendered destination window. Disable that path for the
|
||||
// stream window and route the green button to the immediate transition.
|
||||
window.collectionBehavior =
|
||||
(window.collectionBehavior &
|
||||
~(NSWindowCollectionBehaviorFullScreenPrimary |
|
||||
NSWindowCollectionBehaviorFullScreenAuxiliary |
|
||||
NSWindowCollectionBehaviorFullScreenAllowsTiling)) |
|
||||
NSWindowCollectionBehaviorFullScreenNone;
|
||||
InstallStreamFullscreenButton(window);
|
||||
content_view.needsDisplay = YES;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void UnregisterStreamWindow(void *opaque_slint_view) {
|
||||
@autoreleasepool {
|
||||
NSView *slint_view = (__bridge NSView *)opaque_slint_view;
|
||||
if (slint_view == nil || slint_view != stream_slint_view) {
|
||||
return;
|
||||
}
|
||||
stream_window = nil;
|
||||
stream_slint_view = nil;
|
||||
stream_fullscreen = false;
|
||||
stream_restore_frame = NSZeroRect;
|
||||
stream_restore_style_mask = NSWindowStyleMaskTitled;
|
||||
stream_restore_title_visibility = NSWindowTitleVisible;
|
||||
stream_restore_titlebar_transparent = NO;
|
||||
stream_restore_movable = YES;
|
||||
stream_restore_close_hidden = NO;
|
||||
stream_restore_miniaturize_hidden = NO;
|
||||
stream_restore_zoom_hidden = NO;
|
||||
stream_restore_presentation_options = NSApplicationPresentationDefault;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include "display_stream_id.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#ifdef __APPLE__
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
@@ -223,7 +226,27 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
#ifdef __APPLE__
|
||||
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
|
||||
}
|
||||
#ifdef __APPLE__
|
||||
if ((!frame_snapshot || frame_snapshot->empty()) &&
|
||||
mac_metal_video_renderer_) {
|
||||
auto metal_snapshot =
|
||||
std::make_shared<std::vector<unsigned char>>();
|
||||
if (mac_metal_video_renderer_->CopyLatestNv12(
|
||||
props->remote_id_, metal_snapshot.get(), &video_width,
|
||||
&video_height)) {
|
||||
frame_snapshot = std::move(metal_snapshot);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
||||
video_height > 0) {
|
||||
@@ -247,6 +270,13 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (mac_metal_video_renderer_) {
|
||||
mac_metal_video_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_);
|
||||
LeaveConnection(props->peer_, props->remote_id_.c_str());
|
||||
@@ -307,6 +337,12 @@ void GuiRuntime::ResetRemoteSessionResources(
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
#ifdef __APPLE__
|
||||
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;
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "localization.h"
|
||||
#ifdef __APPLE__
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
#ifdef __APPLE__
|
||||
class MacMetalVideoRenderer;
|
||||
#endif
|
||||
|
||||
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
|
||||
// state, but no window lifecycle, ImGui view, or transport callback methods.
|
||||
class GuiRuntime : protected gui_detail::GuiState {
|
||||
@@ -83,6 +87,9 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
KeyboardController keyboard_;
|
||||
PeerEventHandler peer_events_;
|
||||
std::atomic<bool> video_frame_dirty_{false};
|
||||
#ifdef __APPLE__
|
||||
std::unique_ptr<MacMetalVideoRenderer> mac_metal_video_renderer_;
|
||||
#endif
|
||||
|
||||
private:
|
||||
friend class ClipboardController;
|
||||
|
||||
@@ -10,11 +10,15 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "file_transfer.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#ifdef __APPLE__
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "runtime/remote_action_codec.h"
|
||||
@@ -312,16 +316,50 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
props->enable_mouse_control_ = false;
|
||||
runtime->ResetRemoteServiceStatus(*props);
|
||||
|
||||
#ifdef __APPLE__
|
||||
std::shared_ptr<std::vector<unsigned char>> metal_snapshot;
|
||||
int metal_snapshot_width = 0;
|
||||
int metal_snapshot_height = 0;
|
||||
bool needs_metal_snapshot = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
needs_metal_snapshot = !props->thumbnail_frame_ ||
|
||||
props->thumbnail_frame_->empty();
|
||||
}
|
||||
if (needs_metal_snapshot && runtime->mac_metal_video_renderer_) {
|
||||
auto snapshot = std::make_shared<std::vector<unsigned char>>();
|
||||
if (runtime->mac_metal_video_renderer_->CopyLatestNv12(
|
||||
remote_id, snapshot.get(), &metal_snapshot_width,
|
||||
&metal_snapshot_height)) {
|
||||
metal_snapshot = std::move(snapshot);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
#ifdef __APPLE__
|
||||
if (metal_snapshot &&
|
||||
(!props->thumbnail_frame_ || props->thumbnail_frame_->empty())) {
|
||||
props->thumbnail_frame_ = std::move(metal_snapshot);
|
||||
props->thumbnail_width_ = metal_snapshot_width;
|
||||
props->thumbnail_height_ = metal_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;
|
||||
}
|
||||
#ifdef __APPLE__
|
||||
if (runtime->mac_metal_video_renderer_) {
|
||||
runtime->mac_metal_video_renderer_->DiscardStream(remote_id);
|
||||
runtime->video_frame_dirty_.store(true,
|
||||
std::memory_order_release);
|
||||
}
|
||||
#endif
|
||||
|
||||
runtime->focus_on_stream_window_ = false;
|
||||
|
||||
|
||||
@@ -7,8 +7,20 @@
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/gui_runtime.h"
|
||||
#ifdef __APPLE__
|
||||
#include <chrono>
|
||||
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
#ifdef __APPLE__
|
||||
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,
|
||||
@@ -29,8 +41,66 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
runtime->remote_sessions_.find(remote_id)->second.get();
|
||||
|
||||
if (props->connection_established_) {
|
||||
#ifdef __APPLE__
|
||||
bool background_snapshot_only = false;
|
||||
if (runtime->mac_metal_video_renderer_ &&
|
||||
runtime->mac_metal_video_renderer_->IsReady() &&
|
||||
runtime->mac_metal_video_renderer_->IsAttached()) {
|
||||
const auto submit_result =
|
||||
runtime->mac_metal_video_renderer_->SubmitNv12(
|
||||
remote_id,
|
||||
reinterpret_cast<const uint8_t*>(video_frame->data),
|
||||
video_frame->size, video_frame->width, video_frame->height);
|
||||
if (submit_result == MacMetalVideoRenderer::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);
|
||||
if (size_changed) {
|
||||
props->render_rect_dirty_ = true;
|
||||
}
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
// Once the native renderer owns the current stream, do not leave an
|
||||
// older CPU frame ahead of the Metal close snapshot.
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
props->thumbnail_frame_.reset();
|
||||
props->thumbnail_width_ = 0;
|
||||
props->thumbnail_height_ = 0;
|
||||
props->background_snapshot_time_ = {};
|
||||
++props->video_frame_sequence_;
|
||||
props->streaming_ = true;
|
||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
if (submit_result == MacMetalVideoRenderer::SubmitResult::dropped ||
|
||||
submit_result == MacMetalVideoRenderer::SubmitResult::failed) {
|
||||
// Keep presenting the last Metal frame. A later decoded frame can
|
||||
// reuse the renderer without changing ownership mid-window.
|
||||
props->streaming_ = true;
|
||||
return;
|
||||
}
|
||||
if (submit_result == MacMetalVideoRenderer::SubmitResult::not_selected) {
|
||||
background_snapshot_only = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
#ifdef __APPLE__
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (background_snapshot_only && props->thumbnail_frame_ &&
|
||||
!props->thumbnail_frame_->empty() &&
|
||||
props->background_snapshot_time_ !=
|
||||
std::chrono::steady_clock::time_point{} &&
|
||||
now - props->background_snapshot_time_ <
|
||||
kBackgroundSnapshotInterval) {
|
||||
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) {
|
||||
@@ -55,10 +125,23 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
props->video_size_ = video_frame->size;
|
||||
|
||||
props->front_frame_.swap(props->back_frame_);
|
||||
#ifdef __APPLE__
|
||||
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;
|
||||
#ifdef __APPLE__
|
||||
if (background_snapshot_only) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,14 @@ 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_;
|
||||
#ifdef __APPLE__
|
||||
// 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;
|
||||
|
||||
@@ -116,10 +116,12 @@ export component StreamWindow inherits Window {
|
||||
min-width: 640px;
|
||||
min-height: 360px + (root.custom-titlebar ? 32px : 0px);
|
||||
no-frame: root.custom-titlebar;
|
||||
background: root.custom-titlebar ? transparent : black;
|
||||
background: root.native-video-enabled
|
||||
? transparent : root.custom-titlebar ? transparent : black;
|
||||
default-font-size: ImGuiFontStyle.base;
|
||||
|
||||
in property <bool> custom-titlebar: false;
|
||||
in property <bool> native-video-enabled: false;
|
||||
in property <bool> window-active: true;
|
||||
in property <bool> window-maximized: false;
|
||||
in property <[StreamTab]> tabs;
|
||||
@@ -263,7 +265,7 @@ export component StreamWindow inherits Window {
|
||||
window-surface := Rectangle {
|
||||
width: root.width;
|
||||
height: root.height;
|
||||
background: black;
|
||||
background: root.native-video-enabled ? transparent : black;
|
||||
border-radius:
|
||||
root.custom-titlebar && !root.fullscreen-enabled && !root.window-maximized
|
||||
? root.window-corner-radius : 0px;
|
||||
@@ -419,7 +421,7 @@ export component StreamWindow inherits Window {
|
||||
y: root.titlebar-height;
|
||||
width: parent.width;
|
||||
height: parent.height - self.y;
|
||||
background: black;
|
||||
background: root.native-video-enabled ? transparent : black;
|
||||
clip: true;
|
||||
|
||||
// Keep the ImGui tab strip behavior, including pointer-drag reordering.
|
||||
@@ -499,7 +501,7 @@ export component StreamWindow inherits Window {
|
||||
video := Rectangle {
|
||||
y: root.fullscreen-enabled ? 0px : (root.tabs.length > 1 ? 30px : 0px);
|
||||
height: parent.height - self.y;
|
||||
background: black;
|
||||
background: root.native-video-enabled ? transparent : black;
|
||||
clip: true;
|
||||
|
||||
frame-image := Image {
|
||||
@@ -507,7 +509,7 @@ export component StreamWindow inherits Window {
|
||||
height: parent.height;
|
||||
source: root.frame;
|
||||
image-fit: contain;
|
||||
visible: root.has-frame;
|
||||
visible: root.has-frame && !root.native-video-enabled;
|
||||
}
|
||||
|
||||
Text {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#ifndef CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
#define CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
|
||||
#include <slint.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "crossdesk_ui.h"
|
||||
#include "localization.h"
|
||||
|
||||
namespace crossdesk::ui_localization {
|
||||
|
||||
inline slint::SharedString Text(const std::string& value) {
|
||||
return slint::SharedString(value);
|
||||
}
|
||||
|
||||
inline int ApplyMainWindowStrings(
|
||||
const slint::ComponentHandle<ui::MainWindow>& window,
|
||||
int language_index) {
|
||||
const int language =
|
||||
localization::detail::ClampLanguageIndex(language_index);
|
||||
auto& strings = window->global<ui::UiStrings>();
|
||||
strings.set_local_desktop(Text(localization::local_desktop[language]));
|
||||
strings.set_remote_desktop(Text(localization::remote_desktop[language]));
|
||||
strings.set_recent_connections(
|
||||
Text(localization::recent_connections[language]));
|
||||
strings.set_local_id(Text(localization::local_id[language]));
|
||||
strings.set_device_name(Text(localization::device_name[language]));
|
||||
strings.set_remote_id(Text(localization::remote_id[language]));
|
||||
strings.set_online(Text(localization::online[language]));
|
||||
strings.set_offline(Text(localization::offline[language]));
|
||||
strings.set_password(Text(localization::password[language]));
|
||||
strings.set_copied(
|
||||
Text(localization::local_id_copied_to_clipboard[language]));
|
||||
strings.set_connect(Text(localization::connect[language]));
|
||||
strings.set_settings(Text(localization::settings[language]));
|
||||
strings.set_about(Text(localization::about[language]));
|
||||
strings.set_ok(Text(localization::ok[language]));
|
||||
strings.set_cancel(Text(localization::cancel[language]));
|
||||
strings.set_new_password(Text(localization::new_password[language]));
|
||||
strings.set_invalid_password(Text(localization::max_password_len[language]));
|
||||
strings.set_edit_alias(
|
||||
Text(localization::input_connection_alias[language]));
|
||||
strings.set_delete_connection(
|
||||
Text(localization::delete_connection[language]));
|
||||
strings.set_confirm_delete(
|
||||
Text(localization::confirm_delete_connection[language]));
|
||||
strings.set_language(Text(localization::language[language]));
|
||||
strings.set_video_quality(Text(localization::video_quality[language]));
|
||||
strings.set_frame_rate(Text(localization::video_frame_rate[language]));
|
||||
strings.set_codec(Text(localization::video_encode_format[language]));
|
||||
strings.set_hardware_codec(
|
||||
Text(localization::enable_hardware_video_codec[language]));
|
||||
strings.set_turn_relay(Text(localization::enable_turn[language]));
|
||||
strings.set_srtp(Text(localization::enable_srtp[language]));
|
||||
strings.set_self_hosted(
|
||||
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]));
|
||||
strings.set_server_host(
|
||||
Text(localization::self_hosted_server_address[language]));
|
||||
strings.set_server_port(
|
||||
Text(localization::self_hosted_server_port[language]));
|
||||
strings.set_coturn_port(
|
||||
Text(localization::self_hosted_server_coturn_server_port[language]));
|
||||
strings.set_version(Text(localization::version[language]));
|
||||
strings.set_signal_connected(Text(localization::signal_connected[language]));
|
||||
strings.set_signal_disconnected(
|
||||
Text(localization::signal_disconnected[language]));
|
||||
strings.set_tls_error(Text(localization::signal_tls_cert_error[language]));
|
||||
strings.set_update_available(
|
||||
Text(localization::new_version_available[language]));
|
||||
strings.set_release_notes(Text(localization::release_notes[language]));
|
||||
strings.set_download(Text(localization::update[language]));
|
||||
strings.set_input_password(Text(localization::input_password[language]));
|
||||
strings.set_reinput_password(Text(localization::reinput_password[language]));
|
||||
strings.set_remember_password(
|
||||
Text(localization::remember_password[language]));
|
||||
strings.set_validate_password(
|
||||
Text(localization::validate_password[language]));
|
||||
strings.set_request_permissions(
|
||||
Text(localization::request_permissions[language]));
|
||||
strings.set_permission_required(
|
||||
Text(localization::permission_required_message[language]));
|
||||
strings.set_screen_recording_permission(
|
||||
Text(localization::screen_recording_permission[language]));
|
||||
strings.set_accessibility_permission(
|
||||
Text(localization::accessibility_permission[language]));
|
||||
strings.set_service_setup_title(
|
||||
Text(localization::windows_service_setup_title[language]));
|
||||
strings.set_service_setup_message(
|
||||
Text(localization::windows_service_setup_message[language]));
|
||||
strings.set_service_settings_label(
|
||||
Text(localization::windows_service_settings_label[language]));
|
||||
strings.set_install_service(
|
||||
Text(localization::install_windows_service[language]));
|
||||
strings.set_service_installed(
|
||||
Text(localization::windows_service_installed[language]));
|
||||
strings.set_do_not_remind(
|
||||
Text(localization::do_not_remind_again[language]));
|
||||
strings.set_notification(Text(localization::notification[language]));
|
||||
strings.set_service_suppressed_message(
|
||||
Text(localization::windows_service_prompt_suppressed_message[language]));
|
||||
strings.set_quality_low(Text(localization::video_quality_low[language]));
|
||||
strings.set_quality_medium(
|
||||
Text(localization::video_quality_medium[language]));
|
||||
strings.set_quality_high(Text(localization::video_quality_high[language]));
|
||||
strings.set_codec_h264(Text(localization::h264[language]));
|
||||
strings.set_codec_av1(Text(localization::av1[language]));
|
||||
strings.set_self_hosted_settings(
|
||||
Text(localization::self_hosted_server_settings[language]));
|
||||
strings.set_access_website(Text(localization::access_website[language]));
|
||||
strings.set_release_date_label(Text(localization::release_date[language]));
|
||||
strings.set_later(Text(localization::cancel[language]));
|
||||
return language;
|
||||
}
|
||||
|
||||
template <typename Window>
|
||||
inline int ApplyStreamWindowStrings(
|
||||
const slint::ComponentHandle<Window>& window, int language_index) {
|
||||
const int language =
|
||||
localization::detail::ClampLanguageIndex(language_index);
|
||||
auto& strings = window->template global<ui::StreamStrings>();
|
||||
strings.set_select_display(Text(localization::select_display[language]));
|
||||
strings.set_send_shortcut(Text(localization::send_shortcut[language]));
|
||||
strings.set_control_mouse(Text(localization::control_mouse[language]));
|
||||
strings.set_release_mouse(Text(localization::release_mouse[language]));
|
||||
strings.set_audio(Text(localization::audio_capture[language]));
|
||||
strings.set_mute(Text(localization::mute[language]));
|
||||
strings.set_select_file(Text(localization::select_file[language]));
|
||||
strings.set_show_stats(
|
||||
Text(localization::show_net_traffic_stats[language]));
|
||||
strings.set_hide_stats(
|
||||
Text(localization::hide_net_traffic_stats[language]));
|
||||
strings.set_fullscreen(Text(localization::fullscreen[language]));
|
||||
strings.set_exit_fullscreen(Text(localization::exit_fullscreen[language]));
|
||||
strings.set_disconnect(Text(localization::disconnect[language]));
|
||||
strings.set_file_transfer(
|
||||
Text(localization::file_transfer_progress[language]));
|
||||
strings.set_expand_control(Text(localization::expand_control_bar[language]));
|
||||
strings.set_collapse_control(
|
||||
Text(localization::collapse_control_bar[language]));
|
||||
strings.set_stats_in(Text(localization::in[language]));
|
||||
strings.set_stats_out(Text(localization::out[language]));
|
||||
strings.set_stats_loss_rate(Text(localization::loss_rate[language]));
|
||||
strings.set_stats_resolution(Text(localization::resolution[language]));
|
||||
strings.set_stats_connection_mode(
|
||||
Text(localization::connection_mode[language]));
|
||||
return language;
|
||||
}
|
||||
|
||||
} // namespace crossdesk::ui_localization
|
||||
|
||||
#endif // CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+4
-2
@@ -32,6 +32,7 @@ package("slint")
|
||||
break
|
||||
end
|
||||
end
|
||||
local use_macos_metal = package:is_plat("macosx")
|
||||
local configs = {
|
||||
"-DSLINT_BUILD_TESTING=OFF",
|
||||
"-DSLINT_BUILD_EXAMPLES=OFF",
|
||||
@@ -41,10 +42,11 @@ package("slint")
|
||||
"-DSLINT_FEATURE_SYSTEM_TESTING=OFF",
|
||||
"-DSLINT_FEATURE_MCP=OFF",
|
||||
"-DSLINT_FEATURE_BACKEND_QT=OFF",
|
||||
"-DSLINT_FEATURE_RENDERER_SKIA=OFF",
|
||||
"-DSLINT_FEATURE_RENDERER_SKIA=" .. (use_macos_metal and "ON" or "OFF"),
|
||||
"-DSLINT_FEATURE_RENDERER_SKIA_OPENGL=OFF",
|
||||
"-DSLINT_FEATURE_RENDERER_SKIA_VULKAN=OFF",
|
||||
"-DSLINT_FEATURE_RENDERER_FEMTOVG=ON",
|
||||
"-DSLINT_FEATURE_RENDERER_FEMTOVG=" .. (use_macos_metal and "OFF" or "ON"),
|
||||
"-DSLINT_FEATURE_RENDERER_FEMTOVG_WGPU=OFF",
|
||||
"-DSLINT_FEATURE_RENDERER_SOFTWARE=ON",
|
||||
"-DSLINT_STYLE=fluent",
|
||||
"-DBUILD_SHARED_LIBS=ON"
|
||||
|
||||
+3
-3
@@ -84,8 +84,8 @@ function setup_platform_settings()
|
||||
add_ldflags("-Wl,-ld_classic")
|
||||
end
|
||||
add_cxflags("-Wno-unused-variable")
|
||||
add_frameworks("Cocoa", "OpenGL", "IOSurface", "ScreenCaptureKit",
|
||||
"AVFoundation", "CoreMedia", "CoreVideo", "CoreAudio",
|
||||
"AudioToolbox")
|
||||
add_frameworks("Cocoa", "Metal", "QuartzCore", "IOSurface",
|
||||
"ScreenCaptureKit", "AVFoundation", "CoreMedia", "CoreVideo",
|
||||
"CoreAudio", "AudioToolbox")
|
||||
end
|
||||
end
|
||||
|
||||
+4
-2
@@ -112,7 +112,8 @@ function setup_targets()
|
||||
add_cxxflags("/bigobj")
|
||||
end
|
||||
add_packages("slint")
|
||||
add_includedirs("src/gui/assets/fonts")
|
||||
add_includedirs("src/gui", "src/gui/assets/fonts",
|
||||
"src/gui/assets/localization")
|
||||
add_rules("slint")
|
||||
add_files("src/gui/ui/crossdesk_ui.slint")
|
||||
add_files("tests/slint_ui_smoke_test.cpp")
|
||||
@@ -280,7 +281,8 @@ function setup_targets()
|
||||
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/window_drag_mac.mm",
|
||||
"src/gui/platform/metal_video_renderer.mm")
|
||||
elseif is_os("linux") then
|
||||
add_links("GL")
|
||||
add_files("src/gui/platform/tray/linux_tray.cpp")
|
||||
|
||||
Reference in New Issue
Block a user