diff --git a/docs/gui-architecture.md b/docs/gui-architecture.md new file mode 100644 index 0000000..caa1b66 --- /dev/null +++ b/docs/gui-architecture.md @@ -0,0 +1,207 @@ +# GUI 目录结构与职责说明 + +本文档说明 `src/gui` 的目录结构、核心类型、依赖方向和代码归属规则,用于避免 `Render`、`GuiApplication` 或 `GuiRuntime` 再次演变成职责混杂的大类。 + +## 目录结构 + +```text +src/gui/ +├── render.h # 对外稳定入口 Render +├── render.cpp # 创建 GuiApplication 并转发 Run +├── application/ # SDL/ImGui 应用外壳 +│ ├── gui_application.h # GuiApplication 声明 +│ ├── gui_application.cpp # 初始化、主循环和清理 +│ ├── application_state.h # 窗口、交互和 UI 状态 +│ ├── sdl_event_dispatch.cpp # SDL 窗口及应用事件分发 +│ ├── sdl_events.cpp # 键盘和鼠标事件转换 +│ ├── window_lifecycle.cpp # 原生窗口及 ImGui 上下文生命周期 +│ └── window_rendering.cpp # 三类窗口的渲染流程 +├── runtime/ # 非界面的 GUI 运行时 +│ ├── gui_runtime.h/.cpp # 运行时协调接口与公共实现 +│ ├── connection_runtime.cpp # 连接、在线探测、超时及会话清理 +│ ├── windows_service_runtime.cpp # Windows 服务和安全桌面集成 +│ ├── mac_permission_runtime.mm # macOS 权限检查和系统设置调用 +│ ├── gui_state.h # ApplicationState 与 RuntimeState 组合点 +│ ├── runtime_state.h # 配置、连接、平台和通信状态 +│ ├── remote_session.h # 单个远端会话 RemoteSession +│ ├── device_presence_cache.h # 设备在线状态缓存 +│ ├── peer_event_handler.h/.cpp # 信令和连接状态回调 +│ ├── peer_media_callbacks.cpp # 视频和音频回调 +│ ├── peer_data_callbacks.cpp # 控制、剪贴板和文件数据回调 +│ └── remote_action_codec.h/.cpp # RemoteAction 编解码 +├── features/ # 可独立演进的功能模块 +│ ├── clipboard/ # 本地与远端剪贴板同步 +│ ├── devices/ # 媒体及输入设备生命周期 +│ ├── file_transfer/ # 文件队列、发送、确认和进度 +│ ├── input/ # 键盘状态、命令和超时处理 +│ └── settings/ # 配置缓存和最近连接别名 +├── views/ # ImGui 视图实现 +│ ├── panels/ # 主窗口内嵌面板 +│ ├── toolbars/ # 标题栏、状态栏和控制栏 +│ └── windows/ # 独立窗口和模态对话框 +├── platform/ # 原生桌面平台适配 +│ └── tray/ # Windows、macOS 和 Linux 托盘 +└── assets/ # 字体、图标、布局和本地化资源 +``` + +## 分层关系 + +```text +Render + └── GuiApplication + ├── SDL/ImGui 生命周期 + ├── views/panels / views/toolbars / views/windows + └── GuiRuntime + ├── SessionDeviceManager + ├── KeyboardController + ├── ClipboardController + ├── FileTransferManager + ├── SettingsManager + └── PeerEventHandler +``` + +依赖方向应保持从上向下: + +```text +公开入口 → 应用层 → 运行时协调层 → 功能模块 → 底层库 + ↘ 视图层 +``` + +底层功能模块不应反向依赖具体面板、工具栏或窗口。 + +## 核心类型 + +### Render + +`Render` 是应用其他部分可见的稳定入口,只负责: + +- 管理 `GuiApplication` 生命周期; +- 将 `Run()` 转发给 `GuiApplication`; +- 隔离 GUI 内部类型,避免实现细节扩散到其他模块。 + +不要向 `Render` 添加窗口状态、连接状态或业务方法。 + +### GuiApplication + +`GuiApplication` 是 SDL/ImGui 应用外壳,负责: + +- 初始化日志、配置、SDL 和功能模块; +- 创建、销毁原生窗口及 ImGui 上下文; +- 执行主事件循环; +- 分发 SDL 事件; +- 调用面板、工具栏和窗口的绘制方法; +- 在程序退出时按顺序清理资源。 + +它不应实现连接协议、文件传输、剪贴板或设备控制细节。 + +### GuiRuntime + +`GuiRuntime` 是 GUI 进程的非界面协调层,负责: + +- 初始化 MiniRTC Peer 及回调参数; +- 协调连接、远端会话和在线状态; +- 持有各功能 Manager/Controller; +- 处理跨功能模块的调用顺序; +- 提供平台集成功能所需的运行时上下文。 + +`GuiRuntime` 应保持为协调器。新的独立功能应优先创建 Manager 或 Controller,不应直接继续堆积到 `GuiRuntime`。 + +### RemoteSession + +`RemoteSession` 表示一个连接中或已连接的远端端点,其生命周期覆盖: + +- MiniRTC Peer 和连接状态; +- 视频帧、纹理和渲染区域; +- 远端显示器和控制栏状态; +- 音频、鼠标和键盘控制状态; +- 文件传输状态; +- Windows 服务及安全桌面状态。 + +`remote_sessions_` 是按远端 ID 索引的会话表。会话查找、连接和清理统一由运行时处理。 + +## 状态划分 + +状态定义按生命周期和使用范围划分: + +| 文件 | 状态范围 | +| --- | --- | +| `application/application_state.h` | SDL 窗口、渲染上下文、交互标志和 UI 显示状态 | +| `runtime/runtime_state.h` | 配置、Peer、连接表、在线探测和平台集成状态 | +| `runtime/remote_session.h` | 单个远端会话独占的连接、媒体、控制和文件状态 | +| `runtime/gui_state.h` | 仅组合 `ApplicationState` 与 `RuntimeState` | + +添加状态前应先判断其生命周期: + +- 只属于某个远端连接:放入 `RemoteSession`; +- 只属于窗口或 UI:放入 `ApplicationState` 对应子状态; +- 属于整个 GUI 运行期:放入 `RuntimeState` 对应子状态; +- 只属于某个功能模块:优先作为 Manager/Controller 的私有成员。 + +## 主要运行流程 + +### 启动 + +```text +Render::Run + → GuiApplication::Run + → 初始化路径、日志和配置 + → 初始化 SDL 和功能模块 + → GuiRuntime::CreateConnectionPeer + → 创建主窗口 + → GuiApplication::MainLoop +``` + +### 建立远端连接 + +```text +remote_peer_panel + → GuiRuntime::ConnectTo + → 在线状态探测 + → 创建或复用 RemoteSession + → MiniRTC JoinConnection + → PeerEventHandler 接收连接状态 + → 创建串流窗口并开始渲染 +``` + +### 接收远端数据 + +```text +MiniRTC callback + → PeerEventHandler + ├── peer_media_callbacks:视频、音频 + ├── peer_data_callbacks:控制、文件、剪贴板 + └── peer_event_handler:信令、连接状态、网络统计 + → 对应 Manager/Controller + → 更新 RemoteSession 或运行时状态 +``` + +### 退出和清理 + +```text +SDL quit / tray exit + → GuiApplication::Cleanup + → CloseAllRemoteSessions + → 停止设备及后台任务 + → 销毁 Peer + → 销毁 ImGui 上下文和 SDL 窗口 + → SDL_Quit +``` + +## 新代码归属规则 + +新增代码时按以下规则选择目录: + +| 功能 | 目录 | +| --- | --- | +| SDL 初始化、事件循环、窗口生命周期 | `application/` | +| 连接、会话、Peer 回调和平台运行时 | `runtime/` | +| 视频、音频、鼠标、键盘设备生命周期 | `features/devices/` | +| 键盘协议和按键状态 | `features/input/` | +| 剪贴板同步 | `features/clipboard/` | +| 文件传输 | `features/file_transfer/` | +| 配置持久化 | `features/settings/` | +| 主窗口内嵌区域 | `views/panels/` | +| 标题栏、状态栏、控制栏 | `views/toolbars/` | +| 独立窗口和对话框 | `views/windows/` | +| 系统托盘 | `platform/tray/` | +| 字体、图标、布局和本地化数据 | `assets/` | diff --git a/src/gui/application/application_state.h b/src/gui/application/application_state.h new file mode 100644 index 0000000..b3dc83e --- /dev/null +++ b/src/gui/application/application_state.h @@ -0,0 +1,228 @@ +#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_ +#define CROSSDESK_GUI_APPLICATION_STATE_H_ + +#include + +#include +#include +#include +#include +#include + +#include "imgui.h" + +#if _WIN32 +#include "platform/tray/win_tray.h" +#elif defined(__APPLE__) +#include "platform/tray/mac_tray.h" +#elif defined(__linux__) +#include "platform/tray/linux_tray.h" +#endif + +namespace crossdesk::gui_detail { + +struct MainWindowState { + float title_bar_width_ = 640; + float title_bar_height_ = 30; + float title_bar_button_width_ = 30; + float title_bar_button_height_ = 30; + + SDL_Window *main_window_ = nullptr; + SDL_Renderer *main_renderer_ = nullptr; + ImGuiContext *main_ctx_ = nullptr; + ImFont *main_windows_system_chinese_font_ = nullptr; + ImFont *stream_windows_system_chinese_font_ = nullptr; + ImFont *server_windows_system_chinese_font_ = nullptr; + bool exit_ = false; + const int sdl_refresh_ms_ = 16; +#if _WIN32 + std::unique_ptr tray_; +#elif defined(__APPLE__) + std::unique_ptr tray_; +#elif defined(__linux__) + std::unique_ptr tray_; +#endif + + bool main_window_minimized_ = false; + uint32_t last_main_minimize_request_tick_ = 0; + uint32_t last_stream_minimize_request_tick_ = 0; + int main_window_width_real_ = 720; + int main_window_height_real_ = 540; + float main_window_dpi_scaling_w_ = 1.0f; + float main_window_dpi_scaling_h_ = 1.0f; + float dpi_scale_ = 1.0f; + float main_window_width_default_ = 640; + float main_window_height_default_ = 480; + float main_window_width_ = 640; + float main_window_height_ = 480; + float main_window_width_last_ = 640; + float main_window_height_last_ = 480; + float local_window_width_ = 320; + float local_window_height_ = 235; + float remote_window_width_ = 320; + float remote_window_height_ = 235; + float local_child_window_width_ = 266; + float local_child_window_height_ = 180; + float remote_child_window_width_ = 266; + float remote_child_window_height_ = 180; + float main_window_text_y_padding_ = 10; + float main_child_window_x_padding_ = 27; + float main_child_window_y_padding_ = 45; + float status_bar_height_ = 22; + float connection_status_window_width_ = 200; + float connection_status_window_height_ = 150; + float notification_window_width_ = 200; + float notification_window_height_ = 80; + float about_window_width_ = 300; + float about_window_height_ = 170; + float update_notification_window_width_ = 400; + float update_notification_window_height_ = 320; + uint32_t STREAM_REFRESH_EVENT = 0; + uint32_t APP_EXIT_EVENT = 0; +}; + +// Application-global interaction flags that coordinate SDL events with media +// and remote-control devices. +struct InteractionState { + bool start_mouse_controller_ = false; + bool mouse_controller_is_started_ = false; + bool start_screen_capturer_ = false; + bool screen_capturer_is_started_ = false; + bool start_speaker_capturer_ = false; + bool speaker_capturer_is_started_ = false; + bool start_keyboard_capturer_ = false; + bool show_cursor_ = false; + bool keyboard_capturer_is_started_ = false; + bool keyboard_capturer_uses_sdl_events_ = false; + bool foucs_on_main_window_ = false; + bool focus_on_stream_window_ = false; + bool audio_capture_ = false; + int screen_width_ = 1280; + int screen_height_ = 720; + int selected_display_ = 0; + std::string connect_button_label_ = "Connect"; + char input_password_tmp_[7] = ""; + char input_password_[7] = ""; + std::string random_password_; + char new_password_[7] = ""; + char remote_id_display_[12] = ""; + unsigned char audio_buffer_[720]{}; + int audio_len_ = 0; + bool audio_buffer_fresh_ = false; + bool need_to_rejoin_ = false; + std::chrono::steady_clock::time_point last_rejoin_check_time_ = + std::chrono::steady_clock::now(); + bool just_created_ = false; + std::string controlled_remote_id_; + std::string focused_remote_id_; + std::string remote_client_id_; + SDL_Event last_mouse_event{}; +}; + +struct UpdateState { + nlohmann::json latest_version_info_ = nlohmann::json{}; + bool update_available_ = false; + std::string latest_version_; + std::string release_notes_; + bool show_new_version_icon_ = false; + bool show_new_version_icon_in_menu_ = true; + double new_version_icon_last_trigger_time_ = 0.0; + double new_version_icon_render_start_time_ = 0.0; +}; + +struct StreamWindowState { + SDL_Window *stream_window_ = nullptr; + SDL_Renderer *stream_renderer_ = nullptr; + ImGuiContext *stream_ctx_ = nullptr; + bool need_to_create_stream_window_ = false; + bool stream_window_created_ = false; + bool stream_window_inited_ = false; + bool window_maximized_ = false; + bool stream_window_grabbed_ = false; + bool control_mouse_ = false; + int stream_window_width_default_ = 1280; + int stream_window_height_default_ = 720; + float stream_window_width_ = 1280; + float stream_window_height_ = 720; + SDL_PixelFormat stream_pixformat_ = SDL_PIXELFORMAT_NV12; + int stream_window_width_real_ = 1280; + int stream_window_height_real_ = 720; + float stream_window_dpi_scaling_w_ = 1.0f; + float stream_window_dpi_scaling_h_ = 1.0f; +}; + +struct ServerWindowState { + SDL_Window *server_window_ = nullptr; + SDL_Renderer *server_renderer_ = nullptr; + ImGuiContext *server_ctx_ = nullptr; + bool need_to_create_server_window_ = false; + bool need_to_destroy_server_window_ = false; + bool server_window_created_ = false; + bool server_window_inited_ = false; + int server_window_width_default_ = 250; + int server_window_height_default_ = 150; + float server_window_width_ = 250; + float server_window_height_ = 150; + float server_window_title_bar_height_ = 30.0f; + SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12; + int server_window_normal_width_ = 250; + int server_window_normal_height_ = 150; + float server_window_dpi_scaling_w_ = 1.0f; + float server_window_dpi_scaling_h_ = 1.0f; + float window_rounding_ = 6.0f; + float window_rounding_default_ = 6.0f; + bool server_window_collapsed_ = false; + bool server_window_collapsed_dragging_ = false; + float server_window_collapsed_drag_start_mouse_x_ = 0.0f; + float server_window_collapsed_drag_start_mouse_y_ = 0.0f; + int server_window_collapsed_drag_start_win_x_ = 0; + int server_window_collapsed_drag_start_win_y_ = 0; + bool server_window_dragging_ = false; + float server_window_drag_start_mouse_x_ = 0.0f; + float server_window_drag_start_mouse_y_ = 0.0f; + int server_window_drag_start_win_x_ = 0; + int server_window_drag_start_win_y_ = 0; +}; + +struct UiState { + bool label_inited_ = false; + bool connect_button_pressed_ = false; + bool password_validating_ = false; + uint32_t password_validating_time_ = 0; + bool show_settings_window_ = false; + bool show_self_hosted_server_config_window_ = false; + bool rejoin_ = false; + bool local_id_copied_ = false; + bool show_password_ = true; + bool show_about_window_ = false; + bool show_connection_status_window_ = false; + bool show_reset_password_window_ = false; + bool show_update_notification_window_ = false; + bool fullscreen_button_pressed_ = false; + bool focus_on_input_widget_ = true; + bool is_client_mode_ = false; + bool is_server_mode_ = false; + bool reload_recent_connections_ = true; + bool show_confirm_delete_connection_ = false; + bool show_edit_connection_alias_window_ = false; + bool show_offline_warning_window_ = false; + bool delete_connection_ = false; + bool is_tab_bar_hovered_ = false; + std::string delete_connection_name_; + std::string edit_connection_alias_remote_id_; + char edit_connection_alias_[128] = ""; + std::string offline_warning_text_; + bool re_enter_remote_id_ = false; + double copy_start_time_ = 0; +}; + +struct ApplicationState : MainWindowState, + InteractionState, + UpdateState, + StreamWindowState, + ServerWindowState, + UiState {}; + +} // namespace crossdesk::gui_detail + +#endif // CROSSDESK_GUI_APPLICATION_STATE_H_ diff --git a/src/gui/application/gui_application.cpp b/src/gui/application/gui_application.cpp new file mode 100644 index 0000000..844bc87 --- /dev/null +++ b/src/gui/application/gui_application.cpp @@ -0,0 +1,295 @@ +#include "application/gui_application.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "localization.h" +#include "rd_log.h" +#include "version_checker.h" + +namespace crossdesk { + +GuiApplication::GuiApplication() = default; + +GuiApplication::~GuiApplication() = default; + +int GuiApplication::Run() { + path_manager_ = std::make_unique("CrossDesk"); + if (path_manager_) { + exec_log_path_ = path_manager_->GetLogPath().string(); + dll_log_path_ = path_manager_->GetLogPath().string(); + cache_path_ = path_manager_->GetCachePath().string(); + config_center_ = + std::make_unique(cache_path_ + "/config.ini"); + strncpy(signal_server_ip_self_, + config_center_->GetSignalServerHost().c_str(), + sizeof(signal_server_ip_self_) - 1); + signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0'; + int signal_port_init = config_center_->GetSignalServerPort(); + if (signal_port_init > 0) { + strncpy(signal_server_port_self_, + std::to_string(signal_port_init).c_str(), + sizeof(signal_server_port_self_) - 1); + signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0'; + } else { + signal_server_port_self_[0] = '\0'; + } + } else { + std::cerr << "Failed to create PathManager" << std::endl; + return -1; + } + + InitializeLogger(); + LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION); + + latest_version_info_ = CheckUpdate(); + if (!latest_version_info_.empty()) { + std::string version; + if (latest_version_info_.contains("latest_version") && + latest_version_info_["latest_version"].is_string()) { + version = latest_version_info_["latest_version"].get(); + } else if (latest_version_info_.contains("version") && + latest_version_info_["version"].is_string()) { + version = latest_version_info_["version"].get(); + } + + if (!version.empty()) { + latest_version_ = 'v' + version; + } else { + latest_version_ = ""; + } + if (latest_version_info_.contains("releaseNotes") && + latest_version_info_["releaseNotes"].is_string()) { + release_notes_ = latest_version_info_["releaseNotes"].get(); + } else { + release_notes_ = ""; + } + update_available_ = + !version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_); + LOG_INFO("Update check: current={}, latest={}, available={}", + CROSSDESK_VERSION, latest_version_, update_available_); + if (update_available_) { + show_update_notification_window_ = true; + } + } else { + latest_version_ = ""; + update_available_ = false; + LOG_WARN("Update check skipped: version.json is empty or missing " + "latest_version"); + } + + InitializeSettings(); + InitializeSDL(); + InitializeModules(); + InitializeMainWindow(); + +#if _WIN32 && CROSSDESK_PORTABLE + CheckPortableWindowsService(); +#endif + + const int scaled_video_width_ = 160; + const int scaled_video_height_ = 90; + + MainLoop(); + + Cleanup(); + + return 0; +} + +void GuiApplication::InitializeLogger() { InitLogger(exec_log_path_); } + +void GuiApplication::InitializeSettings() { + settings_.Load(); + settings_.LoadRecentConnectionAliases(); + + localization_language_index_ = + localization::detail::ClampLanguageIndex(language_button_value_); + language_button_value_ = localization_language_index_; + + if (localization_language_index_ == 0) { + localization_language_ = ConfigCenter::LANGUAGE::CHINESE; + } else if (localization_language_index_ == 1) { + localization_language_ = ConfigCenter::LANGUAGE::ENGLISH; + } else { + localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN; + } +} + +void GuiApplication::InitializeSDL() { +#if defined(__linux__) && !defined(__APPLE__) + if (!getenv("SDL_AUDIODRIVER")) { + // Prefer PulseAudio first on Linux to avoid hard ALSA plugin dependency. + setenv("SDL_AUDIODRIVER", "pulseaudio", 0); + } +#endif + + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { + LOG_ERROR("Error: {}", SDL_GetError()); + return; + } + + const SDL_DisplayMode *dm = SDL_GetCurrentDisplayMode(0); + if (dm) { + screen_width_ = dm->w; + screen_height_ = dm->h; + } + + const uint32_t custom_event_base = SDL_RegisterEvents(3); + if (custom_event_base == static_cast(-1)) { + LOG_ERROR("Failed to register custom SDL events"); + } else { + STREAM_REFRESH_EVENT = custom_event_base; + APP_EXIT_EVENT = custom_event_base + 1; + clipboard_.SetEventType(custom_event_base + 2); + } + + LOG_INFO("Screen resolution: [{}x{}]", screen_width_, screen_height_); +} + +void GuiApplication::InitializeModules() { + if (!modules_inited_) { + devices_.Initialize(); + CreateConnectionPeer(); + + modules_inited_ = true; + } +} + +void GuiApplication::InitializeMainWindow() { + CreateMainWindow(); + clipboard_.Initialize(); + if (SDL_WINDOW_HIDDEN & SDL_GetWindowFlags(main_window_)) { + SDL_ShowWindow(main_window_); + } +} + +void GuiApplication::MainLoop() { + while (!exit_) { + if (!peer_) { + CreateConnectionPeer(); + } + + SDL_Event event; + if (SDL_WaitEventTimeout(&event, sdl_refresh_ms_)) { + ProcessSdlEvent(event); + } + // Also drain the pending value after a timeout so a full SDL event queue + // cannot indefinitely delay a clipboard update received from the network. + clipboard_.ApplyPendingRemoteText(); + +#if _WIN32 + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } +#elif defined(__linux__) && !defined(__APPLE__) + if (tray_) { + tray_->ProcessEvents(); + } +#endif + + UpdateLabels(); + HandleRecentConnections(); + HandleConnectionStatusChange(); + HandlePendingPresenceProbe(); + HandleConnectionTimeouts(); + HandleStreamWindow(); + HandleServerWindow(); + HandleWindowsServiceIntegration(); + + const bool main_window_visible = + main_window_ && !(SDL_GetWindowFlags(main_window_) & SDL_WINDOW_HIDDEN); + if (main_window_visible) { + DrawMainWindow(); + } + if (stream_window_inited_) { + DrawStreamWindow(); + } + + if (is_server_mode_) { + DrawServerWindow(); + } + + devices_.UpdateInteractions(); + } +} + +void GuiApplication::HandleStreamWindow() { + if (need_to_create_stream_window_) { + CreateStreamWindow(); + need_to_create_stream_window_ = false; + } + + if (stream_window_inited_) { + if (!stream_window_grabbed_ && control_mouse_) { + SDL_SetWindowMouseGrab(stream_window_, true); + stream_window_grabbed_ = true; + } else if (stream_window_grabbed_ && !control_mouse_) { + SDL_SetWindowMouseGrab(stream_window_, false); + stream_window_grabbed_ = false; + } + } +} + +void GuiApplication::HandleServerWindow() { + if (need_to_create_server_window_) { + CreateServerWindow(); + need_to_create_server_window_ = false; + } + + if (need_to_destroy_server_window_) { + DestroyServerWindow(); + DestroyServerWindowContext(); + need_to_destroy_server_window_ = false; + } +} + +void GuiApplication::Cleanup() { + clipboard_.Shutdown(); + + devices_.DestroyDevices(); + devices_.DestroyFactories(); + CloseAllRemoteSessions(); + +#if _WIN32 && CROSSDESK_PORTABLE + JoinPortableWindowsServiceInstallThread(); +#endif + + WaitForThumbnailSaveTasks(); + + devices_.DestroyAudioOutput(); +#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) + tray_.reset(); +#endif + + if (stream_window_created_) { + if (stream_window_) { + SDL_SetWindowMouseGrab(stream_window_, false); + } + DestroyStreamWindow(); + } + if (stream_ctx_) { + DestroyStreamWindowContext(); + } + + if (server_window_created_) { + DestroyServerWindow(); + } + if (server_ctx_) { + DestroyServerWindowContext(); + } + + DestroyMainWindowContext(); + DestroyMainWindow(); + SDL_Quit(); +} + + +} // namespace crossdesk diff --git a/src/gui/application/gui_application.h b/src/gui/application/gui_application.h new file mode 100644 index 0000000..1785f19 --- /dev/null +++ b/src/gui/application/gui_application.h @@ -0,0 +1,112 @@ +#ifndef CROSSDESK_GUI_APPLICATION_H_ +#define CROSSDESK_GUI_APPLICATION_H_ + +#include +#include +#include + +#include "IconsFontAwesome6.h" +#include "imgui_impl_sdl3.h" +#include "imgui_impl_sdlrenderer3.h" +#include "imgui_internal.h" +#include "runtime/gui_runtime.h" + +namespace crossdesk { + +// SDL/ImGui application shell. Feature state and protocol behavior live in +// GuiRuntime and its composed controllers. +class GuiApplication final : private GuiRuntime { +public: + GuiApplication(); + ~GuiApplication(); + + int Run(); + +private: + // Native window integration. + static SDL_HitTestResult HitTestCallback(SDL_Window *window, + const SDL_Point *area, void *data); + + // Application lifecycle. + void InitializeLogger(); + void InitializeSettings(); + void InitializeSDL(); + void InitializeModules(); + void InitializeMainWindow(); + void MainLoop(); + void HandleStreamWindow(); + void HandleServerWindow(); + void Cleanup(); + + // Window lifecycle and rendering. + int CreateMainWindow(); + int DestroyMainWindow(); + int CreateStreamWindow(); + int DestroyStreamWindow(); + int CreateServerWindow(); + int DestroyServerWindow(); + int SetupFontAndStyle(ImFont **system_chinese_font_out); + int DestroyMainWindowContext(); + int DestroyStreamWindowContext(); + int DestroyServerWindowContext(); + int DrawMainWindow(); + int DrawStreamWindow(); + int DrawServerWindow(); + + // Views and dialogs. + int TitleBar(bool main_window); + int MainWindow(); + int UpdateNotificationWindow(); + int StreamWindow(); + int ServerWindow(); + int RemoteClientInfoWindow(); + int LocalWindow(); + int RemoteWindow(); + int RecentConnectionsWindow(); + int SettingWindow(); + int SelfHostedServerWindow(); + int ControlWindow(std::shared_ptr &props); + int ControlBar(std::shared_ptr &props); + int AboutWindow(); + int StatusBar(); + bool + ConnectionStatusWindow(std::shared_ptr &props); + int ShowRecentConnections(); + int ConfirmDeleteConnection(); + int EditRecentConnectionAliasWindow(); + int OfflineWarningWindow(); + int NetTrafficStats(std::shared_ptr &props); + int FileTransferWindow(std::shared_ptr &props); + void + DrawConnectionStatusText(std::shared_ptr &props); + void + DrawReceivingScreenText(std::shared_ptr &props); + bool OpenUrl(const std::string &url); + void Hyperlink(const std::string &label, const std::string &url, + float window_width); + std::string OpenFileDialog(std::string title); + bool MinimizeMainWindowToTray(); + + // SDL event dispatch and input translation. + void UpdateRenderRect(); + void ProcessSdlEvent(const SDL_Event &event); + int ProcessKeyboardEvent(const SDL_Event &event); + int ProcessMouseEvent(const SDL_Event &event); + void CloseTab(decltype(remote_sessions_)::iterator &it); + +#if _WIN32 && CROSSDESK_PORTABLE + void CheckPortableWindowsService(); + int PortableServiceInstallWindow(); + void StartPortableWindowsServiceInstall(); + void JoinPortableWindowsServiceInstallThread(); +#endif + +#ifdef __APPLE__ + int RequestPermissionWindow(); + bool DrawToggleSwitch(const char *id, bool active, bool enabled); +#endif +}; + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_APPLICATION_H_ diff --git a/src/gui/application/sdl_event_dispatch.cpp b/src/gui/application/sdl_event_dispatch.cpp new file mode 100644 index 0000000..9503529 --- /dev/null +++ b/src/gui/application/sdl_event_dispatch.cpp @@ -0,0 +1,320 @@ +#include "application/gui_application.h" + +#include +#include +#include +#include +#include + +#include "rd_log.h" + +namespace crossdesk { + +void GuiApplication::ProcessSdlEvent(const SDL_Event &event) { + if (main_ctx_) { + ImGui::SetCurrentContext(main_ctx_); + ImGui_ImplSDL3_ProcessEvent(&event); + } else { + LOG_ERROR("Main context is null"); + return; + } + + if (stream_window_inited_) { + if (stream_ctx_) { + ImGui::SetCurrentContext(stream_ctx_); + ImGui_ImplSDL3_ProcessEvent(&event); + } else { + LOG_ERROR("Stream context is null"); + return; + } + } + + if (server_window_inited_) { + if (server_ctx_) { + ImGui::SetCurrentContext(server_ctx_); + ImGui_ImplSDL3_ProcessEvent(&event); + } else { + LOG_ERROR("Server context is null"); + return; + } + } + + if (APP_EXIT_EVENT != 0 && event.type == APP_EXIT_EVENT) { + LOG_INFO("Quit program from system tray"); + if (stream_window_) { + SDL_SetWindowMouseGrab(stream_window_, false); + } +#if defined(__linux__) && !defined(__APPLE__) + if (tray_) { + tray_->RemoveTrayIcon(); + } +#endif + exit_ = true; + return; + } + + if (clipboard_.event_type() != 0 && event.type == clipboard_.event_type()) { + clipboard_.ApplyPendingRemoteText(); + return; + } + + switch (event.type) { + case SDL_EVENT_QUIT: + if (stream_window_inited_) { + LOG_INFO("Destroy stream window"); + SDL_SetWindowMouseGrab(stream_window_, false); + DestroyStreamWindow(); + DestroyStreamWindowContext(); + + { + // std::shared_lock lock(remote_sessions_mutex_); + for (auto &[host_name, props] : remote_sessions_) { + std::shared_ptr> frame_snapshot; + int video_width = 0; + int video_height = 0; + { + std::lock_guard lock(props->video_frame_mutex_); + frame_snapshot = props->front_frame_; + video_width = props->video_width_; + video_height = props->video_height_; + } + if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 && + video_height > 0) { + thumbnail_->SaveToThumbnail( + (char *)frame_snapshot->data(), video_width, video_height, + host_name, props->remote_host_name_, + props->remember_password_ ? props->remote_password_ : ""); + } + + if (props->peer_) { + std::string client_id = (host_name == client_id_) + ? "C-" + std::string(client_id_) + : client_id_; + LOG_INFO("[{}] Leave connection [{}]", client_id, host_name); + LeaveConnection(props->peer_, host_name.c_str()); + LOG_INFO("Destroy peer [{}]", client_id); + DestroyPeer(&props->peer_); + } + + props->streaming_ = false; + props->remember_password_ = false; + props->connection_established_ = false; + props->audio_capture_button_pressed_ = false; + + memset(&props->net_traffic_stats_, 0, + sizeof(props->net_traffic_stats_)); + SDL_SetWindowFullscreen(main_window_, false); + SDL_FlushEvents(STREAM_REFRESH_EVENT, STREAM_REFRESH_EVENT); + memset(audio_buffer_, 0, 720); + } + } + + { + // std::unique_lock lock(remote_sessions_mutex_); + remote_sessions_.clear(); + } + + rejoin_ = false; + is_client_mode_ = false; + reload_recent_connections_ = true; + fullscreen_button_pressed_ = false; + start_keyboard_capturer_ = false; + just_created_ = false; + recent_connection_image_save_time_ = SDL_GetTicks(); + } else { + LOG_INFO("Quit program"); + exit_ = true; + } + break; + + case SDL_EVENT_WINDOW_CLOSE_REQUESTED: + if (stream_window_ && + event.window.windowID == SDL_GetWindowID(stream_window_)) { + break; + } + + if (main_window_ && + event.window.windowID == SDL_GetWindowID(main_window_) && + MinimizeMainWindowToTray()) { + break; + } + + exit_ = true; + break; + + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + if (stream_window_created_ && + event.window.windowID == SDL_GetWindowID(stream_window_)) { + UpdateRenderRect(); + } + break; + + case SDL_EVENT_WINDOW_FOCUS_GAINED: + if (stream_window_ && + SDL_GetWindowID(stream_window_) == event.window.windowID) { + focus_on_stream_window_ = true; + } else if (main_window_ && + SDL_GetWindowID(main_window_) == event.window.windowID) { + foucs_on_main_window_ = true; + } + break; + + case SDL_EVENT_WINDOW_FOCUS_LOST: + if (stream_window_ && + SDL_GetWindowID(stream_window_) == event.window.windowID) { + keyboard_.ForceReleasePressedKeys(); + focus_on_stream_window_ = false; + } else if (main_window_ && + SDL_GetWindowID(main_window_) == event.window.windowID) { + foucs_on_main_window_ = false; + } + break; + case SDL_EVENT_DROP_FILE: + transfers_.HandleDropEvent(event); + break; + + case SDL_EVENT_CLIPBOARD_UPDATE: + clipboard_.HandleLocalUpdate(); + break; + + case SDL_EVENT_MOUSE_MOTION: + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + case SDL_EVENT_MOUSE_WHEEL: { + Uint32 mouse_window_id = 0; + if (event.type == SDL_EVENT_MOUSE_MOTION) { + mouse_window_id = event.motion.windowID; + } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || + event.type == SDL_EVENT_MOUSE_BUTTON_UP) { + mouse_window_id = event.button.windowID; + } else if (event.type == SDL_EVENT_MOUSE_WHEEL) { + mouse_window_id = event.wheel.windowID; + } + + if (focus_on_stream_window_ && stream_window_ && + SDL_GetWindowID(stream_window_) == mouse_window_id) { + ProcessMouseEvent(event); + } + break; + } + + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ && + focus_on_stream_window_ && stream_window_ && + SDL_GetWindowID(stream_window_) == event.key.windowID) { + ProcessKeyboardEvent(event); + } + break; + + default: + if (event.type == STREAM_REFRESH_EVENT) { + auto *props = static_cast(event.user.data1); + if (!props) { + break; + } + std::shared_ptr> frame_snapshot; + int video_width = 0; + int video_height = 0; + bool render_rect_dirty = false; + bool cleanup_pending = false; + { + std::lock_guard lock(props->video_frame_mutex_); + cleanup_pending = props->stream_cleanup_pending_; + if (!cleanup_pending) { + frame_snapshot = props->front_frame_; + video_width = props->video_width_; + video_height = props->video_height_; + } + render_rect_dirty = props->render_rect_dirty_; + } + + if (cleanup_pending) { + if (props->stream_texture_) { + SDL_DestroyTexture(props->stream_texture_); + props->stream_texture_ = nullptr; + } + { + std::lock_guard lock(props->video_frame_mutex_); + props->stream_cleanup_pending_ = false; + } + + if (render_rect_dirty) { + UpdateRenderRect(); + std::lock_guard lock(props->video_frame_mutex_); + props->render_rect_dirty_ = false; + } + break; + } + + if (video_width <= 0 || video_height <= 0) { + break; + } + if (!frame_snapshot || frame_snapshot->empty()) { + break; + } + + if (props->stream_texture_) { + if (video_width != props->texture_width_ || + video_height != props->texture_height_) { + props->texture_width_ = video_width; + props->texture_height_ = video_height; + + SDL_DestroyTexture(props->stream_texture_); + // props->stream_texture_ = SDL_CreateTexture( + // stream_renderer_, stream_pixformat_, + // SDL_TEXTUREACCESS_STREAMING, props->texture_width_, + // props->texture_height_); + + SDL_PropertiesID nvProps = SDL_CreateProperties(); + SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, + props->texture_width_); + SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, + props->texture_height_); + SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, + SDL_PIXELFORMAT_NV12); + SDL_SetNumberProperty(nvProps, + SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, + SDL_COLORSPACE_BT601_LIMITED); + props->stream_texture_ = + SDL_CreateTextureWithProperties(stream_renderer_, nvProps); + SDL_DestroyProperties(nvProps); + } + } else { + props->texture_width_ = video_width; + props->texture_height_ = video_height; + // props->stream_texture_ = SDL_CreateTexture( + // stream_renderer_, stream_pixformat_, + // SDL_TEXTUREACCESS_STREAMING, props->texture_width_, + // props->texture_height_); + + SDL_PropertiesID nvProps = SDL_CreateProperties(); + SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, + props->texture_width_); + SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, + props->texture_height_); + SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, + SDL_PIXELFORMAT_NV12); + SDL_SetNumberProperty(nvProps, + SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, + SDL_COLORSPACE_BT601_LIMITED); + props->stream_texture_ = + SDL_CreateTextureWithProperties(stream_renderer_, nvProps); + SDL_DestroyProperties(nvProps); + } + + SDL_UpdateTexture(props->stream_texture_, NULL, frame_snapshot->data(), + props->texture_width_); + + if (render_rect_dirty) { + UpdateRenderRect(); + std::lock_guard lock(props->video_frame_mutex_); + props->render_rect_dirty_ = false; + } + } + break; + } +} + + +} // namespace crossdesk diff --git a/src/gui/application/sdl_events.cpp b/src/gui/application/sdl_events.cpp new file mode 100644 index 0000000..4346e25 --- /dev/null +++ b/src/gui/application/sdl_events.cpp @@ -0,0 +1,415 @@ +#include "application/gui_application.h" + +#include +#include +#include + +#include "device_controller.h" +#include "rd_log.h" + +namespace crossdesk { +namespace { +int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent &event) { + const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0; + + switch (event.scancode) { + case SDL_SCANCODE_NUMLOCKCLEAR: + return 0x90; + case SDL_SCANCODE_KP_ENTER: + return 0x0D; + case SDL_SCANCODE_KP_0: + if (!numlock_enabled) { + return 0x2D; + } + return 0x60; + case SDL_SCANCODE_KP_1: + if (!numlock_enabled) { + return 0x23; + } + return 0x61; + case SDL_SCANCODE_KP_2: + if (!numlock_enabled) { + return 0x28; + } + return 0x62; + case SDL_SCANCODE_KP_3: + if (!numlock_enabled) { + return 0x22; + } + return 0x63; + case SDL_SCANCODE_KP_4: + if (!numlock_enabled) { + return 0x25; + } + return 0x64; + case SDL_SCANCODE_KP_5: + return 0x65; + case SDL_SCANCODE_KP_6: + if (!numlock_enabled) { + return 0x27; + } + return 0x66; + case SDL_SCANCODE_KP_7: + if (!numlock_enabled) { + return 0x24; + } + return 0x67; + case SDL_SCANCODE_KP_8: + if (!numlock_enabled) { + return 0x26; + } + return 0x68; + case SDL_SCANCODE_KP_9: + if (!numlock_enabled) { + return 0x21; + } + return 0x69; + case SDL_SCANCODE_KP_PERIOD: + case SDL_SCANCODE_KP_COMMA: + if (!numlock_enabled) { + return 0x2E; + } + return 0x6E; + case SDL_SCANCODE_KP_DIVIDE: + return 0x6F; + case SDL_SCANCODE_KP_MULTIPLY: + return 0x6A; + case SDL_SCANCODE_KP_MINUS: + return 0x6D; + case SDL_SCANCODE_KP_PLUS: + return 0x6B; + case SDL_SCANCODE_KP_EQUALS: + return 0xBB; + default: + return -1; + } +} + +int TranslateSdlKeyboardEventToVk(const SDL_KeyboardEvent &event) { + const int keypad_key_code = TranslateSdlKeypadScancodeToVk(event); + if (keypad_key_code >= 0) { + return keypad_key_code; + } + + const int key = static_cast(event.key); + if (key >= 'a' && key <= 'z') { + return key - 'a' + 0x41; + } + if (key >= 'A' && key <= 'Z') { + return key; + } + if (key >= '0' && key <= '9') { + return key; + } + + switch (key) { + case ';': + return 0xBA; + case '\'': + return 0xDE; + case '`': + return 0xC0; + case ',': + return 0xBC; + case '.': + return 0xBE; + case '/': + return 0xBF; + case '\\': + return 0xDC; + case '[': + return 0xDB; + case ']': + return 0xDD; + case '-': + return 0xBD; + case '=': + return 0xBB; + default: + break; + } + + switch (event.scancode) { + case SDL_SCANCODE_ESCAPE: + return 0x1B; + case SDL_SCANCODE_RETURN: + return 0x0D; + case SDL_SCANCODE_SPACE: + return 0x20; + case SDL_SCANCODE_BACKSPACE: + return 0x08; + case SDL_SCANCODE_TAB: + return 0x09; + case SDL_SCANCODE_PRINTSCREEN: + return 0x2C; + case SDL_SCANCODE_SCROLLLOCK: + return 0x91; + case SDL_SCANCODE_PAUSE: + return 0x13; + case SDL_SCANCODE_INSERT: + return 0x2D; + case SDL_SCANCODE_DELETE: + return 0x2E; + case SDL_SCANCODE_HOME: + return 0x24; + case SDL_SCANCODE_END: + return 0x23; + case SDL_SCANCODE_PAGEUP: + return 0x21; + case SDL_SCANCODE_PAGEDOWN: + return 0x22; + case SDL_SCANCODE_LEFT: + return 0x25; + case SDL_SCANCODE_RIGHT: + return 0x27; + case SDL_SCANCODE_UP: + return 0x26; + case SDL_SCANCODE_DOWN: + return 0x28; + case SDL_SCANCODE_F1: + return 0x70; + case SDL_SCANCODE_F2: + return 0x71; + case SDL_SCANCODE_F3: + return 0x72; + case SDL_SCANCODE_F4: + return 0x73; + case SDL_SCANCODE_F5: + return 0x74; + case SDL_SCANCODE_F6: + return 0x75; + case SDL_SCANCODE_F7: + return 0x76; + case SDL_SCANCODE_F8: + return 0x77; + case SDL_SCANCODE_F9: + return 0x78; + case SDL_SCANCODE_F10: + return 0x79; + case SDL_SCANCODE_F11: + return 0x7A; + case SDL_SCANCODE_F12: + return 0x7B; + case SDL_SCANCODE_CAPSLOCK: + return 0x14; + case SDL_SCANCODE_LSHIFT: + return 0xA0; + case SDL_SCANCODE_RSHIFT: + return 0xA1; + case SDL_SCANCODE_LCTRL: + return 0xA2; + case SDL_SCANCODE_RCTRL: + return 0xA3; + case SDL_SCANCODE_LALT: + return 0xA4; + case SDL_SCANCODE_RALT: + return 0xA5; + case SDL_SCANCODE_LGUI: + return 0x5B; + case SDL_SCANCODE_RGUI: + return 0x5C; + default: + return -1; + } +} + +} // namespace + +int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) { + if (event.type != SDL_EVENT_KEY_DOWN && event.type != SDL_EVENT_KEY_UP) { + return -1; + } + + if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) { + return 0; + } + + const int key_code = TranslateSdlKeyboardEventToVk(event.key); + if (key_code < 0) { + return 0; + } + + return keyboard_.SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN); +} + +int GuiApplication::ProcessMouseEvent(const SDL_Event &event) { + controlled_remote_id_ = ""; + RemoteAction remote_action{}; + float cursor_x = last_mouse_event.motion.x; + float cursor_y = last_mouse_event.motion.y; + + auto normalize_cursor_to_window_space = [&](float *x, float *y) { + if (!x || !y || !stream_window_) { + return; + } + + int window_width = 0; + int window_height = 0; + int pixel_width = 0; + int pixel_height = 0; + SDL_GetWindowSize(stream_window_, &window_width, &window_height); + SDL_GetWindowSizeInPixels(stream_window_, &pixel_width, &pixel_height); + + if (window_width <= 0 || window_height <= 0 || pixel_width <= 0 || + pixel_height <= 0) { + return; + } + + if ((window_width != pixel_width || window_height != pixel_height) && + (*x > static_cast(window_width) + 1.0f || + *y > static_cast(window_height) + 1.0f)) { + const float scale_x = + static_cast(window_width) / static_cast(pixel_width); + const float scale_y = + static_cast(window_height) / static_cast(pixel_height); + *x *= scale_x; + *y *= scale_y; + + static bool logged_pixel_to_window_conversion = false; + if (!logged_pixel_to_window_conversion) { + LOG_INFO( + "Mouse coordinate space converted from pixels to window units: " + "window={}x{}, pixels={}x{}, scale=({:.4f},{:.4f})", + window_width, window_height, pixel_width, pixel_height, scale_x, + scale_y); + logged_pixel_to_window_conversion = true; + } + } + }; + + if (event.type == SDL_EVENT_MOUSE_MOTION) { + cursor_x = event.motion.x; + cursor_y = event.motion.y; + normalize_cursor_to_window_space(&cursor_x, &cursor_y); + } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || + event.type == SDL_EVENT_MOUSE_BUTTON_UP) { + cursor_x = event.button.x; + cursor_y = event.button.y; + normalize_cursor_to_window_space(&cursor_x, &cursor_y); + } else if (event.type == SDL_EVENT_MOUSE_WHEEL) { + cursor_x = last_mouse_event.motion.x; + cursor_y = last_mouse_event.motion.y; + } + + const bool is_pointer_position_event = + (event.type == SDL_EVENT_MOUSE_MOTION || + event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || + event.type == SDL_EVENT_MOUSE_BUTTON_UP); + + // std::shared_lock lock(remote_sessions_mutex_); + for (auto &it : remote_sessions_) { + auto props = it.second; + if (!props->control_mouse_) { + continue; + } + + const bool file_transfer_window_hovered = + props->file_transfer_.file_transfer_window_hovered_; + const bool overlay_hovered = + props->control_bar_hovered_ || props->display_selectable_hovered_ || + props->shortcut_selectable_hovered_ || file_transfer_window_hovered; + + const SDL_FRect render_rect = props->stream_render_rect_f_; + if (render_rect.w <= 1.0f || render_rect.h <= 1.0f) { + continue; + } + + if (is_pointer_position_event && cursor_x >= render_rect.x && + cursor_x <= render_rect.x + render_rect.w && + cursor_y >= render_rect.y && + cursor_y <= render_rect.y + render_rect.h) { + controlled_remote_id_ = it.first; + last_mouse_event.motion.x = cursor_x; + last_mouse_event.motion.y = cursor_y; + last_mouse_event.button.x = cursor_x; + last_mouse_event.button.y = cursor_y; + + remote_action.m.x = (cursor_x - render_rect.x) / render_rect.w; + remote_action.m.y = (cursor_y - render_rect.y) / render_rect.h; + remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f); + remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f); + + if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) { + remote_action.type = ControlType::mouse; + if (SDL_BUTTON_LEFT == event.button.button) { + remote_action.m.flag = MouseFlag::left_down; + } else if (SDL_BUTTON_RIGHT == event.button.button) { + remote_action.m.flag = MouseFlag::right_down; + } else if (SDL_BUTTON_MIDDLE == event.button.button) { + remote_action.m.flag = MouseFlag::middle_down; + } + } else if (SDL_EVENT_MOUSE_BUTTON_UP == event.type) { + remote_action.type = ControlType::mouse; + if (SDL_BUTTON_LEFT == event.button.button) { + remote_action.m.flag = MouseFlag::left_up; + } else if (SDL_BUTTON_RIGHT == event.button.button) { + remote_action.m.flag = MouseFlag::right_up; + } else if (SDL_BUTTON_MIDDLE == event.button.button) { + remote_action.m.flag = MouseFlag::middle_up; + } + } else if (SDL_EVENT_MOUSE_MOTION == event.type) { + remote_action.type = ControlType::mouse; + remote_action.m.flag = MouseFlag::move; + } + + if (overlay_hovered) { + break; + } + if (props->peer_) { + std::string msg = remote_action.to_json(); + SendDataFrame(props->peer_, msg.c_str(), msg.size(), + props->mouse_label_.c_str()); + } + } else if (SDL_EVENT_MOUSE_WHEEL == event.type && + last_mouse_event.button.x >= render_rect.x && + last_mouse_event.button.x <= render_rect.x + render_rect.w && + last_mouse_event.button.y >= render_rect.y && + last_mouse_event.button.y <= render_rect.y + render_rect.h) { + float scroll_x = event.wheel.x; + float scroll_y = event.wheel.y; + if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) { + scroll_x = -scroll_x; + scroll_y = -scroll_y; + } + + remote_action.type = ControlType::mouse; + + auto roundUp = [](float value) -> int { + if (value > 0) { + return static_cast(std::ceil(value)); + } else if (value < 0) { + return static_cast(std::floor(value)); + } + return 0; + }; + + if (std::abs(scroll_y) >= std::abs(scroll_x)) { + remote_action.m.flag = MouseFlag::wheel_vertical; + remote_action.m.s = roundUp(scroll_y); + } else { + remote_action.m.flag = MouseFlag::wheel_horizontal; + remote_action.m.s = roundUp(scroll_x); + } + + remote_action.m.x = (last_mouse_event.button.x - render_rect.x) / + (std::max)(render_rect.w, 1.0f); + remote_action.m.y = (last_mouse_event.button.y - render_rect.y) / + (std::max)(render_rect.h, 1.0f); + remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f); + remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f); + + if (overlay_hovered) { + continue; + } + if (props->peer_) { + std::string msg = remote_action.to_json(); + SendDataFrame(props->peer_, msg.c_str(), msg.size(), + props->mouse_label_.c_str()); + } + } + } + + return 0; +} + +} // namespace crossdesk \ No newline at end of file diff --git a/src/gui/application/window_lifecycle.cpp b/src/gui/application/window_lifecycle.cpp new file mode 100644 index 0000000..0396300 --- /dev/null +++ b/src/gui/application/window_lifecycle.cpp @@ -0,0 +1,687 @@ +#include "application/gui_application.h" + +#include + +#if defined(__linux__) && !defined(__APPLE__) +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fa_regular_400.h" +#include "fa_solid_900.h" +#include "layout_relative.h" +#include "localization.h" +#include "rd_log.h" +#include "version_checker.h" + +#if defined(__APPLE__) +#include "window_util_mac.h" +#endif + +namespace crossdesk { + +namespace { +const ImWchar *GetMultilingualGlyphRanges() { + static std::vector glyph_ranges; + if (glyph_ranges.empty()) { + ImGuiIO &io = ImGui::GetIO(); + ImFontGlyphRangesBuilder builder; + builder.AddRanges(io.Fonts->GetGlyphRangesDefault()); + builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull()); + builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic()); + + ImVector built_ranges; + builder.BuildRanges(&built_ranges); + glyph_ranges.assign(built_ranges.Data, + built_ranges.Data + built_ranges.Size); + } + return glyph_ranges.empty() ? nullptr : glyph_ranges.data(); +} + +bool CanReadFontFile(const char *font_path) { + if (!font_path) { + return false; + } + + std::ifstream font_file(font_path, std::ios::binary); + return font_file.good(); +} + +#if _WIN32 +HICON LoadTrayIcon() { + HMODULE module = GetModuleHandleW(nullptr); + HICON icon = reinterpret_cast( + LoadImageW(module, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE)); + if (icon) { + return icon; + } + + return LoadIconW(nullptr, IDI_APPLICATION); +} + +#endif + +#if defined(__linux__) && !defined(__APPLE__) +inline bool X11GetDisplayAndWindow(SDL_Window *window, Display **display_out, + ::Window *x11_window_out) { + if (!window || !display_out || !x11_window_out) { + return false; + } + +#if !defined(SDL_PROP_WINDOW_X11_DISPLAY_POINTER) || \ + !defined(SDL_PROP_WINDOW_X11_WINDOW_NUMBER) + // SDL build does not expose X11 window properties. + return false; +#else + SDL_PropertiesID props = SDL_GetWindowProperties(window); + Display *display = (Display *)SDL_GetPointerProperty( + props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL); + const Sint64 x11_window_num = + SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0); + const ::Window x11_window = (::Window)x11_window_num; + + if (!display || !x11_window) { + return false; + } + + *display_out = display; + *x11_window_out = x11_window; + return true; +#endif +} + +inline void X11SendNetWmState(Display *display, ::Window x11_window, + long action, Atom state1, Atom state2 = 0) { + if (!display || !x11_window) { + return; + } + + const Atom wm_state = XInternAtom(display, "_NET_WM_STATE", False); + + XEvent event; + memset(&event, 0, sizeof(event)); + event.xclient.type = ClientMessage; + event.xclient.serial = 0; + event.xclient.send_event = True; + event.xclient.message_type = wm_state; + event.xclient.window = x11_window; + event.xclient.format = 32; + event.xclient.data.l[0] = action; + event.xclient.data.l[1] = (long)state1; + event.xclient.data.l[2] = (long)state2; + event.xclient.data.l[3] = 1; // normal source indication + event.xclient.data.l[4] = 0; + + XSendEvent(display, DefaultRootWindow(display), False, + SubstructureRedirectMask | SubstructureNotifyMask, &event); +} + +inline void X11SetWindowTypeUtility(Display *display, ::Window x11_window) { + if (!display || !x11_window) { + return; + } + + const Atom wm_window_type = + XInternAtom(display, "_NET_WM_WINDOW_TYPE", False); + const Atom wm_window_type_utility = + XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False); + + XChangeProperty(display, x11_window, wm_window_type, XA_ATOM, 32, + PropModeReplace, (unsigned char *)&wm_window_type_utility, 1); +} + +inline void X11SetWindowAlwaysOnTop(SDL_Window *window) { + Display *display = nullptr; + ::Window x11_window = 0; + if (!X11GetDisplayAndWindow(window, &display, &x11_window)) { + return; + } + + const Atom state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False); + const Atom state_stays_on_top = + XInternAtom(display, "_NET_WM_STATE_STAYS_ON_TOP", False); + + // Request _NET_WM_STATE_ADD for ABOVE + STAYS_ON_TOP. + X11SendNetWmState(display, x11_window, 1, state_above, state_stays_on_top); + XFlush(display); +} + +inline void X11SetWindowSkipTaskbar(SDL_Window *window) { + Display *display = nullptr; + ::Window x11_window = 0; + if (!X11GetDisplayAndWindow(window, &display, &x11_window)) { + return; + } + + const Atom skip_taskbar = + XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", False); + const Atom skip_pager = + XInternAtom(display, "_NET_WM_STATE_SKIP_PAGER", False); + + // Request _NET_WM_STATE_ADD for SKIP_TASKBAR + SKIP_PAGER. + X11SendNetWmState(display, x11_window, 1, skip_taskbar, skip_pager); + + // Hint the WM that this is an auxiliary/utility window. + X11SetWindowTypeUtility(display, x11_window); + + XFlush(display); +} +#endif +} // namespace + +SDL_HitTestResult GuiApplication::HitTestCallback(SDL_Window *window, + const SDL_Point *area, + void *data) { + GuiApplication *application = (GuiApplication *)data; + if (!application) { + return SDL_HITTEST_NORMAL; + } + + if (application->fullscreen_button_pressed_) { + return SDL_HITTEST_NORMAL; + } + + // Server window: OS-level dragging for the title bar, but keep the left-side + // collapse/expand button clickable. + if (application->server_window_ && window == application->server_window_) { + const float title_h = application->server_window_title_bar_height_; + const float button_w = title_h; + if (area->y >= 0 && area->y < title_h) { + if (area->x >= 0 && area->x < button_w) { + return SDL_HITTEST_NORMAL; + } + return SDL_HITTEST_DRAGGABLE; + } + return SDL_HITTEST_NORMAL; + } + + int window_width, window_height; + SDL_GetWindowSize(window, &window_width, &window_height); + + // check if curosor is in tab bar + if (application->stream_window_inited_ && application->stream_window_created_ && + !application->fullscreen_button_pressed_ && application->stream_ctx_) { + ImGuiContext *prev_ctx = ImGui::GetCurrentContext(); + ImGui::SetCurrentContext(application->stream_ctx_); + + ImGuiWindow *tab_bar_window = ImGui::FindWindowByName("TabBar"); + if (tab_bar_window && tab_bar_window->Active) { + ImGuiIO &io = ImGui::GetIO(); + float scale_x = io.DisplayFramebufferScale.x; + float scale_y = io.DisplayFramebufferScale.y; + + float tab_bar_x = tab_bar_window->Pos.x * scale_x; + float tab_bar_y = tab_bar_window->Pos.y * scale_y; + float tab_bar_width = tab_bar_window->Size.x * scale_x; + float tab_bar_height = tab_bar_window->Size.y * scale_y; + + ImGui::SetCurrentContext(prev_ctx); + + if (area->x >= tab_bar_x && area->x <= tab_bar_x + tab_bar_width && + area->y >= tab_bar_y && area->y <= tab_bar_y + tab_bar_height) { + return SDL_HITTEST_NORMAL; + } + } else { + ImGui::SetCurrentContext(prev_ctx); + } + } + + float mouse_grab_padding = application->title_bar_button_width_ * 0.16f; + if (area->y < application->title_bar_button_width_ && + area->y > mouse_grab_padding && + area->x < window_width - application->title_bar_button_width_ * 3.0f && + area->x > mouse_grab_padding) { + return SDL_HITTEST_DRAGGABLE; + } + + // if (!application->streaming_) { + // return SDL_HITTEST_NORMAL; + // } + + if (area->y < mouse_grab_padding) { + if (area->x < mouse_grab_padding) { + return SDL_HITTEST_RESIZE_TOPLEFT; + } else if (area->x > window_width - mouse_grab_padding) { + return SDL_HITTEST_RESIZE_TOPRIGHT; + } else { + return SDL_HITTEST_RESIZE_TOP; + } + } else if (area->y > window_height - mouse_grab_padding) { + if (area->x < mouse_grab_padding) { + return SDL_HITTEST_RESIZE_BOTTOMLEFT; + } else if (area->x > window_width - mouse_grab_padding) { + return SDL_HITTEST_RESIZE_BOTTOMRIGHT; + } else { + return SDL_HITTEST_RESIZE_BOTTOM; + } + } else if (area->x < mouse_grab_padding) { + return SDL_HITTEST_RESIZE_LEFT; + } else if (area->x > window_width - mouse_grab_padding) { + return SDL_HITTEST_RESIZE_RIGHT; + } + + return SDL_HITTEST_NORMAL; +} + +int GuiApplication::CreateMainWindow() { + main_ctx_ = ImGui::CreateContext(); + if (!main_ctx_) { + LOG_ERROR("Main context is null"); + return -1; + } + + ImGui::SetCurrentContext(main_ctx_); + + if (!SDL_CreateWindowAndRenderer( + "CrossDesk Main Window", (int)main_window_width_, + (int)main_window_height_, + SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS | + SDL_WINDOW_HIDDEN | SDL_WINDOW_TRANSPARENT, + &main_window_, &main_renderer_)) { + LOG_ERROR("Error creating MainWindow and MainRenderer: {}", SDL_GetError()); + return -1; + } + + float dpi_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay()); + if (std::abs(dpi_scale_ - dpi_scale) > 0.01f) { + dpi_scale_ = dpi_scale; + + main_window_width_ = (int)(main_window_width_default_ * dpi_scale_); + main_window_height_ = (int)(main_window_height_default_ * dpi_scale_); + stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_); + stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_); + server_window_width_ = (int)(server_window_width_default_ * dpi_scale_); + server_window_height_ = (int)(server_window_height_default_ * dpi_scale_); + server_window_normal_width_ = + (int)(server_window_width_default_ * dpi_scale_); + server_window_normal_height_ = + (int)(server_window_height_default_ * dpi_scale_); + window_rounding_ = window_rounding_default_ * dpi_scale_; + + SDL_SetWindowSize(main_window_, (int)main_window_width_, + (int)main_window_height_); + } + + SDL_SetWindowResizable(main_window_, false); + + // for window region action + SDL_SetWindowHitTest(main_window_, HitTestCallback, this); + + SDL_SetRenderDrawBlendMode(main_renderer_, SDL_BLENDMODE_BLEND); + + SetupFontAndStyle(&main_windows_system_chinese_font_); + + ImGuiStyle &style = ImGui::GetStyle(); + style.ScaleAllSizes(dpi_scale_); + style.FontScaleDpi = dpi_scale_; + +#if _WIN32 + SDL_PropertiesID props = SDL_GetWindowProperties(main_window_); + HWND main_hwnd = (HWND)SDL_GetPointerProperty( + props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); + + HICON tray_icon = LoadTrayIcon(); + tray_ = std::make_unique(main_hwnd, tray_icon, L"CrossDesk", + localization_language_index_); +#elif defined(__APPLE__) + tray_ = std::make_unique(main_window_, "CrossDesk", + localization_language_index_); +#elif defined(__linux__) && !defined(__APPLE__) + tray_ = std::make_unique( + main_window_, "CrossDesk", localization_language_index_, APP_EXIT_EVENT); +#endif + + ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_); + ImGui_ImplSDLRenderer3_Init(main_renderer_); + + return 0; +} + +int GuiApplication::DestroyMainWindow() { + if (main_ctx_) { + ImGui::SetCurrentContext(main_ctx_); + } + + if (main_renderer_) { + SDL_DestroyRenderer(main_renderer_); + } + + if (main_window_) { + SDL_DestroyWindow(main_window_); + } + + return 0; +} + +int GuiApplication::CreateStreamWindow() { + if (stream_window_created_) { + return 0; + } + + stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_); + stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_); + + stream_ctx_ = ImGui::CreateContext(); + if (!stream_ctx_) { + LOG_ERROR("Stream context is null"); + return -1; + } + + ImGui::SetCurrentContext(stream_ctx_); + + if (!SDL_CreateWindowAndRenderer( + "CrossDesk Stream Window", (int)stream_window_width_, + (int)stream_window_height_, + SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS | + SDL_WINDOW_TRANSPARENT, + &stream_window_, &stream_renderer_)) { + LOG_ERROR("Error creating stream_window_ and stream_renderer_: {}", + SDL_GetError()); + return -1; + } + + stream_pixformat_ = SDL_PIXELFORMAT_NV12; + + SDL_SetWindowResizable(stream_window_, true); + + // for window region action + SDL_SetWindowHitTest(stream_window_, HitTestCallback, this); + + SDL_SetRenderDrawBlendMode(stream_renderer_, SDL_BLENDMODE_BLEND); + + SetupFontAndStyle(&stream_windows_system_chinese_font_); + + ImGuiStyle &style = ImGui::GetStyle(); + style.ScaleAllSizes(dpi_scale_); + style.FontScaleDpi = dpi_scale_; + + ImGui_ImplSDL3_InitForSDLRenderer(stream_window_, stream_renderer_); + ImGui_ImplSDLRenderer3_Init(stream_renderer_); + + // change props->stream_render_rect_ + SDL_Event event; + event.type = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED; + event.window.windowID = SDL_GetWindowID(stream_window_); + SDL_PushEvent(&event); + + stream_window_created_ = true; + just_created_ = true; + + stream_window_inited_ = true; + LOG_INFO("Stream window inited"); + + return 0; +} + +int GuiApplication::DestroyStreamWindow() { + stream_window_width_ = (float)stream_window_width_default_; + stream_window_height_ = (float)stream_window_height_default_; + + if (stream_ctx_) { + ImGui::SetCurrentContext(stream_ctx_); + } + + if (stream_renderer_) { + SDL_DestroyRenderer(stream_renderer_); + stream_renderer_ = nullptr; + } + + if (stream_window_) { + SDL_DestroyWindow(stream_window_); + stream_window_ = nullptr; + } + + stream_window_created_ = false; + focus_on_stream_window_ = false; + stream_window_grabbed_ = false; + control_mouse_ = false; + + return 0; +} + +int GuiApplication::CreateServerWindow() { + if (server_window_created_) { + return 0; + } + server_ctx_ = ImGui::CreateContext(); + if (!server_ctx_) { + LOG_ERROR("Server context is null"); + return -1; + } + ImGui::SetCurrentContext(server_ctx_); + if (!SDL_CreateWindowAndRenderer( + "CrossDesk Server Window", (int)server_window_width_, + (int)server_window_height_, + SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS | + SDL_WINDOW_TRANSPARENT, + &server_window_, &server_renderer_)) { + LOG_ERROR("Error creating server_window_ and server_renderer_: {}", + SDL_GetError()); + return -1; + } + +#if _WIN32 + // Hide server window from the taskbar by making it a tool window. + { + SDL_PropertiesID server_props = SDL_GetWindowProperties(server_window_); + HWND server_hwnd = (HWND)SDL_GetPointerProperty( + server_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); + + if (server_hwnd) { + LONG_PTR ex_style = GetWindowLongPtr(server_hwnd, GWL_EXSTYLE); + ex_style |= WS_EX_TOOLWINDOW; + ex_style &= ~WS_EX_APPWINDOW; + SetWindowLongPtr(server_hwnd, GWL_EXSTYLE, ex_style); + + // Keep the server window above normal windows. + SetWindowPos(server_hwnd, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOACTIVATE); + } + } +#endif + +#if defined(__linux__) && !defined(__APPLE__) + // Best-effort keep above other windows on X11. + X11SetWindowAlwaysOnTop(server_window_); + // Best-effort hide from taskbar on X11. + X11SetWindowSkipTaskbar(server_window_); +#endif + +#if defined(__APPLE__) + // Best-effort keep above other windows on macOS. + MacSetWindowAlwaysOnTop(server_window_, true); + // Best-effort exclude from Window menu / window cycling. + MacSetWindowExcludedFromWindowMenu(server_window_, true); +#endif + + // Set window position to bottom-right corner + SDL_Rect display_bounds; + if (SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(server_window_), + &display_bounds)) { + int window_x = + display_bounds.x + display_bounds.w - (int)server_window_width_; + int window_y = + display_bounds.y + display_bounds.h - (int)server_window_height_; + SDL_SetWindowPosition(server_window_, window_x, window_y); + } + + SDL_SetWindowResizable(server_window_, false); + + SDL_SetRenderDrawBlendMode(server_renderer_, SDL_BLENDMODE_BLEND); + + // for window region action + SDL_SetWindowHitTest(server_window_, HitTestCallback, this); + + SetupFontAndStyle(&server_windows_system_chinese_font_); + + ImGuiStyle &style = ImGui::GetStyle(); + style.ScaleAllSizes(dpi_scale_); + style.FontScaleDpi = dpi_scale_; + + ImGui_ImplSDL3_InitForSDLRenderer(server_window_, server_renderer_); + ImGui_ImplSDLRenderer3_Init(server_renderer_); + + server_window_created_ = true; + server_window_inited_ = true; + + LOG_INFO("Server window inited"); + + return 0; +} + +int GuiApplication::DestroyServerWindow() { + if (server_ctx_) { + ImGui::SetCurrentContext(server_ctx_); + } + + if (server_renderer_) { + SDL_DestroyRenderer(server_renderer_); + server_renderer_ = nullptr; + } + + if (server_window_) { + SDL_DestroyWindow(server_window_); + server_window_ = nullptr; + } + + server_window_created_ = false; + server_window_inited_ = false; + + return 0; +} + +int GuiApplication::SetupFontAndStyle(ImFont **system_chinese_font_out) { + float font_size = 32.0f; + + // Setup Dear ImGui style + ImGuiIO &io = ImGui::GetIO(); + + io.IniFilename = NULL; // disable imgui.ini + + // Build one merged atlas: UI font + icon font + multilingual fallback fonts. + ImFontConfig config; + config.FontDataOwnedByAtlas = false; + config.MergeMode = false; + + if (system_chinese_font_out) { + *system_chinese_font_out = nullptr; + } + + ImFont *ui_font = nullptr; + const ImWchar *multilingual_ranges = GetMultilingualGlyphRanges(); + +#if defined(_WIN32) + const char *base_font_paths[] = { + "C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/msyhbd.ttc", + "C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/simsun.ttc", nullptr}; +#elif defined(__APPLE__) + const char *base_font_paths[] = { + "/System/Library/Fonts/PingFang.ttc", + "/System/Library/Fonts/Supplemental/Arial Unicode.ttf", + "/System/Library/Fonts/Supplemental/Arial.ttf", + "/System/Library/Fonts/SFNS.ttf", nullptr}; +#else + const char *base_font_paths[] = { + "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", + "/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf", + "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", + "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + nullptr}; +#endif + + for (int i = 0; base_font_paths[i] != nullptr && ui_font == nullptr; ++i) { + if (!CanReadFontFile(base_font_paths[i])) { + continue; + } + ui_font = io.Fonts->AddFontFromFileTTF(base_font_paths[i], font_size, + &config, multilingual_ranges); + if (ui_font != nullptr) { + LOG_INFO("Loaded base UI font: {}", base_font_paths[i]); + } + } + if (!ui_font) { + ui_font = io.Fonts->AddFontDefault(&config); + } + + if (!ui_font) { + LOG_WARN("Failed to initialize base UI font"); + ImGui::StyleColorsLight(); + return 0; + } + + ImFontConfig icon_config = config; + icon_config.MergeMode = true; + static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; + io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len, + font_size, &icon_config, icon_ranges); + + io.FontDefault = ui_font; + if (system_chinese_font_out) { + *system_chinese_font_out = ui_font; + } + + ImGui::StyleColorsLight(); + + return 0; +} + +int GuiApplication::DestroyMainWindowContext() { + if (!main_ctx_) { + return 0; + } + + ImGui::SetCurrentContext(main_ctx_); + ImGui_ImplSDLRenderer3_Shutdown(); + ImGui_ImplSDL3_Shutdown(); + ImGui::DestroyContext(main_ctx_); + main_ctx_ = nullptr; + + return 0; +} + +int GuiApplication::DestroyStreamWindowContext() { + if (!stream_ctx_) { + stream_window_inited_ = false; + return 0; + } + + stream_window_inited_ = false; + ImGui::SetCurrentContext(stream_ctx_); + ImGui_ImplSDLRenderer3_Shutdown(); + ImGui_ImplSDL3_Shutdown(); + ImGui::DestroyContext(stream_ctx_); + stream_ctx_ = nullptr; + + return 0; +} + +int GuiApplication::DestroyServerWindowContext() { + if (!server_ctx_) { + server_window_inited_ = false; + return 0; + } + + server_window_inited_ = false; + ImGui::SetCurrentContext(server_ctx_); + ImGui_ImplSDLRenderer3_Shutdown(); + ImGui_ImplSDL3_Shutdown(); + ImGui::DestroyContext(server_ctx_); + server_ctx_ = nullptr; + + return 0; +} + +} // namespace crossdesk diff --git a/src/gui/application/window_rendering.cpp b/src/gui/application/window_rendering.cpp new file mode 100644 index 0000000..0ea7216 --- /dev/null +++ b/src/gui/application/window_rendering.cpp @@ -0,0 +1,236 @@ +#include "application/gui_application.h" + +#include + +#include "rd_log.h" + +namespace crossdesk { + +int GuiApplication::DrawMainWindow() { + if (!main_ctx_) { + LOG_ERROR("Main context is null"); + return -1; + } + + ImGui::SetCurrentContext(main_ctx_); + ImGui_ImplSDLRenderer3_NewFrame(); + ImGui_ImplSDL3_NewFrame(); + ImGui::NewFrame(); + + ImGuiIO &io = ImGui::GetIO(); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_); + + ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y), + ImGuiCond_Always); + ImGui::Begin("MainRender", nullptr, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoDocking); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + + TitleBar(true); + + MainWindow(); + + UpdateNotificationWindow(); + +#if _WIN32 && CROSSDESK_PORTABLE + PortableServiceInstallWindow(); +#endif + +#ifdef __APPLE__ + if (show_request_permission_window_) { + RequestPermissionWindow(); + } +#endif + + ImGui::End(); + + // Rendering + (void)io; + ImGui::Render(); + SDL_SetRenderScale(main_renderer_, io.DisplayFramebufferScale.x, + io.DisplayFramebufferScale.y); + SDL_SetRenderDrawColor(main_renderer_, 0, 0, 0, 0); + SDL_RenderClear(main_renderer_); + ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), main_renderer_); + SDL_RenderPresent(main_renderer_); + + return 0; +} + +int GuiApplication::DrawStreamWindow() { + if (!stream_ctx_) { + LOG_ERROR("Stream context is null"); + return -1; + } + + ImGui::SetCurrentContext(stream_ctx_); + ImGui_ImplSDLRenderer3_NewFrame(); + ImGui_ImplSDL3_NewFrame(); + ImGui::NewFrame(); + + StreamWindow(); + + ImGuiIO &io = ImGui::GetIO(); + float stream_title_window_height = + fullscreen_button_pressed_ ? 0 : title_bar_height_; + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + // Set minimum window size to 0 to allow exact height control + ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, stream_title_window_height), + ImGuiCond_Always); + ImGui::Begin("StreamTitleWindow", nullptr, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoDocking); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(); + + if (!fullscreen_button_pressed_) { + TitleBar(false); + } + + ImGui::End(); + + // Rendering + (void)io; + ImGui::Render(); + SDL_SetRenderScale(stream_renderer_, io.DisplayFramebufferScale.x, + io.DisplayFramebufferScale.y); + SDL_SetRenderDrawColor(stream_renderer_, 0, 0, 0, 255); + SDL_RenderClear(stream_renderer_); + + // std::shared_lock lock(remote_sessions_mutex_); + for (auto &it : remote_sessions_) { + auto props = it.second; + if (props->tab_selected_) { + SDL_FRect render_rect_f = { + props->stream_render_rect_f_.x, props->stream_render_rect_f_.y, + props->stream_render_rect_f_.w, props->stream_render_rect_f_.h}; + SDL_RenderTexture(stream_renderer_, props->stream_texture_, NULL, + &render_rect_f); + } + } + ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), stream_renderer_); + SDL_RenderPresent(stream_renderer_); + + return 0; +} + +int GuiApplication::DrawServerWindow() { + if (!server_ctx_) { + LOG_ERROR("Server context is null"); + return -1; + } + ImGui::SetCurrentContext(server_ctx_); + ImGui_ImplSDLRenderer3_NewFrame(); + ImGui_ImplSDL3_NewFrame(); + ImGui::NewFrame(); + + ImGuiIO &io = ImGui::GetIO(); + server_window_width_ = io.DisplaySize.x; + server_window_height_ = io.DisplaySize.y; + + ServerWindow(); + ImGui::Render(); + SDL_SetRenderScale(server_renderer_, io.DisplayFramebufferScale.x, + io.DisplayFramebufferScale.y); + SDL_SetRenderDrawColor(server_renderer_, 0, 0, 0, 0); + SDL_RenderClear(server_renderer_); + ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), server_renderer_); + SDL_RenderPresent(server_renderer_); + return 0; +} + +bool GuiApplication::MinimizeMainWindowToTray() { + if (!enable_minimize_to_tray_ || !main_window_) { + return false; + } + +#if defined(_WIN32) || defined(__APPLE__) + if (!tray_) { + return false; + } + + tray_->MinimizeToTray(); + return true; +#elif defined(__linux__) && !defined(__APPLE__) + if (!tray_) { + return false; + } + + return tray_->MinimizeToTray(); +#else + return false; +#endif +} + +void GuiApplication::UpdateRenderRect() { + // std::shared_lock lock(remote_sessions_mutex_); + for (auto &[_, props] : remote_sessions_) { + if (!props->reset_control_bar_pos_) { + props->mouse_diff_control_bar_pos_x_ = 0; + props->mouse_diff_control_bar_pos_y_ = 0; + } + + if (!just_created_) { + props->reset_control_bar_pos_ = true; + } + + int stream_window_width, stream_window_height; + SDL_GetWindowSize(stream_window_, &stream_window_width, + &stream_window_height); + stream_window_width_ = (float)stream_window_width; + stream_window_height_ = (float)stream_window_height; + + float video_ratio = + (float)props->video_width_ / (float)props->video_height_; + float video_ratio_reverse = + (float)props->video_height_ / (float)props->video_width_; + + float render_area_width = props->render_window_width_; + float render_area_height = props->render_window_height_; + + props->stream_render_rect_last_ = props->stream_render_rect_; + + SDL_FRect rect_f{props->render_window_x_, props->render_window_y_, + render_area_width, render_area_height}; + if (render_area_width < render_area_height * video_ratio) { + rect_f.x = props->render_window_x_; + rect_f.y = std::abs(render_area_height - + render_area_width * video_ratio_reverse) / + 2.0f + + props->render_window_y_; + rect_f.w = render_area_width; + rect_f.h = render_area_width * video_ratio_reverse; + } else if (render_area_width > render_area_height * video_ratio) { + rect_f.x = + std::abs(render_area_width - render_area_height * video_ratio) / + 2.0f + + props->render_window_x_; + rect_f.y = props->render_window_y_; + rect_f.w = render_area_height * video_ratio; + rect_f.h = render_area_height; + } else { + rect_f.x = props->render_window_x_; + rect_f.y = props->render_window_y_; + rect_f.w = render_area_width; + rect_f.h = render_area_height; + } + + props->stream_render_rect_f_ = rect_f; + props->stream_render_rect_ = {static_cast(std::lround(rect_f.x)), + static_cast(std::lround(rect_f.y)), + static_cast(std::lround(rect_f.w)), + static_cast(std::lround(rect_f.h))}; + } +} + +} // namespace crossdesk diff --git a/src/gui/assets/layouts/layout.h b/src/gui/assets/layouts/layout.h index 45c8602..d5b8374 100644 --- a/src/gui/assets/layouts/layout.h +++ b/src/gui/assets/layouts/layout.h @@ -7,8 +7,6 @@ #ifndef _LAYOUT_STYLE_H_ #define _LAYOUT_STYLE_H_ -#include "render.h" - #define MENU_WINDOW_WIDTH_CN 300 * dpi_scale_ #define MENU_WINDOW_HEIGHT_CN 280 * dpi_scale_ #define LOCAL_WINDOW_WIDTH_CN 300 * dpi_scale_ @@ -84,4 +82,4 @@ #define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125 * dpi_scale_ #define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90 * dpi_scale_ #define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210 * dpi_scale_ -#endif \ No newline at end of file +#endif diff --git a/src/gui/assets/layouts/layout_relative.h b/src/gui/assets/layouts/layout_relative.h index ac1a530..e58a0e4 100644 --- a/src/gui/assets/layouts/layout_relative.h +++ b/src/gui/assets/layouts/layout_relative.h @@ -7,8 +7,6 @@ #ifndef _LAYOUT_STYLE_H_ #define _LAYOUT_STYLE_H_ -#include "render.h" - #define TITLE_BAR_HEIGHT 0.0625f #define TITLE_BAR_BUTTON_WIDTH 0.0625f #define TITLE_BAR_BUTTON_HEIGHT 0.0625f @@ -88,4 +86,4 @@ #define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125 #define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90 #define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210 -#endif \ No newline at end of file +#endif diff --git a/src/gui/features/clipboard/clipboard_controller.cpp b/src/gui/features/clipboard/clipboard_controller.cpp new file mode 100644 index 0000000..6ceaeae --- /dev/null +++ b/src/gui/features/clipboard/clipboard_controller.cpp @@ -0,0 +1,177 @@ +#include "features/clipboard/clipboard_controller.h" + +#include +#include +#include + +#include "rd_log.h" +#include "runtime/gui_runtime.h" + +namespace crossdesk { +namespace { + +// Keep a reliable clipboard message within MiniRTC/KCP's single-message +// fragmentation window (MTU is configured to 1200 bytes). +constexpr size_t kMaxClipboardTextBytes = 128 * 1024; + +} // namespace + +ClipboardController::ClipboardController(GuiRuntime &owner) : owner_(owner) {} + +void ClipboardController::SetEventType(uint32_t event_type) { + event_type_ = event_type; +} + +uint32_t ClipboardController::event_type() const { return event_type_; } + +void ClipboardController::Initialize() { + last_text_.clear(); + + char *clipboard_text = SDL_GetClipboardText(); + if (clipboard_text) { + last_text_.assign(clipboard_text); + SDL_free(clipboard_text); + } + + if (event_type_ == 0) { + LOG_ERROR("Clipboard synchronization disabled: SDL event is unavailable"); + return; + } + + events_enabled_.store(true, std::memory_order_release); +} + +void ClipboardController::Shutdown() { + events_enabled_.store(false, std::memory_order_release); + std::lock_guard lock(pending_mutex_); + pending_remote_text_.reset(); +} + +void ClipboardController::QueueRemoteText(const char *data, size_t size) { + if (!events_enabled_.load(std::memory_order_acquire)) { + return; + } + if (!data || size == 0) { + return; + } + if (size > kMaxClipboardTextBytes) { + LOG_WARN("Ignore oversized remote clipboard text: {} bytes", size); + return; + } + if (std::memchr(data, '\0', size) != nullptr) { + LOG_WARN("Ignore remote clipboard text containing an embedded NUL byte"); + return; + } + + { + std::lock_guard lock(pending_mutex_); + if (!events_enabled_.load(std::memory_order_relaxed)) { + return; + } + // Only the newest clipboard value matters. Replacing it also bounds the + // amount of memory a busy or malicious peer can queue. + pending_remote_text_ = std::string(data, size); + } + + SDL_Event event{}; + event.type = event_type_; + if (!SDL_PushEvent(&event)) { + // MainLoop also drains the pending value after its wait timeout. + LOG_WARN("Failed to wake SDL loop for remote clipboard text: {}", + SDL_GetError()); + } +} + +void ClipboardController::ApplyPendingRemoteText() { + if (!events_enabled_.load(std::memory_order_acquire)) { + return; + } + + std::optional pending_text; + { + std::lock_guard lock(pending_mutex_); + pending_text.swap(pending_remote_text_); + } + if (!pending_text || *pending_text == last_text_) { + return; + } + + // SDL clipboard functions must run on the thread that initialized SDL. + if (!SDL_SetClipboardText(pending_text->c_str())) { + LOG_ERROR("Failed to set remote clipboard text: {}", SDL_GetError()); + return; + } + + // SDL will normally emit SDL_EVENT_CLIPBOARD_UPDATE for this write. Update + // the baseline first so that event cannot echo the text back to the sender. + last_text_ = std::move(*pending_text); +} + +void ClipboardController::HandleLocalUpdate() { + if (!events_enabled_.load(std::memory_order_acquire)) { + return; + } + if (!SDL_HasClipboardText()) { + last_text_.clear(); + return; + } + + char *clipboard_text = SDL_GetClipboardText(); + if (!clipboard_text) { + LOG_WARN("Failed to read local clipboard text: {}", SDL_GetError()); + return; + } + + std::string text(clipboard_text); + SDL_free(clipboard_text); + if (text == last_text_) { + return; + } + + // Record the value before sending. Duplicate SDL notifications and a + // remote write of the same value must not create a clipboard feedback loop. + last_text_ = text; + if (text.empty()) { + return; + } + if (text.size() > kMaxClipboardTextBytes) { + LOG_WARN("Ignore oversized local clipboard text: {} bytes", text.size()); + return; + } + + SendToPeers(text); +} + +int ClipboardController::SendToPeers(const std::string &text) { + std::shared_lock lock(owner_.remote_sessions_mutex_); + for (const auto &[remote_id, props] : owner_.remote_sessions_) { + if (!props || !props->peer_ || !props->connection_established_ || + !props->enable_mouse_control_) { + continue; + } + + const int ret = + SendReliableDataFrame(props->peer_, text.data(), text.size(), + props->clipboard_label_.c_str()); + if (ret != 0) { + LOG_WARN("Failed to send clipboard data to peer [{}], ret={}", + remote_id.c_str(), ret); + return ret; + } + } + + if (owner_.peer_) { + const int ret = + SendReliableDataFrame(owner_.peer_, text.data(), text.size(), + owner_.clipboard_label_.c_str()); + if (ret != 0) { + LOG_WARN("Failed to send clipboard data to peer [{}], ret={}", + owner_.remote_id_display_, ret); + return ret; + } + } + + return 0; +} + +} // namespace crossdesk diff --git a/src/gui/features/clipboard/clipboard_controller.h b/src/gui/features/clipboard/clipboard_controller.h new file mode 100644 index 0000000..3ac66cf --- /dev/null +++ b/src/gui/features/clipboard/clipboard_controller.h @@ -0,0 +1,45 @@ +#ifndef CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_ +#define CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_ + +#include + +#include +#include +#include +#include +#include +#include + +namespace crossdesk { + +class GuiRuntime; + +// Owns clipboard synchronization state and keeps feedback-loop prevention out +// of the main UI coordinator. +class ClipboardController { +public: + explicit ClipboardController(GuiRuntime &owner); + + void SetEventType(uint32_t event_type); + uint32_t event_type() const; + + void Initialize(); + void Shutdown(); + void QueueRemoteText(const char *data, size_t size); + void ApplyPendingRemoteText(); + void HandleLocalUpdate(); + +private: + int SendToPeers(const std::string &text); + + GuiRuntime &owner_; + uint32_t event_type_ = 0; + std::atomic events_enabled_{false}; + std::mutex pending_mutex_; + std::optional pending_remote_text_; + std::string last_text_; +}; + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_ diff --git a/src/gui/features/devices/session_device_manager.cpp b/src/gui/features/devices/session_device_manager.cpp new file mode 100644 index 0000000..d7461db --- /dev/null +++ b/src/gui/features/devices/session_device_manager.cpp @@ -0,0 +1,484 @@ +#include "features/devices/session_device_manager.h" + +#include +#include + +#include "platform.h" +#include "rd_log.h" +#include "runtime/gui_runtime.h" + +namespace crossdesk { +namespace { + +constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500; + +} // namespace + +SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner) : owner_(owner) {} + +void SessionDeviceManager::Initialize() { + InitializeAudioOutput(); + screen_capturer_factory_ = new ScreenCapturerFactory(); + speaker_capturer_factory_ = new SpeakerCapturerFactory(); + device_controller_factory_ = new DeviceControllerFactory(); + keyboard_capturer_ = + static_cast(device_controller_factory_->Create( + DeviceControllerFactory::Device::Keyboard)); +} + +int SessionDeviceManager::InitializeScreenCapturer() { +#ifdef __APPLE__ + if (!owner_.EnsureMacScreenRecordingPermission()) { + return -1; + } +#endif + + if (!screen_capturer_) { + screen_capturer_ = + static_cast(screen_capturer_factory_->Create()); + } + + last_frame_time_ = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + const int fps = owner_.config_center_->GetVideoFrameRate() == + ConfigCenter::VIDEO_FRAME_RATE::FPS_30 + ? 30 + : 60; + LOG_INFO("Init screen capturer with {} fps", fps); + + const int init_ret = screen_capturer_->Init( + fps, [this, fps](unsigned char *data, int size, int width, int height, + const char *display_name) { + const auto now_time = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); + const auto duration = now_time - last_frame_time_; + if (duration * fps < 1000) { + return; + } + + const std::string stream_id = display_name ? display_name : ""; + const bool resumed_after_gap = + last_frame_time_ != 0 && duration >= kCaptureResumeKeyFrameGapMs; + const bool stream_changed = !last_video_frame_stream_id_.empty() && + last_video_frame_stream_id_ != stream_id; + if (resumed_after_gap || stream_changed) { + if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) { + LOG_INFO("Request video key frame before sending captured frame, " + "stream='{}', gap_ms={}, stream_changed={}", + stream_id, duration, stream_changed); + } + } + + XVideoFrame frame{}; + frame.data = reinterpret_cast(data); + frame.size = size; + frame.width = width; + frame.height = height; + frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_); + SendVideoFrame(owner_.peer_, &frame, stream_id.c_str()); + last_video_frame_stream_id_ = stream_id; + last_frame_time_ = now_time; + }); + + if (init_ret == 0) { + LOG_INFO("Init screen capturer success"); + const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); + if (!latest_display_info.empty()) { + display_info_list_ = latest_display_info; + } + return 0; + } + + LOG_ERROR("Init screen capturer failed"); + screen_capturer_->Destroy(); + delete screen_capturer_; + screen_capturer_ = nullptr; + return -1; +} + +int SessionDeviceManager::StartScreenCapturer() { +#ifdef __APPLE__ + if (!owner_.EnsureMacScreenRecordingPermission()) { + return -1; + } +#endif + + if (!screen_capturer_) { + LOG_INFO("Screen capturer instance missing, recreating before start"); + if (InitializeScreenCapturer() != 0) { + LOG_ERROR("Recreate screen capturer failed"); + return -1; + } + } + + LOG_INFO("Start screen capturer, show cursor: {}", owner_.show_cursor_); + const int ret = screen_capturer_->Start(owner_.show_cursor_); + if (ret != 0) { + LOG_ERROR("Start screen capturer failed: {}", ret); + } + return ret; +} + +int SessionDeviceManager::StopScreenCapturer() { + if (screen_capturer_) { + LOG_INFO("Stop screen capturer"); + screen_capturer_->Stop(); + } + return 0; +} + +int SessionDeviceManager::StartSpeakerCapturer() { + if (!speaker_capturer_) { + speaker_capturer_ = + static_cast(speaker_capturer_factory_->Create()); + const int init_ret = speaker_capturer_->Init( + [this](unsigned char *data, size_t size, const char *audio_name) { + SendAudioFrame(owner_.peer_, reinterpret_cast(data), + size, owner_.audio_label_.c_str()); + }); + + if (init_ret != 0) { + speaker_capturer_->Destroy(); + delete speaker_capturer_; + speaker_capturer_ = nullptr; + } + } + + if (!speaker_capturer_) { + return -1; + } + + const int ret = speaker_capturer_->Start(); + if (ret != 0) { + LOG_ERROR("Start speaker capturer failed: {}", ret); + return ret; + } + owner_.start_speaker_capturer_ = true; + return 0; +} + +int SessionDeviceManager::StopSpeakerCapturer() { + if (speaker_capturer_) { + speaker_capturer_->Stop(); + owner_.start_speaker_capturer_ = false; + } + return 0; +} + +int SessionDeviceManager::StartMouseController() { +#ifdef __APPLE__ + if (!owner_.EnsureMacAccessibilityPermission()) { + return -1; + } +#endif + + if (!device_controller_factory_) { + LOG_INFO("Device controller factory is nullptr"); + return -1; + } + +#if defined(__linux__) && !defined(__APPLE__) + if (IsWaylandSession()) { + if (!screen_capturer_) { + return 1; + } + const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); + if (latest_display_info.empty() || + latest_display_info[0].handle == nullptr) { + return 1; + } + } + + if (screen_capturer_) { + const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); + if (!latest_display_info.empty()) { + display_info_list_ = latest_display_info; + } + } +#endif + + mouse_controller_ = + static_cast(device_controller_factory_->Create( + DeviceControllerFactory::Device::Mouse)); + if (!mouse_controller_) { + LOG_ERROR("Create mouse controller failed"); + return -1; + } + + const int init_ret = mouse_controller_->Init(display_info_list_); + if (init_ret != 0) { + LOG_INFO("Destroy mouse controller"); + mouse_controller_->Destroy(); + delete mouse_controller_; + mouse_controller_ = nullptr; + } + return init_ret; +} + +int SessionDeviceManager::StopMouseController() { + if (mouse_controller_) { + mouse_controller_->Destroy(); + delete mouse_controller_; + mouse_controller_ = nullptr; + } + return 0; +} + +int SessionDeviceManager::StartKeyboardCapturer() { + owner_.keyboard_capturer_uses_sdl_events_ = false; + +#ifdef __APPLE__ + if (!owner_.EnsureMacAccessibilityPermission()) { + owner_.keyboard_capturer_uses_sdl_events_ = true; + return 0; + } +#endif + +#if defined(__linux__) && !defined(__APPLE__) + if (IsWaylandSession()) { + owner_.keyboard_capturer_uses_sdl_events_ = true; + LOG_INFO("Start keyboard capturer with SDL Wayland backend"); + return 0; + } +#endif + + if (!keyboard_capturer_) { + owner_.keyboard_capturer_uses_sdl_events_ = true; + LOG_WARN( + "keyboard capturer is nullptr, falling back to SDL keyboard events"); + return 0; + } + + const int hook_ret = keyboard_capturer_->Hook( + [](int key_code, bool is_down, uint32_t scan_code, bool extended, + void *user_ptr) { + if (user_ptr) { + auto *runtime = static_cast(user_ptr); + runtime->keyboard_.SendKeyCommand(key_code, is_down, scan_code, + extended); + } + }, + &owner_); + if (hook_ret != 0) { + owner_.keyboard_capturer_uses_sdl_events_ = true; + LOG_WARN( + "Start keyboard capturer failed, falling back to SDL keyboard events"); + } else { + LOG_INFO("Start keyboard capturer with native hook"); + } + return 0; +} + +int SessionDeviceManager::StopKeyboardCapturer() { + if (owner_.keyboard_capturer_uses_sdl_events_) { + owner_.keyboard_capturer_uses_sdl_events_ = false; + LOG_INFO("Stop keyboard capturer with SDL keyboard backend"); + return 0; + } + + if (keyboard_capturer_) { + keyboard_capturer_->Unhook(); + LOG_INFO("Stop keyboard capturer"); + } + return 0; +} + +int SessionDeviceManager::InitializeAudioOutput() { + SDL_AudioSpec desired_out{}; + desired_out.freq = 48000; + desired_out.format = SDL_AUDIO_S16; + desired_out.channels = 1; + + auto open_stream = [&]() { + output_stream_ = SDL_OpenAudioDeviceStream( + SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_out, nullptr, nullptr); + return output_stream_ != nullptr; + }; + + if (!open_stream()) { +#if defined(__linux__) && !defined(__APPLE__) + LOG_WARN("Failed to open output stream with driver [{}]: {}", + getenv("SDL_AUDIODRIVER") ? getenv("SDL_AUDIODRIVER") + : "(default)", + SDL_GetError()); + + setenv("SDL_AUDIODRIVER", "dummy", 1); + SDL_QuitSubSystem(SDL_INIT_AUDIO); + if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) { + LOG_ERROR("Failed to reinitialize SDL audio with dummy driver: {}", + SDL_GetError()); + return -1; + } + if (!open_stream()) { + LOG_ERROR("Failed to open output stream with dummy driver: {}", + SDL_GetError()); + return -1; + } + LOG_WARN("Audio output disabled, using SDL dummy audio driver"); +#else + LOG_ERROR("Failed to open output stream: {}", SDL_GetError()); + return -1; +#endif + } + + SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(output_stream_)); + return 0; +} + +int SessionDeviceManager::DestroyAudioOutput() { + if (output_stream_) { + SDL_CloseAudioDevice(SDL_GetAudioStreamDevice(output_stream_)); + SDL_DestroyAudioStream(output_stream_); + output_stream_ = nullptr; + } + return 0; +} + +void SessionDeviceManager::PushAudio(const char *data, size_t size) { + if (!output_stream_) { + return; + } + const int pushed = SDL_PutAudioStreamData( + output_stream_, reinterpret_cast(data), + static_cast(size)); + if (pushed < 0) { + LOG_ERROR("Failed to push audio data: {}", SDL_GetError()); + } +} + +void SessionDeviceManager::UpdateInteractions() { +#if defined(__linux__) && !defined(__APPLE__) + const bool is_wayland_session = IsWaylandSession(); + const bool stop_wayland_mouse_before_screen = + is_wayland_session && !owner_.start_screen_capturer_ && + owner_.screen_capturer_is_started_ && !owner_.start_mouse_controller_ && + owner_.mouse_controller_is_started_; + if (stop_wayland_mouse_before_screen) { + LOG_INFO("Stopping Wayland mouse controller before screen capturer to " + "cleanly release the shared portal session"); + StopMouseController(); + owner_.mouse_controller_is_started_ = false; + } +#endif + + if (owner_.start_screen_capturer_ && !owner_.screen_capturer_is_started_) { + if (StartScreenCapturer() == 0) { + owner_.screen_capturer_is_started_ = true; + } + } else if (!owner_.start_screen_capturer_ && + owner_.screen_capturer_is_started_) { + StopScreenCapturer(); + owner_.screen_capturer_is_started_ = false; + } + + if (owner_.start_speaker_capturer_ && !owner_.speaker_capturer_is_started_) { + if (StartSpeakerCapturer() == 0) { + owner_.speaker_capturer_is_started_ = true; + } + } else if (!owner_.start_speaker_capturer_ && + owner_.speaker_capturer_is_started_) { + StopSpeakerCapturer(); + owner_.speaker_capturer_is_started_ = false; + } + + if (owner_.start_mouse_controller_ && !owner_.mouse_controller_is_started_) { + if (StartMouseController() == 0) { + owner_.mouse_controller_is_started_ = true; + } + } else if (!owner_.start_mouse_controller_ && + owner_.mouse_controller_is_started_) { + StopMouseController(); + owner_.mouse_controller_is_started_ = false; + } + +#if defined(__linux__) && !defined(__APPLE__) + if (owner_.screen_capturer_is_started_ && screen_capturer_ && + mouse_controller_) { + const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); + if (!latest_display_info.empty()) { + display_info_list_ = latest_display_info; + mouse_controller_->UpdateDisplayInfoList(display_info_list_); + } + } +#endif + + if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_) { + if (!owner_.keyboard_capturer_is_started_ && StartKeyboardCapturer() == 0) { + owner_.keyboard_capturer_is_started_ = true; + } + if (owner_.keyboard_capturer_is_started_) { + owner_.keyboard_.SendHeartbeat(false); + } + } else if (owner_.keyboard_capturer_is_started_) { + owner_.keyboard_.ForceReleasePressedKeys(); + StopKeyboardCapturer(); + owner_.keyboard_capturer_is_started_ = false; + } + + owner_.keyboard_.CheckRemoteTimeouts(); +} + +bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down, + uint32_t scan_code, + bool extended) { + return keyboard_capturer_ && keyboard_capturer_->SendKeyboardCommand( + key_code, is_down, scan_code, extended) == 0; +} + +void SessionDeviceManager::SendMouseCommand(const RemoteAction &action, + int selected_display) { + if (mouse_controller_) { + mouse_controller_->SendMouseCommand(action, selected_display); + } +} + +int SessionDeviceManager::SwitchDisplay(int display_id) { + return screen_capturer_ ? screen_capturer_->SwitchTo(display_id) : -1; +} + +void SessionDeviceManager::ResetToInitialDisplay() { + if (screen_capturer_) { + screen_capturer_->ResetToInitialMonitor(); + } +} + +const std::vector & +SessionDeviceManager::display_info_list() const { + return display_info_list_; +} + +void SessionDeviceManager::DestroyDevices() { + if (mouse_controller_) { + mouse_controller_->Destroy(); + delete mouse_controller_; + mouse_controller_ = nullptr; + } + if (screen_capturer_) { + screen_capturer_->Destroy(); + delete screen_capturer_; + screen_capturer_ = nullptr; + } + if (speaker_capturer_) { + speaker_capturer_->Destroy(); + delete speaker_capturer_; + speaker_capturer_ = nullptr; + } + if (keyboard_capturer_) { + delete keyboard_capturer_; + keyboard_capturer_ = nullptr; + } +} + +void SessionDeviceManager::DestroyFactories() { + delete screen_capturer_factory_; + screen_capturer_factory_ = nullptr; + delete speaker_capturer_factory_; + speaker_capturer_factory_ = nullptr; + delete device_controller_factory_; + device_controller_factory_ = nullptr; +} + +} // namespace crossdesk diff --git a/src/gui/features/devices/session_device_manager.h b/src/gui/features/devices/session_device_manager.h new file mode 100644 index 0000000..5db858d --- /dev/null +++ b/src/gui/features/devices/session_device_manager.h @@ -0,0 +1,72 @@ +#ifndef CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_ +#define CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_ + +#include + +#include +#include +#include +#include + +#include "device_controller.h" +#include "device_controller_factory.h" +#include "display_info.h" +#include "screen_capturer_factory.h" +#include "speaker_capturer_factory.h" + +namespace crossdesk { + +class GuiRuntime; + +// Owns media capture/playback and remote input devices for the active GUI +// session. GuiRuntime supplies application intent; this class handles device +// creation, updates and teardown. +class SessionDeviceManager { +public: + explicit SessionDeviceManager(GuiRuntime &owner); + + void Initialize(); + void UpdateInteractions(); + void DestroyDevices(); + void DestroyFactories(); + + int InitializeScreenCapturer(); + int StartScreenCapturer(); + int StopScreenCapturer(); + int StartSpeakerCapturer(); + int StopSpeakerCapturer(); + int StartMouseController(); + int StopMouseController(); + int StartKeyboardCapturer(); + int StopKeyboardCapturer(); + + int InitializeAudioOutput(); + int DestroyAudioOutput(); + void PushAudio(const char *data, size_t size); + + bool SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code, + bool extended); + void SendMouseCommand(const RemoteAction &action, int selected_display); + int SwitchDisplay(int display_id); + void ResetToInitialDisplay(); + + const std::vector &display_info_list() const; + +private: + GuiRuntime &owner_; + SDL_AudioStream *output_stream_ = nullptr; + ScreenCapturerFactory *screen_capturer_factory_ = nullptr; + ScreenCapturer *screen_capturer_ = nullptr; + SpeakerCapturerFactory *speaker_capturer_factory_ = nullptr; + SpeakerCapturer *speaker_capturer_ = nullptr; + DeviceControllerFactory *device_controller_factory_ = nullptr; + MouseController *mouse_controller_ = nullptr; + KeyboardCapturer *keyboard_capturer_ = nullptr; + std::vector display_info_list_; + uint64_t last_frame_time_ = 0; + std::string last_video_frame_stream_id_; +}; + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_ diff --git a/src/gui/features/file_transfer/file_transfer_manager.cpp b/src/gui/features/file_transfer/file_transfer_manager.cpp new file mode 100644 index 0000000..ca198d4 --- /dev/null +++ b/src/gui/features/file_transfer/file_transfer_manager.cpp @@ -0,0 +1,379 @@ +#include "features/file_transfer/file_transfer_manager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "file_transfer.h" +#include "rd_log.h" +#include "runtime/gui_runtime.h" + +namespace crossdesk { + +FileTransferManager::FileTransferManager(GuiRuntime &owner) : owner_(owner) {} + +FileTransferManager::FileTransferState &FileTransferManager::global_state() { + return global_state_; +} + +FileTransferManager::FileTransferState &FileTransferManager::state_for( + const std::shared_ptr &props) { + return props ? props->file_transfer_ : global_state_; +} + +void FileTransferManager::ProcessSelectedFile( + const std::string &path, + const std::shared_ptr &props, + const std::string &file_label, const std::string &remote_id) { + if (path.empty()) { + return; + } + + FileTransferState &state = state_for(props); + LOG_INFO("Selected file: {}", path.c_str()); + + const std::filesystem::path file_path = std::filesystem::u8path(path); + std::error_code ec; + if (!std::filesystem::is_regular_file(file_path, ec)) { + LOG_ERROR("Selected path is not a regular file: {}", path); + return; + } + const uint64_t file_size = std::filesystem::file_size(file_path, ec); + if (ec) { + LOG_ERROR("Failed to get file size: {}", ec.message()); + return; + } + + { + std::lock_guard lock(state.file_transfer_list_mutex_); + FileTransferState::FileTransferInfo info; + info.file_name = file_path.filename().u8string(); + info.file_path = file_path; + info.file_size = file_size; + info.status = FileTransferState::FileTransferStatus::Queued; + state.file_transfer_list_.push_back(std::move(info)); + } + state.file_transfer_window_visible_ = true; + + auto enqueue = [&]() { + size_t queue_size = 0; + { + std::lock_guard lock(state.file_queue_mutex_); + state.file_send_queue_.push( + FileTransferState::QueuedFile{file_path, file_label, remote_id}); + queue_size = state.file_send_queue_.size(); + } + LOG_INFO("File added to queue: {} ({} files in queue)", + file_path.filename().string().c_str(), queue_size); + }; + + if (state.file_sending_.load()) { + enqueue(); + return; + } + + Start(props, file_path, file_label, remote_id); + if (!state.file_sending_.load()) { + enqueue(); + } +} + +void FileTransferManager::Start( + std::shared_ptr props, + const std::filesystem::path &file_path, const std::string &file_label, + const std::string &remote_id) { + const bool is_global = !props; + PeerPtr *peer = is_global ? owner_.peer_ : props->peer_; + if (!peer) { + LOG_ERROR("StartFileTransfer: invalid peer"); + return; + } + + FileTransferState &initial_state = state_for(props); + bool expected = false; + if (!initial_state.file_sending_.compare_exchange_strong(expected, true)) { + LOG_WARN("StartFileTransfer called while another file is active: {}", + file_path.filename().string().c_str()); + return; + } + + const auto props_weak = std::weak_ptr(props); + std::thread([this, peer, file_path, file_label, props_weak, remote_id, + is_global]() { + auto props_locked = props_weak.lock(); + FileTransferState *state = nullptr; + if (props_locked) { + state = &props_locked->file_transfer_; + } else if (is_global) { + state = &global_state_; + } else { + return; + } + + std::error_code ec; + const uint64_t total_size = std::filesystem::file_size(file_path, ec); + if (ec) { + LOG_ERROR("Failed to get file size: {}", ec.message().c_str()); + state->file_sending_ = false; + return; + } + + state->file_sent_bytes_ = 0; + state->file_total_bytes_ = total_size; + state->file_send_rate_bps_ = 0; + state->file_transfer_window_visible_ = true; + { + std::lock_guard lock(state->file_transfer_mutex_); + state->file_send_start_time_ = std::chrono::steady_clock::now(); + state->file_send_last_update_time_ = state->file_send_start_time_; + state->file_send_last_bytes_ = 0; + } + + FileSender sender; + const uint32_t file_id = FileSender::NextFileId(); + if (props_locked) { + std::lock_guard lock(file_id_to_props_mutex_); + file_id_to_props_[file_id] = props_weak; + } else { + std::lock_guard lock(file_id_to_state_mutex_); + file_id_to_state_[file_id] = state; + } + state->current_file_id_ = file_id; + + { + std::lock_guard lock(state->file_transfer_list_mutex_); + for (auto &info : state->file_transfer_list_) { + if (info.file_path == file_path && + info.status == FileTransferState::FileTransferStatus::Queued) { + info.status = FileTransferState::FileTransferStatus::Sending; + info.file_id = file_id; + info.file_size = total_size; + info.sent_bytes = 0; + break; + } + } + } + + const int ret = sender.SendFile( + file_path, file_path.filename().string(), + [peer, file_label, remote_id](const char *buffer, size_t size) { + if (remote_id.empty()) { + return SendReliableDataFrame(peer, buffer, size, + file_label.c_str()); + } + return SendReliableDataFrameToPeer( + peer, buffer, size, file_label.c_str(), remote_id.c_str(), + remote_id.size()); + }, + 64 * 1024, file_id); + + if (ret == 0) { + return; + } + + state->file_sending_ = false; + state->file_transfer_window_visible_ = false; + state->file_sent_bytes_ = 0; + state->file_total_bytes_ = 0; + state->file_send_rate_bps_ = 0; + state->current_file_id_ = 0; + Unregister(file_id, props_locked != nullptr); + + { + std::lock_guard lock(state->file_transfer_list_mutex_); + for (auto &info : state->file_transfer_list_) { + if (info.file_id == file_id) { + info.status = FileTransferState::FileTransferStatus::Failed; + break; + } + } + } + LOG_ERROR("FileSender::SendFile failed for [{}], ret={}", + file_path.string().c_str(), ret); + ProcessQueue(props_locked); + }).detach(); +} + +void FileTransferManager::ProcessQueue( + std::shared_ptr props) { + FileTransferState &state = state_for(props); + if (state.file_sending_.load()) { + return; + } + + FileTransferState::QueuedFile queued_file; + { + std::lock_guard lock(state.file_queue_mutex_); + if (state.file_send_queue_.empty()) { + return; + } + queued_file = state.file_send_queue_.front(); + state.file_send_queue_.pop(); + } + Start(props, queued_file.file_path, queued_file.file_label, + queued_file.remote_id); +} + +void FileTransferManager::Unregister(uint32_t file_id, bool per_peer) { + if (per_peer) { + std::lock_guard lock(file_id_to_props_mutex_); + file_id_to_props_.erase(file_id); + } else { + std::lock_guard lock(file_id_to_state_mutex_); + file_id_to_state_.erase(file_id); + } +} + +void FileTransferManager::HandleAck(const char *data, size_t size) { + if (size < sizeof(FileTransferAck)) { + LOG_ERROR("FileTransferAck: buffer too small, size={}", size); + return; + } + + FileTransferAck ack{}; + std::memcpy(&ack, data, sizeof(ack)); + if (ack.magic != kFileAckMagic) { + LOG_ERROR("FileTransferAck: invalid magic, got 0x{:08X}", ack.magic); + return; + } + + std::shared_ptr props; + { + std::shared_lock lock(file_id_to_props_mutex_); + const auto it = file_id_to_props_.find(ack.file_id); + if (it != file_id_to_props_.end()) { + props = it->second.lock(); + } + } + + FileTransferState *state = props ? &props->file_transfer_ : nullptr; + if (!state) { + std::shared_lock lock(file_id_to_state_mutex_); + const auto it = file_id_to_state_.find(ack.file_id); + if (it != file_id_to_state_.end()) { + state = it->second; + } + } + if (!state) { + LOG_WARN("FileTransferAck: no state found for file_id={}", ack.file_id); + return; + } + + state->file_sent_bytes_ = ack.acked_offset; + state->file_total_bytes_ = ack.total_size; + uint32_t rate_bps = 0; + if (props) { + const uint32_t bitrate = + props->net_traffic_stats_.data_outbound_stats.bitrate; + if (bitrate > 0 && state->file_sending_.load()) { + rate_bps = static_cast(bitrate * 0.99f); + const uint32_t current_rate = state->file_send_rate_bps_.load(); + if (current_rate > 0) { + rate_bps = static_cast(current_rate * 0.7 + rate_bps * 0.3); + } + } else { + rate_bps = state->file_send_rate_bps_.load(); + } + } else { + const uint32_t current_rate = state->file_send_rate_bps_.load(); + uint32_t estimated_rate = 0; + const auto now = std::chrono::steady_clock::now(); + uint64_t last_bytes = 0; + std::chrono::steady_clock::time_point last_time; + { + std::lock_guard lock(state->file_transfer_mutex_); + last_bytes = state->file_send_last_bytes_; + last_time = state->file_send_last_update_time_; + } + if (state->file_sending_.load() && ack.acked_offset >= last_bytes) { + const auto delta_bytes = ack.acked_offset - last_bytes; + const double seconds = + std::chrono::duration(now - last_time).count(); + if (seconds > 0.0 && delta_bytes > 0) { + const double bits_per_second = delta_bytes * 8.0 / seconds; + estimated_rate = static_cast((std::min)( + bits_per_second, + static_cast((std::numeric_limits::max)()))); + } + } + rate_bps = + estimated_rate > 0 && current_rate > 0 + ? static_cast(current_rate * 0.7 + estimated_rate * 0.3) + : estimated_rate > 0 ? estimated_rate + : current_rate; + } + + state->file_send_rate_bps_ = rate_bps; + state->file_send_last_bytes_ = ack.acked_offset; + state->file_send_last_update_time_ = std::chrono::steady_clock::now(); + { + std::lock_guard lock(state->file_transfer_list_mutex_); + for (auto &info : state->file_transfer_list_) { + if (info.file_id == ack.file_id) { + info.sent_bytes = ack.acked_offset; + info.file_size = ack.total_size; + info.rate_bps = rate_bps; + if ((ack.flags & 0x01) != 0) { + info.status = FileTransferState::FileTransferStatus::Completed; + info.sent_bytes = ack.total_size; + } + break; + } + } + } + + if ((ack.flags & 0x01) == 0) { + return; + } + LOG_INFO("File transfer completed: file_id={}, bytes={}", ack.file_id, + ack.total_size); + state->file_transfer_window_visible_ = true; + state->file_sending_ = false; + Unregister(ack.file_id, props != nullptr); + ProcessQueue(props); +} + +void FileTransferManager::HandleDropEvent(const SDL_Event &event) { + if (event.type != SDL_EVENT_DROP_FILE || + !((owner_.stream_window_ && + SDL_GetWindowID(owner_.stream_window_) == event.window.windowID) || + (owner_.server_window_ && + SDL_GetWindowID(owner_.server_window_) == event.window.windowID))) { + return; + } + + if (owner_.stream_window_ && + SDL_GetWindowID(owner_.stream_window_) == event.window.windowID) { + if (!owner_.stream_window_inited_ || !event.drop.data) { + return; + } + std::shared_lock lock(owner_.remote_sessions_mutex_); + for (const auto &[remote_id, props] : owner_.remote_sessions_) { + if (props && props->tab_selected_ && props->peer_) { + ProcessSelectedFile(static_cast(event.drop.data), props, + props->file_label_); + break; + } + } + return; + } + + if (owner_.server_window_inited_ && event.drop.data) { + const auto file_path = std::filesystem::u8path(event.drop.data); + std::error_code ec; + if (std::filesystem::is_regular_file(file_path, ec)) { + LOG_INFO("Drop file [{}] on server window (size: {} bytes)", + event.drop.data, std::filesystem::file_size(file_path, ec)); + } + } +} + +} // namespace crossdesk diff --git a/src/gui/features/file_transfer/file_transfer_manager.h b/src/gui/features/file_transfer/file_transfer_manager.h new file mode 100644 index 0000000..807a17a --- /dev/null +++ b/src/gui/features/file_transfer/file_transfer_manager.h @@ -0,0 +1,57 @@ +#ifndef CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_ +#define CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "runtime/gui_state.h" + +namespace crossdesk { + +class GuiRuntime; + +class FileTransferManager { +public: + using FileTransferState = gui_detail::FileTransferState; + using RemoteSession = gui_detail::RemoteSession; + + explicit FileTransferManager(GuiRuntime &owner); + + FileTransferState &global_state(); + FileTransferState & + state_for(const std::shared_ptr &props); + + void + ProcessSelectedFile(const std::string &path, + const std::shared_ptr &props, + const std::string &file_label, + const std::string &remote_id = ""); + void HandleDropEvent(const SDL_Event &event); + void HandleAck(const char *data, size_t size); + +private: + void Start(std::shared_ptr props, + const std::filesystem::path &file_path, + const std::string &file_label, const std::string &remote_id = ""); + void ProcessQueue(std::shared_ptr props); + void Unregister(uint32_t file_id, bool per_peer); + + GuiRuntime &owner_; + FileTransferState global_state_; + std::unordered_map> + file_id_to_props_; + std::shared_mutex file_id_to_props_mutex_; + std::unordered_map file_id_to_state_; + std::shared_mutex file_id_to_state_mutex_; +}; + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_ diff --git a/src/gui/features/input/keyboard_controller.cpp b/src/gui/features/input/keyboard_controller.cpp new file mode 100644 index 0000000..4ee0c65 --- /dev/null +++ b/src/gui/features/input/keyboard_controller.cpp @@ -0,0 +1,362 @@ +#include "features/input/keyboard_controller.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "minirtc.h" +#include "rd_log.h" +#include "runtime/gui_runtime.h" +#include "windows_key_metadata.h" +#if _WIN32 +#include "interactive_state.h" +#include "service_host.h" +#endif + +namespace crossdesk { +namespace { + +constexpr uint32_t kHeartbeatIntervalMs = 500; +constexpr uint32_t kRemoteReleaseTimeoutMs = 2500; + +int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code, + bool extended) { +#if _WIN32 + if (key_code != 0x10 && key_code != 0x11 && key_code != 0x12) { + return key_code; + } + + UINT scan_code_with_prefix = static_cast(scan_code & 0xFF); + if (extended) { + scan_code_with_prefix |= 0xE000; + } + const UINT normalized_vk = + MapVirtualKeyW(scan_code_with_prefix, MAPVK_VSC_TO_VK_EX); + return normalized_vk != 0 ? static_cast(normalized_vk) : key_code; +#else + (void)scan_code; + (void)extended; + return key_code; +#endif +} + +void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out, + bool *extended_out) { + if (!scan_code_out || !extended_out) { + return; + } +#if _WIN32 + const UINT scan_code = + MapVirtualKeyW(static_cast(key_code), MAPVK_VK_TO_VSC_EX); + if (scan_code != 0) { + *scan_code_out = static_cast(scan_code & 0xFF); + *extended_out = (scan_code & 0xFF00) != 0; + return; + } +#endif + LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out); +} + +#if _WIN32 +constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000; + +void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) { + const uint32_t now = static_cast(SDL_GetTicks()); + if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) { + return; + } + *last_tick = now; + LOG_WARN( + "local secure-desktop input blocked, stage={}, normal SendInput path " + "cannot drive the Windows password UI", + stage ? stage : ""); +} + +bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response, + const RemoteAction &action) { + return response.is_object() && + response.value("error", std::string()) == "send_input_failed" && + response.value("code", 0u) == ERROR_ACCESS_DENIED && + action.type == ControlType::keyboard && + action.k.flag == KeyFlag::key_up; +} +#endif + +} // namespace + +KeyboardController::KeyboardController(GuiRuntime &owner) : owner_(owner) {} + +void KeyboardController::TrackPressedKey(int key_code, bool is_down, + uint32_t scan_code, bool extended) { + std::lock_guard lock(pressed_keys_mutex_); + if (is_down) { + pressed_keys_[key_code] = PressedKey{key_code, scan_code, extended}; + } else { + pressed_keys_.erase(key_code); + } +} + +void KeyboardController::ForceReleasePressedKeys() { + std::vector pressed_keys; + { + std::lock_guard lock(pressed_keys_mutex_); + pressed_keys.reserve(pressed_keys_.size()); + for (const auto &[_, key] : pressed_keys_) { + pressed_keys.push_back(key); + } + pressed_keys_.clear(); + } + + for (const PressedKey &key : pressed_keys) { + SendKeyCommand(key.key_code, false, key.scan_code, key.extended); + } + SendHeartbeat(true); +} + +void KeyboardController::SendHeartbeat(bool force) { + const uint32_t now = static_cast(SDL_GetTicks()); + if (!force && now - last_heartbeat_tick_ < kHeartbeatIntervalMs) { + return; + } + + RemoteAction action{}; + action.type = ControlType::keyboard_state; + action.ks.seq = ++state_sequence_; + { + std::lock_guard lock(pressed_keys_mutex_); + size_t index = 0; + for (const auto &[_, key] : pressed_keys_) { + if (index >= kMaxKeyboardStateKeys) { + LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}", + pressed_keys_.size()); + break; + } + action.ks.pressed_keys[index].key_value = + static_cast(key.key_code); + action.ks.pressed_keys[index].scan_code = key.scan_code; + action.ks.pressed_keys[index].extended = key.extended; + ++index; + } + action.ks.pressed_count = index; + } + + const std::string target_id = owner_.controlled_remote_id_.empty() + ? owner_.focused_remote_id_ + : owner_.controlled_remote_id_; + const auto props_it = owner_.remote_sessions_.find(target_id); + if (target_id.empty() || props_it == owner_.remote_sessions_.end() || + props_it->second->connection_status_.load() != + ConnectionStatus::Connected || + !props_it->second->peer_) { + last_heartbeat_tick_ = now; + return; + } + + const std::string message = action.to_json(); + const int result = SendReliableDataFrame( + props_it->second->peer_, message.c_str(), message.size(), + props_it->second->keyboard_label_.c_str()); + if (result != 0) { + LOG_WARN("Send keyboard heartbeat failed, remote_id={}, ret={}", target_id, + result); + } + last_heartbeat_tick_ = now; +} + +int KeyboardController::SendKeyCommand(int key_code, bool is_down, + uint32_t scan_code, bool extended) { + if (scan_code == 0) { + PopulateWindowsKeyMetadataFromVk(key_code, &scan_code, &extended); + } +#if _WIN32 + key_code = NormalizeWindowsModifierVk(key_code, scan_code, extended); +#endif + + RemoteAction action{}; + action.type = ControlType::keyboard; + action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up; + action.k.key_value = key_code; + action.k.scan_code = scan_code; + action.k.extended = extended; + + const std::string target_id = owner_.controlled_remote_id_.empty() + ? owner_.focused_remote_id_ + : owner_.controlled_remote_id_; + const auto props_it = owner_.remote_sessions_.find(target_id); + if (!target_id.empty() && props_it != owner_.remote_sessions_.end() && + props_it->second->connection_status_.load() == + ConnectionStatus::Connected && + props_it->second->peer_) { + const std::string message = action.to_json(); + const int result = SendReliableDataFrame( + props_it->second->peer_, message.c_str(), message.size(), + props_it->second->keyboard_label_.c_str()); + if (result != 0) { + LOG_WARN("Send keyboard command failed, remote_id={}, ret={}", target_id, + result); + } + } + + TrackPressedKey(key_code, is_down, scan_code, extended); + return 0; +} + +bool KeyboardController::InjectRemoteKey(int key_code, bool is_down, + uint32_t scan_code, bool extended) { +#if _WIN32 + if (owner_.local_service_status_received_ && + IsSecureDesktopInteractionRequired(owner_.local_interactive_stage_)) { + const std::string response = SendCrossDeskSecureDesktopKeyInput( + key_code, is_down, scan_code, extended, 1000); + const auto json = nlohmann::json::parse(response, nullptr, false); + if (json.is_discarded() || !json.value("ok", false)) { + RemoteAction action{}; + action.type = ControlType::keyboard; + action.k.key_value = static_cast(key_code); + action.k.scan_code = scan_code; + action.k.extended = extended; + action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up; + if (!json.is_discarded() && + IsTransientSecureDesktopInputFailure(json, action)) { + LOG_INFO("Secure desktop keyboard injection transient failure, " + "key_code={}, is_down={}, response={}", + key_code, is_down, response); + return true; + } + + LogSecureDesktopInputBlocked( + &owner_.last_local_secure_input_block_log_tick_, + owner_.local_interactive_stage_.c_str()); + LOG_WARN( + "Secure desktop keyboard injection failed, key_code={}, is_down={}, " + "response={}", + key_code, is_down, response); + return false; + } + return true; + } +#endif + return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code, + extended); +} + +void KeyboardController::ApplyRemoteEvent(const std::string &remote_id, + const RemoteAction &action) { + const int key_code = static_cast(action.k.key_value); + const bool is_down = action.k.flag == KeyFlag::key_down; + const bool injected = + InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended); + + std::lock_guard lock(remote_states_mutex_); + auto &state = remote_states_[remote_id]; + state.last_seen_tick = static_cast(SDL_GetTicks()); + if (is_down && injected) { + state.pressed_keys[key_code] = + PressedKey{key_code, action.k.scan_code, action.k.extended}; + } else if (!is_down && injected) { + state.pressed_keys.erase(key_code); + } +} + +void KeyboardController::ApplyRemoteState(const std::string &remote_id, + const RemoteAction &action) { + std::vector keys_to_release; + std::vector keys_to_press; + { + std::lock_guard lock(remote_states_mutex_); + auto &state = remote_states_[remote_id]; + if (action.ks.seq != 0 && state.last_seq != 0 && + static_cast(action.ks.seq - state.last_seq) <= 0) { + return; + } + + state.last_seq = action.ks.seq; + state.last_seen_tick = static_cast(SDL_GetTicks()); + state.keyboard_state_seen = true; + + std::unordered_map desired_keys; + const size_t count = + (std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys); + for (size_t index = 0; index < count; ++index) { + const auto &key = action.ks.pressed_keys[index]; + const int key_code = static_cast(key.key_value); + desired_keys[key_code] = + PressedKey{key_code, key.scan_code, key.extended}; + } + for (const auto &[key_code, key] : state.pressed_keys) { + if (desired_keys.find(key_code) == desired_keys.end()) { + keys_to_release.push_back(key); + } + } + for (const auto &[key_code, key] : desired_keys) { + if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) { + keys_to_press.push_back(key); + } + } + } + + for (const PressedKey &key : keys_to_release) { + if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) { + std::lock_guard lock(remote_states_mutex_); + const auto state_it = remote_states_.find(remote_id); + if (state_it != remote_states_.end()) { + state_it->second.pressed_keys.erase(key.key_code); + } + } + } + for (const PressedKey &key : keys_to_press) { + if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) { + std::lock_guard lock(remote_states_mutex_); + remote_states_[remote_id].pressed_keys[key.key_code] = key; + } + } +} + +void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id, + const char *reason) { + std::vector keys_to_release; + { + std::lock_guard lock(remote_states_mutex_); + const auto state_it = remote_states_.find(remote_id); + if (state_it == remote_states_.end()) { + return; + } + for (const auto &[_, key] : state_it->second.pressed_keys) { + keys_to_release.push_back(key); + } + remote_states_.erase(state_it); + } + + if (!keys_to_release.empty()) { + LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}", + keys_to_release.size(), remote_id, reason ? reason : "unknown"); + } + for (const PressedKey &key : keys_to_release) { + InjectRemoteKey(key.key_code, false, key.scan_code, key.extended); + } +} + +void KeyboardController::CheckRemoteTimeouts() { + const uint32_t now = static_cast(SDL_GetTicks()); + std::vector timed_out_remotes; + { + std::lock_guard lock(remote_states_mutex_); + for (const auto &[remote_id, state] : remote_states_) { + if (state.keyboard_state_seen && !state.pressed_keys.empty() && + state.last_seen_tick != 0 && + now - state.last_seen_tick > kRemoteReleaseTimeoutMs) { + timed_out_remotes.push_back(remote_id); + } + } + } + for (const std::string &remote_id : timed_out_remotes) { + ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout"); + } +} + +} // namespace crossdesk diff --git a/src/gui/features/input/keyboard_controller.h b/src/gui/features/input/keyboard_controller.h new file mode 100644 index 0000000..56a0c68 --- /dev/null +++ b/src/gui/features/input/keyboard_controller.h @@ -0,0 +1,63 @@ +#ifndef CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_ +#define CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_ + +#include +#include +#include +#include + +#include "device_controller.h" + +namespace crossdesk { + +class GuiRuntime; + +// Synchronizes local and remote keyboard state, including heartbeat recovery +// for keys whose key-up event was lost during a connection interruption. +class KeyboardController { +public: + explicit KeyboardController(GuiRuntime &owner); + + int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0, + bool extended = false); + void ForceReleasePressedKeys(); + void SendHeartbeat(bool force); + void ApplyRemoteEvent(const std::string &remote_id, + const RemoteAction &remote_action); + void ApplyRemoteState(const std::string &remote_id, + const RemoteAction &remote_action); + void ReleaseRemotePressedKeys(const std::string &remote_id, + const char *reason); + void CheckRemoteTimeouts(); + +private: + struct PressedKey { + int key_code = 0; + uint32_t scan_code = 0; + bool extended = false; + }; + + struct RemoteState { + std::unordered_map pressed_keys; + uint32_t last_seq = 0; + uint32_t last_seen_tick = 0; + bool keyboard_state_seen = false; + }; + + void TrackPressedKey(int key_code, bool is_down, uint32_t scan_code, + bool extended); + bool InjectRemoteKey(int key_code, bool is_down, uint32_t scan_code, + bool extended); + + GuiRuntime &owner_; + std::unordered_map pressed_keys_; + std::mutex pressed_keys_mutex_; + uint32_t state_sequence_ = 0; + uint32_t last_heartbeat_tick_ = 0; + std::unordered_map remote_states_; + std::mutex remote_states_mutex_; +}; + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_ diff --git a/src/gui/features/settings/settings_manager.cpp b/src/gui/features/settings/settings_manager.cpp new file mode 100644 index 0000000..428a122 --- /dev/null +++ b/src/gui/features/settings/settings_manager.cpp @@ -0,0 +1,345 @@ +#include "features/settings/settings_manager.h" + +#include +#include +#include +#include +#include +#include + +#include "localization.h" +#include "rd_log.h" +#include "runtime/gui_runtime.h" + +namespace crossdesk { +namespace { + +template +void CopyString(char (&destination)[Size], const char *source) { + static_assert(Size > 0); + std::memset(destination, 0, Size); + if (source) { + std::strncpy(destination, source, Size - 1); + } +} + +} // namespace + +SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {} + +int SettingsManager::Save() { + std::lock_guard lock(cache_mutex_); + return SaveLocked(); +} + +int SettingsManager::SaveLocked() { + std::ofstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc", + std::ios::binary); + if (!cache_v2_file) { + return -1; + } + + CopyString(cache_v2_.client_id_with_password, + owner_.client_id_with_password_); + std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_)); + std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_)); + CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_); + + cache_v2_file.write(reinterpret_cast(&cache_v2_), + sizeof(cache_v2_)); + + // Keep writing the legacy cache while older installations may still read it. + std::ofstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc", + std::ios::binary); + if (cache_v1_file) { + CopyString(cache_v1_.client_id_with_password, + owner_.client_id_with_password_); + std::memcpy(cache_v1_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_)); + std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_)); + cache_v1_file.write(reinterpret_cast(&cache_v1_), + sizeof(cache_v1_)); + } + + return 0; +} + +bool SettingsManager::ReadV2Locked() { + std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc", + std::ios::binary); + if (!cache_v2_file) { + return false; + } + cache_v2_file.read(reinterpret_cast(&cache_v2_), sizeof(cache_v2_)); + if (cache_v2_file.gcount() != + static_cast(sizeof(cache_v2_))) { + return false; + } + cache_v2_ + .client_id_with_password[sizeof(cache_v2_.client_id_with_password) - 1] = + '\0'; + cache_v2_.self_hosted_id[sizeof(cache_v2_.self_hosted_id) - 1] = '\0'; + return true; +} + +int SettingsManager::Load() { + std::unique_lock lock(cache_mutex_); + + if (ReadV2Locked()) { + CopyString(owner_.client_id_with_password_, + cache_v2_.client_id_with_password); + CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id); + std::memcpy(owner_.aes128_key_, cache_v2_.key, sizeof(cache_v2_.key)); + std::memcpy(owner_.aes128_iv_, cache_v2_.iv, sizeof(cache_v2_.iv)); + LOG_INFO("Load settings from v2 cache file"); + } else { + std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc", + std::ios::binary); + if (!cache_v1_file) { + lock.unlock(); + + std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_)); + std::memset(owner_.aes128_key_, 0, sizeof(owner_.aes128_key_)); + std::memset(owner_.aes128_iv_, 0, sizeof(owner_.aes128_iv_)); + std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_)); + + owner_.thumbnail_ = + std::make_shared(owner_.cache_path_ + "/thumbnails/"); + owner_.thumbnail_->GetKeyAndIv(owner_.aes128_key_, owner_.aes128_iv_); + owner_.thumbnail_->DeleteAllFilesInDirectory(); + + Save(); + return -1; + } + + cache_v1_file.read(reinterpret_cast(&cache_v1_), sizeof(cache_v1_)); + cache_v1_ + .client_id_with_password[sizeof(cache_v1_.client_id_with_password) - + 1] = '\0'; + + CopyString(cache_v2_.client_id_with_password, + cache_v1_.client_id_with_password); + std::memcpy(cache_v2_.key, cache_v1_.key, sizeof(cache_v1_.key)); + std::memcpy(cache_v2_.iv, cache_v1_.iv, sizeof(cache_v1_.iv)); + std::memset(cache_v2_.self_hosted_id, 0, sizeof(cache_v2_.self_hosted_id)); + + CopyString(owner_.client_id_with_password_, + cache_v1_.client_id_with_password); + std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_)); + std::memcpy(owner_.aes128_key_, cache_v1_.key, sizeof(cache_v1_.key)); + std::memcpy(owner_.aes128_iv_, cache_v1_.iv, sizeof(cache_v1_.iv)); + + SaveLocked(); + LOG_INFO("Migrated settings from v1 to v2 cache file"); + } + + lock.unlock(); + + const char *at_pos = std::strchr(owner_.client_id_with_password_, '@'); + if (at_pos != nullptr) { + const std::string id(owner_.client_id_with_password_, + at_pos - owner_.client_id_with_password_); + const std::string password(at_pos + 1); + CopyString(owner_.client_id_, id.c_str()); + CopyString(owner_.password_saved_, password.c_str()); + } + + owner_.thumbnail_ = + std::make_shared(owner_.cache_path_ + "/thumbnails/", + owner_.aes128_key_, owner_.aes128_iv_); + + owner_.language_button_value_ = localization::detail::ClampLanguageIndex( + static_cast(owner_.config_center_->GetLanguage())); + owner_.video_quality_button_value_ = + static_cast(owner_.config_center_->GetVideoQuality()); + owner_.video_frame_rate_button_value_ = + static_cast(owner_.config_center_->GetVideoFrameRate()); + owner_.video_encode_format_button_value_ = + static_cast(owner_.config_center_->GetVideoEncodeFormat()); + owner_.enable_hardware_video_codec_ = + owner_.config_center_->IsHardwareVideoCodec(); + owner_.enable_turn_ = owner_.config_center_->IsEnableTurn(); + owner_.enable_srtp_ = owner_.config_center_->IsEnableSrtp(); + owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted(); + owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart(); + owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon(); + owner_.enable_minimize_to_tray_ = owner_.config_center_->IsMinimizeToTray(); +#if _WIN32 && CROSSDESK_PORTABLE + owner_.portable_service_prompt_suppressed_ = + owner_.config_center_->IsPortableServicePromptSuppressed(); + owner_.portable_service_do_not_remind_ = + owner_.portable_service_prompt_suppressed_; +#endif + + const std::string saved_path = + owner_.config_center_->GetFileTransferSavePath(); + CopyString(owner_.file_transfer_save_path_buf_, saved_path.c_str()); + owner_.file_transfer_save_path_last_ = saved_path; + + owner_.language_button_value_last_ = owner_.language_button_value_; + owner_.video_quality_button_value_last_ = owner_.video_quality_button_value_; + owner_.video_encode_format_button_value_last_ = + owner_.video_encode_format_button_value_; + owner_.enable_hardware_video_codec_last_ = + owner_.enable_hardware_video_codec_; + owner_.enable_turn_last_ = owner_.enable_turn_; + owner_.enable_srtp_last_ = owner_.enable_srtp_; + owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_; + owner_.enable_autostart_last_ = owner_.enable_autostart_; + owner_.enable_minimize_to_tray_last_ = owner_.enable_minimize_to_tray_; + + LOG_INFO("Load settings from cache file"); + return 0; +} + +bool SettingsManager::LoadCachedSelfHostedIdentity() { + std::lock_guard lock(cache_mutex_); + if (!ReadV2Locked() || cache_v2_.self_hosted_id[0] == '\0') { + std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_)); + return false; + } + + CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id); + const char *at_pos = std::strchr(owner_.self_hosted_id_, '@'); + if (at_pos == nullptr) { + CopyString(owner_.client_id_, owner_.self_hosted_id_); + std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_)); + } else { + const std::string id(owner_.self_hosted_id_, + at_pos - owner_.self_hosted_id_); + CopyString(owner_.client_id_, id.c_str()); + CopyString(owner_.password_saved_, at_pos + 1); + } + return true; +} + +void SettingsManager::PersistSelfHostedIdentity(const char *client_id) { + if (!client_id) { + return; + } + + std::lock_guard lock(cache_mutex_); + if (!ReadV2Locked()) { + cache_v2_ = {}; + } + + CopyString(cache_v2_.self_hosted_id, client_id); + CopyString(cache_v2_.client_id_with_password, + owner_.client_id_with_password_); + std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_)); + std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_)); + + std::ofstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc", + std::ios::binary); + if (cache_v2_file) { + cache_v2_file.write(reinterpret_cast(&cache_v2_), + sizeof(cache_v2_)); + } +} + +int SettingsManager::LoadRecentConnectionAliases() { + recent_connection_aliases_.clear(); + + std::ifstream alias_file(owner_.cache_path_ + + "/recent_connection_aliases.json"); + if (!alias_file.good()) { + return 0; + } + + try { + nlohmann::json alias_json; + alias_file >> alias_json; + + const nlohmann::json *aliases = &alias_json; + if (alias_json.contains("aliases") && alias_json["aliases"].is_object()) { + aliases = &alias_json["aliases"]; + } + if (!aliases->is_object()) { + LOG_WARN("Invalid recent connection alias file"); + return -1; + } + + for (auto it = aliases->begin(); it != aliases->end(); ++it) { + if (it.value().is_string()) { + const std::string remote_id = it.key(); + const std::string alias = it.value().get(); + if (!remote_id.empty() && !alias.empty()) { + recent_connection_aliases_[remote_id] = alias; + } + } + } + } catch (const std::exception &e) { + LOG_WARN("Load recent connection aliases failed: {}", e.what()); + return -1; + } + return 0; +} + +int SettingsManager::SaveRecentConnectionAliases() const { + std::error_code ec; + std::filesystem::create_directories(owner_.cache_path_, ec); + if (ec) { + LOG_WARN("Create cache directory failed while saving aliases: {}", + ec.message()); + return -1; + } + + nlohmann::json alias_json; + alias_json["aliases"] = nlohmann::json::object(); + for (const auto &[remote_id, alias] : recent_connection_aliases_) { + if (!remote_id.empty() && !alias.empty()) { + alias_json["aliases"][remote_id] = alias; + } + } + + std::ofstream alias_file( + owner_.cache_path_ + "/recent_connection_aliases.json", std::ios::trunc); + if (!alias_file.good()) { + LOG_WARN("Open recent connection alias file failed"); + return -1; + } + alias_file << alias_json.dump(2); + return 0; +} + +std::string SettingsManager::RecentConnectionDisplayName( + const Thumbnail::RecentConnection &connection) const { + const auto alias_it = recent_connection_aliases_.find(connection.remote_id); + if (alias_it != recent_connection_aliases_.end() && + !alias_it->second.empty()) { + return alias_it->second; + } + if (!connection.remote_host_name.empty() && + connection.remote_host_name != "unknown") { + return connection.remote_host_name; + } + return connection.remote_id; +} + +void SettingsManager::BeginEditRecentConnectionAlias( + const Thumbnail::RecentConnection &connection) { + owner_.edit_connection_alias_remote_id_ = connection.remote_id; + std::memset(owner_.edit_connection_alias_, 0, + sizeof(owner_.edit_connection_alias_)); + + const auto alias_it = recent_connection_aliases_.find(connection.remote_id); + const std::string alias = alias_it != recent_connection_aliases_.end() + ? alias_it->second + : RecentConnectionDisplayName(connection); + CopyString(owner_.edit_connection_alias_, alias.c_str()); + + owner_.focus_on_input_widget_ = true; + owner_.show_edit_connection_alias_window_ = true; +} + +void SettingsManager::SetRecentConnectionAlias(const std::string &remote_id, + const std::string &alias) { + if (!remote_id.empty()) { + recent_connection_aliases_[remote_id] = alias; + } +} + +void SettingsManager::EraseRecentConnectionAlias(const std::string &remote_id) { + recent_connection_aliases_.erase(remote_id); +} + +} // namespace crossdesk diff --git a/src/gui/features/settings/settings_manager.h b/src/gui/features/settings/settings_manager.h new file mode 100644 index 0000000..f907567 --- /dev/null +++ b/src/gui/features/settings/settings_manager.h @@ -0,0 +1,78 @@ +#ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_ +#define CROSSDESK_GUI_SETTINGS_MANAGER_H_ + +#include +#include +#include + +#include "thumbnail.h" + +namespace crossdesk { + +class GuiRuntime; + +// Owns persistent GUI settings and recent-connection aliases. GuiRuntime keeps +// only the runtime/UI state that consumes these values. +class SettingsManager { +public: + explicit SettingsManager(GuiRuntime &owner); + + int Save(); + int Load(); + + int LoadRecentConnectionAliases(); + int SaveRecentConnectionAliases() const; + std::string RecentConnectionDisplayName( + const Thumbnail::RecentConnection &connection) const; + void + BeginEditRecentConnectionAlias(const Thumbnail::RecentConnection &connection); + void SetRecentConnectionAlias(const std::string &remote_id, + const std::string &alias); + void EraseRecentConnectionAlias(const std::string &remote_id); + + // Loads the cached self-hosted identity into the owner's active connection + // fields. Returns true only when a non-empty identity was restored. + bool LoadCachedSelfHostedIdentity(); + void PersistSelfHostedIdentity(const char *client_id); + +private: + struct CacheV1 { + char client_id_with_password[17]; + int language; + int video_quality; + int video_frame_rate; + int video_encode_format; + bool enable_hardware_video_codec; + bool enable_turn; + bool enable_srtp; + unsigned char key[16]; + unsigned char iv[16]; + }; + + struct CacheV2 { + char client_id_with_password[17]; + int language; + int video_quality; + int video_frame_rate; + int video_encode_format; + bool enable_hardware_video_codec; + bool enable_turn; + bool enable_srtp; + unsigned char key[16]; + unsigned char iv[16]; + char self_hosted_id[17]; + }; + + int SaveLocked(); + bool ReadV2Locked(); + + GuiRuntime &owner_; + CacheV1 cache_v1_{}; + CacheV2 cache_v2_{}; + mutable std::mutex cache_mutex_; + std::unordered_map recent_connection_aliases_; +}; + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_SETTINGS_MANAGER_H_ diff --git a/src/gui/tray/linux_tray.cpp b/src/gui/platform/tray/linux_tray.cpp similarity index 100% rename from src/gui/tray/linux_tray.cpp rename to src/gui/platform/tray/linux_tray.cpp diff --git a/src/gui/tray/linux_tray.h b/src/gui/platform/tray/linux_tray.h similarity index 100% rename from src/gui/tray/linux_tray.h rename to src/gui/platform/tray/linux_tray.h diff --git a/src/gui/tray/mac_tray.h b/src/gui/platform/tray/mac_tray.h similarity index 100% rename from src/gui/tray/mac_tray.h rename to src/gui/platform/tray/mac_tray.h diff --git a/src/gui/tray/mac_tray.mm b/src/gui/platform/tray/mac_tray.mm similarity index 100% rename from src/gui/tray/mac_tray.mm rename to src/gui/platform/tray/mac_tray.mm diff --git a/src/gui/tray/win_tray.cpp b/src/gui/platform/tray/win_tray.cpp similarity index 100% rename from src/gui/tray/win_tray.cpp rename to src/gui/platform/tray/win_tray.cpp diff --git a/src/gui/tray/win_tray.h b/src/gui/platform/tray/win_tray.h similarity index 100% rename from src/gui/tray/win_tray.h rename to src/gui/platform/tray/win_tray.h diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 2f5b6fd..62b4848 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -1,3522 +1,15 @@ #include "render.h" -#include - -#if defined(__linux__) && !defined(__APPLE__) -#include -#include -#endif - -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include "device_controller_factory.h" -#include "fa_regular_400.h" -#include "fa_solid_900.h" -#include "file_transfer.h" -#include "layout_relative.h" -#include "localization.h" -#include "platform.h" -#include "rd_log.h" -#include "screen_capturer_factory.h" -#include "version_checker.h" - -#if _WIN32 -#include "interactive_state.h" -#include "service_host.h" -#endif - -#if defined(__APPLE__) -#include "window_util_mac.h" -#endif - -#define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2 +#include "application/gui_application.h" namespace crossdesk { -namespace { -constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500; -constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5); -constexpr auto kConnectionAttemptTimeout = std::chrono::seconds(20); -// Keep a reliable clipboard message within MiniRTC/KCP's single-message -// fragmentation window (MTU is configured to 1200 bytes). -constexpr size_t kMaxClipboardTextBytes = 128 * 1024; +Render::Render() : application_(std::make_unique()) {} -bool IsConnectionAttemptPending(ConnectionStatus status) { - return status == ConnectionStatus::Connecting || - status == ConnectionStatus::Gathering; -} +Render::~Render() = default; -const ImWchar* GetMultilingualGlyphRanges() { - static std::vector glyph_ranges; - if (glyph_ranges.empty()) { - ImGuiIO& io = ImGui::GetIO(); - ImFontGlyphRangesBuilder builder; - builder.AddRanges(io.Fonts->GetGlyphRangesDefault()); - builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull()); - builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic()); +int Render::Run() { return application_->Run(); } - ImVector built_ranges; - builder.BuildRanges(&built_ranges); - glyph_ranges.assign(built_ranges.Data, - built_ranges.Data + built_ranges.Size); - } - return glyph_ranges.empty() ? nullptr : glyph_ranges.data(); -} - -bool CanReadFontFile(const char* font_path) { - if (!font_path) { - return false; - } - - std::ifstream font_file(font_path, std::ios::binary); - return font_file.good(); -} - -#if _WIN32 -HICON LoadTrayIcon() { - HMODULE module = GetModuleHandleW(nullptr); - HICON icon = reinterpret_cast( - LoadImageW(module, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE)); - if (icon) { - return icon; - } - - return LoadIconW(nullptr, IDI_APPLICATION); -} - -struct WindowsServiceInteractiveStatus { - bool available = false; - bool sas_secure_desktop_grace_active = false; - unsigned int error_code = 0; - std::string interactive_stage; - std::string error; -}; - -constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000; -constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000; -constexpr DWORD kWindowsServiceQueryTimeoutMs = 500; -constexpr DWORD kWindowsServiceSasTimeoutMs = 500; - -bool IsTransientWindowsServiceStatusError(const std::string& error) { - return error == "pipe_unavailable" || error == "pipe_connect_failed" || - error == "pipe_read_failed"; -} - -RemoteAction BuildWindowsServiceStatusAction( - const WindowsServiceInteractiveStatus& status) { - RemoteAction action{}; - action.type = ControlType::service_status; - action.ss.available = status.available; - std::strncpy(action.ss.interactive_stage, status.interactive_stage.c_str(), - sizeof(action.ss.interactive_stage) - 1); - action.ss.interactive_stage[sizeof(action.ss.interactive_stage) - 1] = '\0'; - return action; -} - -bool QueryWindowsServiceInteractiveStatus( - WindowsServiceInteractiveStatus* status) { - if (status == nullptr) { - return false; - } - - *status = WindowsServiceInteractiveStatus{}; - const std::string response = - QueryCrossDeskService("status", kWindowsServiceQueryTimeoutMs); - auto json = nlohmann::json::parse(response, nullptr, false); - if (json.is_discarded() || !json.is_object()) { - status->error = "invalid_service_status_json"; - return false; - } - - status->available = json.value("ok", false); - if (!status->available) { - status->error = json.value("error", std::string("service_unavailable")); - status->error_code = json.value("code", 0u); - return true; - } - - status->interactive_stage = json.value("interactive_stage", std::string()); - status->sas_secure_desktop_grace_active = - json.value("sas_secure_desktop_grace_active", false); - - if (ShouldNormalizeUnlockToUserDesktop( - json.value("interactive_lock_screen_visible", false), - status->interactive_stage, json.value("session_locked", false), - json.value("interactive_logon_ui_visible", false), - json.value("interactive_secure_desktop_active", - json.value("secure_desktop_active", false)), - json.value("credential_ui_visible", false), - json.value("password_box_visible", false), - json.value("unlock_ui_visible", false), - json.value("last_session_event", std::string()))) { - status->interactive_stage = "user-desktop"; - } - return true; -} -#endif - -#if defined(__linux__) && !defined(__APPLE__) -inline bool X11GetDisplayAndWindow(SDL_Window* window, Display** display_out, - ::Window* x11_window_out) { - if (!window || !display_out || !x11_window_out) { - return false; - } - -#if !defined(SDL_PROP_WINDOW_X11_DISPLAY_POINTER) || \ - !defined(SDL_PROP_WINDOW_X11_WINDOW_NUMBER) - // SDL build does not expose X11 window properties. - return false; -#else - SDL_PropertiesID props = SDL_GetWindowProperties(window); - Display* display = (Display*)SDL_GetPointerProperty( - props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL); - const Sint64 x11_window_num = - SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0); - const ::Window x11_window = (::Window)x11_window_num; - - if (!display || !x11_window) { - return false; - } - - *display_out = display; - *x11_window_out = x11_window; - return true; -#endif -} - -inline void X11SendNetWmState(Display* display, ::Window x11_window, - long action, Atom state1, Atom state2 = 0) { - if (!display || !x11_window) { - return; - } - - const Atom wm_state = XInternAtom(display, "_NET_WM_STATE", False); - - XEvent event; - memset(&event, 0, sizeof(event)); - event.xclient.type = ClientMessage; - event.xclient.serial = 0; - event.xclient.send_event = True; - event.xclient.message_type = wm_state; - event.xclient.window = x11_window; - event.xclient.format = 32; - event.xclient.data.l[0] = action; - event.xclient.data.l[1] = (long)state1; - event.xclient.data.l[2] = (long)state2; - event.xclient.data.l[3] = 1; // normal source indication - event.xclient.data.l[4] = 0; - - XSendEvent(display, DefaultRootWindow(display), False, - SubstructureRedirectMask | SubstructureNotifyMask, &event); -} - -inline void X11SetWindowTypeUtility(Display* display, ::Window x11_window) { - if (!display || !x11_window) { - return; - } - - const Atom wm_window_type = - XInternAtom(display, "_NET_WM_WINDOW_TYPE", False); - const Atom wm_window_type_utility = - XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False); - - XChangeProperty(display, x11_window, wm_window_type, XA_ATOM, 32, - PropModeReplace, (unsigned char*)&wm_window_type_utility, 1); -} - -inline void X11SetWindowAlwaysOnTop(SDL_Window* window) { - Display* display = nullptr; - ::Window x11_window = 0; - if (!X11GetDisplayAndWindow(window, &display, &x11_window)) { - return; - } - - const Atom state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False); - const Atom state_stays_on_top = - XInternAtom(display, "_NET_WM_STATE_STAYS_ON_TOP", False); - - // Request _NET_WM_STATE_ADD for ABOVE + STAYS_ON_TOP. - X11SendNetWmState(display, x11_window, 1, state_above, state_stays_on_top); - XFlush(display); -} - -inline void X11SetWindowSkipTaskbar(SDL_Window* window) { - Display* display = nullptr; - ::Window x11_window = 0; - if (!X11GetDisplayAndWindow(window, &display, &x11_window)) { - return; - } - - const Atom skip_taskbar = - XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", False); - const Atom skip_pager = - XInternAtom(display, "_NET_WM_STATE_SKIP_PAGER", False); - - // Request _NET_WM_STATE_ADD for SKIP_TASKBAR + SKIP_PAGER. - X11SendNetWmState(display, x11_window, 1, skip_taskbar, skip_pager); - - // Hint the WM that this is an auxiliary/utility window. - X11SetWindowTypeUtility(display, x11_window); - - XFlush(display); -} -#endif -} // namespace - -std::vector Render::SerializeRemoteAction(const RemoteAction& action) { - std::vector buffer; - buffer.push_back(static_cast(action.type)); - - auto insert_bytes = [&](const void* ptr, size_t len) { - buffer.insert(buffer.end(), (const char*)ptr, (const char*)ptr + len); - }; - - if (action.type == ControlType::host_infomation) { - insert_bytes(&action.i.host_name_size, sizeof(size_t)); - insert_bytes(action.i.host_name, action.i.host_name_size); - - size_t num = action.i.display_num; - insert_bytes(&num, sizeof(size_t)); - - for (size_t i = 0; i < num; ++i) { - size_t len = strlen(action.i.display_list[i]); - insert_bytes(&len, sizeof(size_t)); - insert_bytes(action.i.display_list[i], len); - } - - insert_bytes(action.i.left, sizeof(int) * num); - insert_bytes(action.i.top, sizeof(int) * num); - insert_bytes(action.i.right, sizeof(int) * num); - insert_bytes(action.i.bottom, sizeof(int) * num); - } - - return buffer; -} - -bool Render::DeserializeRemoteAction(const char* data, size_t size, - RemoteAction& out) { - size_t offset = 0; - auto read = [&](void* dst, size_t len) -> bool { - if (offset + len > size) return false; - memcpy(dst, data + offset, len); - offset += len; - return true; - }; - - if (size < 1) return false; - out.type = static_cast(data[offset++]); - - if (out.type == ControlType::host_infomation) { - size_t name_len; - if (!read(&name_len, sizeof(size_t)) || name_len >= sizeof(out.i.host_name)) - return false; - if (!read(out.i.host_name, name_len)) return false; - out.i.host_name[name_len] = '\0'; - out.i.host_name_size = name_len; - - size_t num; - if (!read(&num, sizeof(size_t))) return false; - out.i.display_num = num; - - out.i.display_list = (char**)malloc(num * sizeof(char*)); - for (size_t i = 0; i < num; ++i) { - size_t len; - if (!read(&len, sizeof(size_t))) return false; - if (offset + len > size) return false; - out.i.display_list[i] = (char*)malloc(len + 1); - memcpy(out.i.display_list[i], data + offset, len); - out.i.display_list[i][len] = '\0'; - offset += len; - } - - auto alloc_int_array = [&](int*& arr) { - arr = (int*)malloc(num * sizeof(int)); - return read(arr, num * sizeof(int)); - }; - - return alloc_int_array(out.i.left) && alloc_int_array(out.i.top) && - alloc_int_array(out.i.right) && alloc_int_array(out.i.bottom); - } - - return true; -} - -void Render::FreeRemoteAction(RemoteAction& action) { - if (action.type == ControlType::host_infomation) { - for (size_t i = 0; i < action.i.display_num; ++i) { - free(action.i.display_list[i]); - } - free(action.i.display_list); - free(action.i.left); - free(action.i.top); - free(action.i.right); - free(action.i.bottom); - - action.i.display_list = nullptr; - action.i.left = action.i.top = action.i.right = action.i.bottom = nullptr; - action.i.display_num = 0; - } -} - -SDL_HitTestResult Render::HitTestCallback(SDL_Window* window, - const SDL_Point* area, void* data) { - Render* render = (Render*)data; - if (!render) { - return SDL_HITTEST_NORMAL; - } - - if (render->fullscreen_button_pressed_) { - return SDL_HITTEST_NORMAL; - } - - // Server window: OS-level dragging for the title bar, but keep the left-side - // collapse/expand button clickable. - if (render->server_window_ && window == render->server_window_) { - const float title_h = render->server_window_title_bar_height_; - const float button_w = title_h; - if (area->y >= 0 && area->y < title_h) { - if (area->x >= 0 && area->x < button_w) { - return SDL_HITTEST_NORMAL; - } - return SDL_HITTEST_DRAGGABLE; - } - return SDL_HITTEST_NORMAL; - } - - int window_width, window_height; - SDL_GetWindowSize(window, &window_width, &window_height); - - // check if curosor is in tab bar - if (render->stream_window_inited_ && render->stream_window_created_ && - !render->fullscreen_button_pressed_ && render->stream_ctx_) { - ImGuiContext* prev_ctx = ImGui::GetCurrentContext(); - ImGui::SetCurrentContext(render->stream_ctx_); - - ImGuiWindow* tab_bar_window = ImGui::FindWindowByName("TabBar"); - if (tab_bar_window && tab_bar_window->Active) { - ImGuiIO& io = ImGui::GetIO(); - float scale_x = io.DisplayFramebufferScale.x; - float scale_y = io.DisplayFramebufferScale.y; - - float tab_bar_x = tab_bar_window->Pos.x * scale_x; - float tab_bar_y = tab_bar_window->Pos.y * scale_y; - float tab_bar_width = tab_bar_window->Size.x * scale_x; - float tab_bar_height = tab_bar_window->Size.y * scale_y; - - ImGui::SetCurrentContext(prev_ctx); - - if (area->x >= tab_bar_x && area->x <= tab_bar_x + tab_bar_width && - area->y >= tab_bar_y && area->y <= tab_bar_y + tab_bar_height) { - return SDL_HITTEST_NORMAL; - } - } else { - ImGui::SetCurrentContext(prev_ctx); - } - } - - float mouse_grab_padding = render->title_bar_button_width_ * 0.16f; - if (area->y < render->title_bar_button_width_ && - area->y > mouse_grab_padding && - area->x < window_width - render->title_bar_button_width_ * 3.0f && - area->x > mouse_grab_padding) { - return SDL_HITTEST_DRAGGABLE; - } - - // if (!render->streaming_) { - // return SDL_HITTEST_NORMAL; - // } - - if (area->y < mouse_grab_padding) { - if (area->x < mouse_grab_padding) { - return SDL_HITTEST_RESIZE_TOPLEFT; - } else if (area->x > window_width - mouse_grab_padding) { - return SDL_HITTEST_RESIZE_TOPRIGHT; - } else { - return SDL_HITTEST_RESIZE_TOP; - } - } else if (area->y > window_height - mouse_grab_padding) { - if (area->x < mouse_grab_padding) { - return SDL_HITTEST_RESIZE_BOTTOMLEFT; - } else if (area->x > window_width - mouse_grab_padding) { - return SDL_HITTEST_RESIZE_BOTTOMRIGHT; - } else { - return SDL_HITTEST_RESIZE_BOTTOM; - } - } else if (area->x < mouse_grab_padding) { - return SDL_HITTEST_RESIZE_LEFT; - } else if (area->x > window_width - mouse_grab_padding) { - return SDL_HITTEST_RESIZE_RIGHT; - } - - return SDL_HITTEST_NORMAL; -} - -Render::Render() : last_rejoin_check_time_(std::chrono::steady_clock::now()) {} - -Render::~Render() {} - -int Render::SaveSettingsIntoCacheFile() { - cd_cache_mutex_.lock(); - - std::ofstream cd_cache_v2_file(cache_path_ + "/secure_cache_v2.enc", - std::ios::binary); - if (!cd_cache_v2_file) { - cd_cache_mutex_.unlock(); - return -1; - } - - memset(&cd_cache_v2_.client_id_with_password, 0, - sizeof(cd_cache_v2_.client_id_with_password)); - memcpy(cd_cache_v2_.client_id_with_password, client_id_with_password_, - sizeof(client_id_with_password_)); - memcpy(&cd_cache_v2_.key, &aes128_key_, sizeof(aes128_key_)); - memcpy(&cd_cache_v2_.iv, &aes128_iv_, sizeof(aes128_iv_)); - - memset(&cd_cache_v2_.self_hosted_id, 0, sizeof(cd_cache_v2_.self_hosted_id)); - memcpy(cd_cache_v2_.self_hosted_id, self_hosted_id_, sizeof(self_hosted_id_)); - - cd_cache_v2_file.write(reinterpret_cast(&cd_cache_v2_), - sizeof(CDCacheV2)); - cd_cache_v2_file.close(); - - std::ofstream cd_cache_file(cache_path_ + "/secure_cache.enc", - std::ios::binary); - if (cd_cache_file) { - memset(&cd_cache_.client_id_with_password, 0, - sizeof(cd_cache_.client_id_with_password)); - memcpy(cd_cache_.client_id_with_password, client_id_with_password_, - sizeof(client_id_with_password_)); - memcpy(&cd_cache_.key, &aes128_key_, sizeof(aes128_key_)); - memcpy(&cd_cache_.iv, &aes128_iv_, sizeof(aes128_iv_)); - cd_cache_file.write(reinterpret_cast(&cd_cache_), sizeof(CDCache)); - cd_cache_file.close(); - } - - cd_cache_mutex_.unlock(); - - return 0; -} - -int Render::LoadSettingsFromCacheFile() { - cd_cache_mutex_.lock(); - - std::ifstream cd_cache_v2_file(cache_path_ + "/secure_cache_v2.enc", - std::ios::binary); - bool v2_file_exists = cd_cache_v2_file.good(); - - if (v2_file_exists) { - cd_cache_v2_file.read(reinterpret_cast(&cd_cache_v2_), - sizeof(CDCacheV2)); - cd_cache_v2_file.close(); - - memset(&client_id_with_password_, 0, sizeof(client_id_with_password_)); - memcpy(client_id_with_password_, cd_cache_v2_.client_id_with_password, - sizeof(client_id_with_password_)); - - memset(&self_hosted_id_, 0, sizeof(self_hosted_id_)); - memcpy(self_hosted_id_, cd_cache_v2_.self_hosted_id, - sizeof(self_hosted_id_)); - - memcpy(aes128_key_, cd_cache_v2_.key, sizeof(cd_cache_v2_.key)); - memcpy(aes128_iv_, cd_cache_v2_.iv, sizeof(cd_cache_v2_.iv)); - - LOG_INFO("Load settings from v2 cache file"); - } else { - std::ifstream cd_cache_file(cache_path_ + "/secure_cache.enc", - std::ios::binary); - if (!cd_cache_file) { - cd_cache_mutex_.unlock(); - - memset(password_saved_, 0, sizeof(password_saved_)); - memset(aes128_key_, 0, sizeof(aes128_key_)); - memset(aes128_iv_, 0, sizeof(aes128_iv_)); - memset(self_hosted_id_, 0, sizeof(self_hosted_id_)); - - thumbnail_.reset(); - thumbnail_ = std::make_shared(cache_path_ + "/thumbnails/"); - thumbnail_->GetKeyAndIv(aes128_key_, aes128_iv_); - thumbnail_->DeleteAllFilesInDirectory(); - - SaveSettingsIntoCacheFile(); - - return -1; - } - - cd_cache_file.read(reinterpret_cast(&cd_cache_), sizeof(CDCache)); - cd_cache_file.close(); - - memset(&cd_cache_v2_.client_id_with_password, 0, - sizeof(cd_cache_v2_.client_id_with_password)); - memcpy(cd_cache_v2_.client_id_with_password, - cd_cache_.client_id_with_password, - sizeof(cd_cache_.client_id_with_password)); - memcpy(&cd_cache_v2_.key, &cd_cache_.key, sizeof(cd_cache_.key)); - memcpy(&cd_cache_v2_.iv, &cd_cache_.iv, sizeof(cd_cache_.iv)); - - memset(&cd_cache_v2_.self_hosted_id, 0, - sizeof(cd_cache_v2_.self_hosted_id)); - - memset(&client_id_with_password_, 0, sizeof(client_id_with_password_)); - memcpy(client_id_with_password_, cd_cache_.client_id_with_password, - sizeof(client_id_with_password_)); - - memset(&self_hosted_id_, 0, sizeof(self_hosted_id_)); - - memcpy(aes128_key_, cd_cache_.key, sizeof(cd_cache_.key)); - memcpy(aes128_iv_, cd_cache_.iv, sizeof(cd_cache_.iv)); - - cd_cache_mutex_.unlock(); - SaveSettingsIntoCacheFile(); - cd_cache_mutex_.lock(); - - LOG_INFO("Migrated settings from v1 to v2 cache file"); - } - - cd_cache_mutex_.unlock(); - - if (strchr(client_id_with_password_, '@') != nullptr) { - std::string id, password; - const char* at_pos = strchr(client_id_with_password_, '@'); - if (at_pos == nullptr) { - id = client_id_with_password_; - password.clear(); - } else { - id.assign(client_id_with_password_, at_pos - client_id_with_password_); - password = at_pos + 1; - } - - memset(&client_id_, 0, sizeof(client_id_)); - strncpy(client_id_, id.c_str(), sizeof(client_id_) - 1); - client_id_[sizeof(client_id_) - 1] = '\0'; - - memset(&password_saved_, 0, sizeof(password_saved_)); - strncpy(password_saved_, password.c_str(), sizeof(password_saved_) - 1); - password_saved_[sizeof(password_saved_) - 1] = '\0'; - } - - thumbnail_.reset(); - thumbnail_ = std::make_shared(cache_path_ + "/thumbnails/", - aes128_key_, aes128_iv_); - - language_button_value_ = localization::detail::ClampLanguageIndex( - (int)config_center_->GetLanguage()); - video_quality_button_value_ = (int)config_center_->GetVideoQuality(); - video_frame_rate_button_value_ = (int)config_center_->GetVideoFrameRate(); - video_encode_format_button_value_ = - (int)config_center_->GetVideoEncodeFormat(); - enable_hardware_video_codec_ = config_center_->IsHardwareVideoCodec(); - enable_turn_ = config_center_->IsEnableTurn(); - enable_srtp_ = config_center_->IsEnableSrtp(); - enable_self_hosted_ = config_center_->IsSelfHosted(); - enable_autostart_ = config_center_->IsEnableAutostart(); - enable_daemon_ = config_center_->IsEnableDaemon(); - enable_minimize_to_tray_ = config_center_->IsMinimizeToTray(); -#if _WIN32 && CROSSDESK_PORTABLE - portable_service_prompt_suppressed_ = - config_center_->IsPortableServicePromptSuppressed(); - portable_service_do_not_remind_ = portable_service_prompt_suppressed_; -#endif - - // File transfer save path - { - std::string saved_path = config_center_->GetFileTransferSavePath(); - strncpy(file_transfer_save_path_buf_, saved_path.c_str(), - sizeof(file_transfer_save_path_buf_) - 1); - file_transfer_save_path_buf_[sizeof(file_transfer_save_path_buf_) - 1] = - '\0'; - file_transfer_save_path_last_ = saved_path; - } - - language_button_value_last_ = language_button_value_; - video_quality_button_value_last_ = video_quality_button_value_; - video_encode_format_button_value_last_ = video_encode_format_button_value_; - enable_hardware_video_codec_last_ = enable_hardware_video_codec_; - enable_turn_last_ = enable_turn_; - enable_srtp_last_ = enable_srtp_; - enable_self_hosted_last_ = enable_self_hosted_; - enable_autostart_last_ = enable_autostart_; - enable_minimize_to_tray_last_ = enable_minimize_to_tray_; - - LOG_INFO("Load settings from cache file"); - - return 0; -} - -int Render::LoadRecentConnectionAliases() { - recent_connection_aliases_.clear(); - - std::ifstream alias_file(cache_path_ + "/recent_connection_aliases.json"); - if (!alias_file.good()) { - return 0; - } - - try { - nlohmann::json alias_json; - alias_file >> alias_json; - - const nlohmann::json* aliases = &alias_json; - if (alias_json.contains("aliases") && alias_json["aliases"].is_object()) { - aliases = &alias_json["aliases"]; - } - - if (!aliases->is_object()) { - LOG_WARN("Invalid recent connection alias file"); - return -1; - } - - for (auto it = aliases->begin(); it != aliases->end(); ++it) { - if (!it.value().is_string()) { - continue; - } - - std::string remote_id = it.key(); - std::string alias = it.value().get(); - if (!remote_id.empty() && !alias.empty()) { - recent_connection_aliases_[remote_id] = alias; - } - } - } catch (const std::exception& e) { - LOG_WARN("Load recent connection aliases failed: {}", e.what()); - return -1; - } - - return 0; -} - -int Render::SaveRecentConnectionAliases() const { - std::error_code ec; - std::filesystem::create_directories(cache_path_, ec); - if (ec) { - LOG_WARN("Create cache directory failed while saving aliases: {}", - ec.message()); - return -1; - } - - nlohmann::json alias_json; - alias_json["aliases"] = nlohmann::json::object(); - - for (const auto& [remote_id, alias] : recent_connection_aliases_) { - if (!remote_id.empty() && !alias.empty()) { - alias_json["aliases"][remote_id] = alias; - } - } - - std::ofstream alias_file(cache_path_ + "/recent_connection_aliases.json", - std::ios::trunc); - if (!alias_file.good()) { - LOG_WARN("Open recent connection alias file failed"); - return -1; - } - - alias_file << alias_json.dump(2); - return 0; -} - -std::string Render::GetRecentConnectionDisplayName( - const Thumbnail::RecentConnection& connection) const { - const auto alias_it = recent_connection_aliases_.find(connection.remote_id); - if (alias_it != recent_connection_aliases_.end() && - !alias_it->second.empty()) { - return alias_it->second; - } - - if (!connection.remote_host_name.empty() && - connection.remote_host_name != "unknown") { - return connection.remote_host_name; - } - - return connection.remote_id; -} - -void Render::BeginEditRecentConnectionAlias( - const Thumbnail::RecentConnection& connection) { - edit_connection_alias_remote_id_ = connection.remote_id; - memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_)); - - const auto alias_it = recent_connection_aliases_.find(connection.remote_id); - std::string alias = alias_it != recent_connection_aliases_.end() - ? alias_it->second - : GetRecentConnectionDisplayName(connection); - - if (!alias.empty()) { - strncpy(edit_connection_alias_, alias.c_str(), - sizeof(edit_connection_alias_) - 1); - edit_connection_alias_[sizeof(edit_connection_alias_) - 1] = '\0'; - } - - focus_on_input_widget_ = true; - show_edit_connection_alias_window_ = true; -} - -int Render::ScreenCapturerInit() { -#ifdef __APPLE__ - if (!EnsureMacScreenRecordingPermission()) { - return -1; - } -#endif - - if (!screen_capturer_) { - screen_capturer_ = (ScreenCapturer*)screen_capturer_factory_->Create(); - } - - last_frame_time_ = std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count(); - int fps = config_center_->GetVideoFrameRate() == - ConfigCenter::VIDEO_FRAME_RATE::FPS_30 - ? 30 - : 60; - LOG_INFO("Init screen capturer with {} fps", fps); - - int screen_capturer_init_ret = screen_capturer_->Init( - fps, - [this, fps](unsigned char* data, int size, int width, int height, - const char* display_name) -> void { - const auto now_time = static_cast( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count()); - auto duration = now_time - last_frame_time_; - if (duration * fps >= 1000) { // ~60 FPS - const std::string stream_id = display_name ? display_name : ""; - const bool resumed_after_gap = - last_frame_time_ != 0 && duration >= kCaptureResumeKeyFrameGapMs; - const bool stream_changed = !last_video_frame_stream_id_.empty() && - last_video_frame_stream_id_ != stream_id; - if (resumed_after_gap || stream_changed) { - if (RequestVideoKeyFrame(peer_, stream_id.c_str()) == 0) { - LOG_INFO( - "Request video key frame before sending captured frame, " - "stream='{}', gap_ms={}, stream_changed={}", - stream_id, duration, stream_changed); - } - } - XVideoFrame frame; - frame.data = (const char*)data; - frame.size = size; - frame.width = width; - frame.height = height; - frame.captured_timestamp = GetSystemTimeMicros(peer_); - SendVideoFrame(peer_, &frame, stream_id.c_str()); - last_video_frame_stream_id_ = stream_id; - last_frame_time_ = now_time; - } - }); - - if (0 == screen_capturer_init_ret) { - LOG_INFO("Init screen capturer success"); - const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); - if (!latest_display_info.empty()) { - display_info_list_ = latest_display_info; - } - return 0; - } else { - LOG_ERROR("Init screen capturer failed"); - screen_capturer_->Destroy(); - delete screen_capturer_; - screen_capturer_ = nullptr; - return -1; - } -} - -int Render::StartScreenCapturer() { -#ifdef __APPLE__ - if (!EnsureMacScreenRecordingPermission()) { - return -1; - } -#endif - - if (!screen_capturer_) { - LOG_INFO("Screen capturer instance missing, recreating before start"); - if (0 != ScreenCapturerInit()) { - LOG_ERROR("Recreate screen capturer failed"); - return -1; - } - } - - if (screen_capturer_) { - LOG_INFO("Start screen capturer, show cursor: {}", show_cursor_); - - const int ret = screen_capturer_->Start(show_cursor_); - if (ret != 0) { - LOG_ERROR("Start screen capturer failed: {}", ret); - return ret; - } - } - - return 0; -} - -int Render::StopScreenCapturer() { - if (screen_capturer_) { - LOG_INFO("Stop screen capturer"); - screen_capturer_->Stop(); - } - - return 0; -} - -int Render::StartSpeakerCapturer() { - if (!speaker_capturer_) { - speaker_capturer_ = (SpeakerCapturer*)speaker_capturer_factory_->Create(); - int speaker_capturer_init_ret = - speaker_capturer_->Init([this](unsigned char* data, size_t size, - const char* audio_name) -> void { - SendAudioFrame(peer_, (const char*)data, size, audio_label_.c_str()); - }); - - if (0 != speaker_capturer_init_ret) { - speaker_capturer_->Destroy(); - delete speaker_capturer_; - speaker_capturer_ = nullptr; - } - } - - if (speaker_capturer_) { - const int ret = speaker_capturer_->Start(); - if (ret != 0) { - LOG_ERROR("Start speaker capturer failed: {}", ret); - return ret; - } - start_speaker_capturer_ = true; - return 0; - } - - return -1; -} - -int Render::StopSpeakerCapturer() { - if (speaker_capturer_) { - speaker_capturer_->Stop(); - start_speaker_capturer_ = false; - } - - return 0; -} - -int Render::StartMouseController() { -#ifdef __APPLE__ - if (!EnsureMacAccessibilityPermission()) { - return -1; - } -#endif - - if (!device_controller_factory_) { - LOG_INFO("Device controller factory is nullptr"); - return -1; - } - -#if defined(__linux__) && !defined(__APPLE__) - if (IsWaylandSession()) { - if (!screen_capturer_) { - return 1; - } - - const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); - if (latest_display_info.empty() || - latest_display_info[0].handle == nullptr) { - return 1; - } - } - - if (screen_capturer_) { - const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); - if (!latest_display_info.empty()) { - display_info_list_ = latest_display_info; - } - } -#endif - - mouse_controller_ = (MouseController*)device_controller_factory_->Create( - DeviceControllerFactory::Device::Mouse); - if (!mouse_controller_) { - LOG_ERROR("Create mouse controller failed"); - return -1; - } - - int mouse_controller_init_ret = mouse_controller_->Init(display_info_list_); - if (0 != mouse_controller_init_ret) { - LOG_INFO("Destroy mouse controller"); - mouse_controller_->Destroy(); - delete mouse_controller_; - mouse_controller_ = nullptr; - return mouse_controller_init_ret; - } - - return 0; -} - -int Render::StopMouseController() { - if (mouse_controller_) { - mouse_controller_->Destroy(); - delete mouse_controller_; - mouse_controller_ = nullptr; - } - return 0; -} - -int Render::StartKeyboardCapturer() { - keyboard_capturer_uses_sdl_events_ = false; - -#ifdef __APPLE__ - if (!EnsureMacAccessibilityPermission()) { - keyboard_capturer_uses_sdl_events_ = true; - return 0; - } -#endif - -#if defined(__linux__) && !defined(__APPLE__) - if (IsWaylandSession()) { - keyboard_capturer_uses_sdl_events_ = true; - LOG_INFO("Start keyboard capturer with SDL Wayland backend"); - return 0; - } -#endif - - if (!keyboard_capturer_) { - keyboard_capturer_uses_sdl_events_ = true; - LOG_WARN( - "keyboard capturer is nullptr, falling back to SDL keyboard events"); - return 0; - } - - int keyboard_capturer_init_ret = keyboard_capturer_->Hook( - [](int key_code, bool is_down, uint32_t scan_code, bool extended, - void* user_ptr) { - if (user_ptr) { - Render* render = (Render*)user_ptr; - render->SendKeyCommand(key_code, is_down, scan_code, extended); - } - }, - this); - if (0 != keyboard_capturer_init_ret) { - keyboard_capturer_uses_sdl_events_ = true; - LOG_WARN( - "Start keyboard capturer failed, falling back to SDL keyboard " - "events"); - } else { - LOG_INFO("Start keyboard capturer with native hook"); - } - - return 0; -} - -int Render::StopKeyboardCapturer() { - if (keyboard_capturer_uses_sdl_events_) { - keyboard_capturer_uses_sdl_events_ = false; - LOG_INFO("Stop keyboard capturer with SDL keyboard backend"); - return 0; - } - - if (keyboard_capturer_) { - keyboard_capturer_->Unhook(); - LOG_INFO("Stop keyboard capturer"); - } - return 0; -} - -int Render::CreateConnectionPeer() { - params_.use_cfg_file = false; - - std::string signal_server_ip; - int signal_server_port; - int coturn_server_port; - - if (config_center_->IsSelfHosted()) { - signal_server_ip = config_center_->GetSignalServerHost(); - signal_server_port = config_center_->GetSignalServerPort(); - coturn_server_port = config_center_->GetCoturnServerPort(); - - std::string current_self_hosted_ip = config_center_->GetSignalServerHost(); - bool use_cached_id = false; - - // Check secure_cache_v2.enc exists or not - std::ifstream v2_file(cache_path_ + "/secure_cache_v2.enc", - std::ios::binary); - if (v2_file.good()) { - CDCacheV2 temp_cache; - v2_file.read(reinterpret_cast(&temp_cache), sizeof(CDCacheV2)); - v2_file.close(); - - if (strlen(temp_cache.self_hosted_id) > 0) { - memset(&self_hosted_id_, 0, sizeof(self_hosted_id_)); - strncpy(self_hosted_id_, temp_cache.self_hosted_id, - sizeof(self_hosted_id_) - 1); - self_hosted_id_[sizeof(self_hosted_id_) - 1] = '\0'; - use_cached_id = true; - - std::string id, password; - const char* at_pos = strchr(self_hosted_id_, '@'); - if (at_pos == nullptr) { - id = self_hosted_id_; - password.clear(); - } else { - id.assign(self_hosted_id_, at_pos - self_hosted_id_); - password = at_pos + 1; - } - - memset(&client_id_, 0, sizeof(client_id_)); - strncpy(client_id_, id.c_str(), sizeof(client_id_) - 1); - client_id_[sizeof(client_id_) - 1] = '\0'; - - memset(&password_saved_, 0, sizeof(password_saved_)); - strncpy(password_saved_, password.c_str(), sizeof(password_saved_) - 1); - password_saved_[sizeof(password_saved_) - 1] = '\0'; - } - } else { - memset(&self_hosted_id_, 0, sizeof(self_hosted_id_)); - LOG_INFO( - "secure_cache_v2.enc not found, will use empty id to get new id from " - "server"); - } - - if (use_cached_id && strlen(self_hosted_id_) > 0) { - memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_)); - strncpy(self_hosted_user_id_, self_hosted_id_, - sizeof(self_hosted_user_id_) - 1); - self_hosted_user_id_[sizeof(self_hosted_user_id_) - 1] = '\0'; - params_.user_id = self_hosted_user_id_; - } else { - memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_)); - params_.user_id = self_hosted_user_id_; - LOG_INFO( - "Using empty id for self-hosted server, server will assign new id"); - } - } else { - signal_server_ip = config_center_->GetDefaultServerHost(); - signal_server_port = config_center_->GetDefaultSignalServerPort(); - coturn_server_port = config_center_->GetDefaultCoturnServerPort(); - params_.user_id = client_id_with_password_; - } - - // self hosted server config - strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(), - sizeof(signal_server_ip_self_) - 1); - signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0'; - int signal_port = config_center_->GetSignalServerPort(); - if (signal_port > 0) { - strncpy(signal_server_port_self_, std::to_string(signal_port).c_str(), - sizeof(signal_server_port_self_) - 1); - signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0'; - } else { - signal_server_port_self_[0] = '\0'; - } - int coturn_port = config_center_->GetCoturnServerPort(); - if (coturn_port > 0) { - strncpy(coturn_server_port_self_, std::to_string(coturn_port).c_str(), - sizeof(coturn_server_port_self_) - 1); - coturn_server_port_self_[sizeof(coturn_server_port_self_) - 1] = '\0'; - } else { - coturn_server_port_self_[0] = '\0'; - } - - // peer config - strncpy((char*)params_.signal_server_ip, signal_server_ip.c_str(), - sizeof(params_.signal_server_ip) - 1); - params_.signal_server_ip[sizeof(params_.signal_server_ip) - 1] = '\0'; - params_.signal_server_port = signal_server_port; - strncpy((char*)params_.stun_server_ip, signal_server_ip.c_str(), - sizeof(params_.stun_server_ip) - 1); - params_.stun_server_ip[sizeof(params_.stun_server_ip) - 1] = '\0'; - params_.stun_server_port = coturn_server_port; - strncpy((char*)params_.turn_server_ip, signal_server_ip.c_str(), - sizeof(params_.turn_server_ip) - 1); - params_.turn_server_ip[sizeof(params_.turn_server_ip) - 1] = '\0'; - params_.turn_server_port = coturn_server_port; - strncpy((char*)params_.turn_server_username, "crossdesk", - sizeof(params_.turn_server_username) - 1); - params_.turn_server_username[sizeof(params_.turn_server_username) - 1] = '\0'; - strncpy((char*)params_.turn_server_password, "crossdeskpw", - sizeof(params_.turn_server_password) - 1); - params_.turn_server_password[sizeof(params_.turn_server_password) - 1] = '\0'; - - strncpy(params_.log_path, dll_log_path_.c_str(), - sizeof(params_.log_path) - 1); - params_.log_path[sizeof(params_.log_path) - 1] = '\0'; - params_.hardware_acceleration = config_center_->IsHardwareVideoCodec(); - params_.av1_encoding = config_center_->GetVideoEncodeFormat() == - ConfigCenter::VIDEO_ENCODE_FORMAT::AV1 - ? true - : false; - params_.turn_mode = static_cast(config_center_->GetTurnMode()); - params_.enable_srtp = config_center_->IsEnableSrtp(); - params_.video_quality = - static_cast(config_center_->GetVideoQuality()); - params_.on_receive_video_buffer = nullptr; - params_.on_receive_audio_buffer = OnReceiveAudioBufferCb; - params_.on_receive_data_buffer = OnReceiveDataBufferCb; - - params_.on_receive_video_frame = OnReceiveVideoBufferCb; - - params_.on_signal_status = OnSignalStatusCb; - params_.on_signal_message = OnSignalMessageCb; - params_.on_connection_status = OnConnectionStatusCb; - params_.on_net_status_report = OnNetStatusReport; - - params_.user_data = this; - - peer_ = CreatePeer(¶ms_); - if (peer_) { - LOG_INFO("Create peer instance [{}] successful", client_id_); - Init(peer_); - LOG_INFO("Peer [{}] init finish", client_id_); - } else { - LOG_INFO("Create peer [{}] instance failed", client_id_); - } - - if (0 == ScreenCapturerInit()) { - for (auto& display_info : display_info_list_) { - AddVideoStream(peer_, display_info.name.c_str()); - } - - AddAudioStream(peer_, audio_label_.c_str()); - AddDataStream(peer_, data_label_.c_str(), false); - AddDataStream(peer_, mouse_label_.c_str(), false); - AddDataStream(peer_, keyboard_label_.c_str(), true); - AddDataStream(peer_, control_data_label_.c_str(), true); - AddDataStream(peer_, file_label_.c_str(), true); - AddDataStream(peer_, file_feedback_label_.c_str(), true); - AddDataStream(peer_, clipboard_label_.c_str(), true); - return 0; - } else { - return -1; - } -} - -int Render::AudioDeviceInit() { - SDL_AudioSpec desired_out{}; - desired_out.freq = 48000; - desired_out.format = SDL_AUDIO_S16; - desired_out.channels = 1; - - auto open_stream = [&]() -> bool { - output_stream_ = SDL_OpenAudioDeviceStream( - SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_out, nullptr, nullptr); - return output_stream_ != nullptr; - }; - - if (!open_stream()) { -#if defined(__linux__) && !defined(__APPLE__) - LOG_WARN( - "Failed to open output stream with driver [{}]: {}", - getenv("SDL_AUDIODRIVER") ? getenv("SDL_AUDIODRIVER") : "(default)", - SDL_GetError()); - - setenv("SDL_AUDIODRIVER", "dummy", 1); - SDL_QuitSubSystem(SDL_INIT_AUDIO); - if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) { - LOG_ERROR("Failed to reinitialize SDL audio with dummy driver: {}", - SDL_GetError()); - return -1; - } - - if (!open_stream()) { - LOG_ERROR("Failed to open output stream with dummy driver: {}", - SDL_GetError()); - return -1; - } - - LOG_WARN("Audio output disabled, using SDL dummy audio driver"); -#else - LOG_ERROR("Failed to open output stream: {}", SDL_GetError()); - return -1; -#endif - } - - SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(output_stream_)); - - return 0; -} - -int Render::AudioDeviceDestroy() { - if (output_stream_) { - SDL_CloseAudioDevice(SDL_GetAudioStreamDevice(output_stream_)); - SDL_DestroyAudioStream(output_stream_); - output_stream_ = nullptr; - } - return 0; -} - -void Render::UpdateInteractions() { -#if defined(__linux__) && !defined(__APPLE__) - const bool is_wayland_session = IsWaylandSession(); - const bool stop_wayland_mouse_before_screen = - is_wayland_session && !start_screen_capturer_ && - screen_capturer_is_started_ && !start_mouse_controller_ && - mouse_controller_is_started_; - if (stop_wayland_mouse_before_screen) { - LOG_INFO( - "Stopping Wayland mouse controller before screen capturer to " - "cleanly release the shared portal session"); - StopMouseController(); - mouse_controller_is_started_ = false; - } -#endif - - if (start_screen_capturer_ && !screen_capturer_is_started_) { - if (0 == StartScreenCapturer()) { - screen_capturer_is_started_ = true; - } - } else if (!start_screen_capturer_ && screen_capturer_is_started_) { - StopScreenCapturer(); - screen_capturer_is_started_ = false; - } - - if (start_speaker_capturer_ && !speaker_capturer_is_started_) { - if (0 == StartSpeakerCapturer()) { - speaker_capturer_is_started_ = true; - } - } else if (!start_speaker_capturer_ && speaker_capturer_is_started_) { - StopSpeakerCapturer(); - speaker_capturer_is_started_ = false; - } - - if (start_mouse_controller_ && !mouse_controller_is_started_) { - if (0 == StartMouseController()) { - mouse_controller_is_started_ = true; - } - } else if (!start_mouse_controller_ && mouse_controller_is_started_) { - StopMouseController(); - mouse_controller_is_started_ = false; - } - -#if defined(__linux__) && !defined(__APPLE__) - if (screen_capturer_is_started_ && screen_capturer_ && mouse_controller_) { - const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); - if (!latest_display_info.empty()) { - display_info_list_ = latest_display_info; - mouse_controller_->UpdateDisplayInfoList(display_info_list_); - } - } -#endif - - if (start_keyboard_capturer_ && focus_on_stream_window_) { - if (!keyboard_capturer_is_started_) { - if (StartKeyboardCapturer() == 0) { - keyboard_capturer_is_started_ = true; - } - } - if (keyboard_capturer_is_started_) { - SendKeyboardHeartbeat(false); - } - } else if (keyboard_capturer_is_started_) { - ForceReleasePressedKeys(); - StopKeyboardCapturer(); - keyboard_capturer_is_started_ = false; - } - - CheckRemoteKeyboardTimeouts(); -} - -int Render::CreateMainWindow() { - main_ctx_ = ImGui::CreateContext(); - if (!main_ctx_) { - LOG_ERROR("Main context is null"); - return -1; - } - - ImGui::SetCurrentContext(main_ctx_); - - if (!SDL_CreateWindowAndRenderer( - "CrossDesk Main Window", (int)main_window_width_, - (int)main_window_height_, - SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS | - SDL_WINDOW_HIDDEN | SDL_WINDOW_TRANSPARENT, - &main_window_, &main_renderer_)) { - LOG_ERROR("Error creating MainWindow and MainRenderer: {}", SDL_GetError()); - return -1; - } - - float dpi_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay()); - if (std::abs(dpi_scale_ - dpi_scale) > 0.01f) { - dpi_scale_ = dpi_scale; - - main_window_width_ = (int)(main_window_width_default_ * dpi_scale_); - main_window_height_ = (int)(main_window_height_default_ * dpi_scale_); - stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_); - stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_); - server_window_width_ = (int)(server_window_width_default_ * dpi_scale_); - server_window_height_ = (int)(server_window_height_default_ * dpi_scale_); - server_window_normal_width_ = - (int)(server_window_width_default_ * dpi_scale_); - server_window_normal_height_ = - (int)(server_window_height_default_ * dpi_scale_); - window_rounding_ = window_rounding_default_ * dpi_scale_; - - SDL_SetWindowSize(main_window_, (int)main_window_width_, - (int)main_window_height_); - } - - SDL_SetWindowResizable(main_window_, false); - - // for window region action - SDL_SetWindowHitTest(main_window_, HitTestCallback, this); - - SDL_SetRenderDrawBlendMode(main_renderer_, SDL_BLENDMODE_BLEND); - - SetupFontAndStyle(&main_windows_system_chinese_font_); - - ImGuiStyle& style = ImGui::GetStyle(); - style.ScaleAllSizes(dpi_scale_); - style.FontScaleDpi = dpi_scale_; - -#if _WIN32 - SDL_PropertiesID props = SDL_GetWindowProperties(main_window_); - HWND main_hwnd = (HWND)SDL_GetPointerProperty( - props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); - - HICON tray_icon = LoadTrayIcon(); - tray_ = std::make_unique(main_hwnd, tray_icon, L"CrossDesk", - localization_language_index_); -#elif defined(__APPLE__) - tray_ = std::make_unique(main_window_, "CrossDesk", - localization_language_index_); -#elif defined(__linux__) && !defined(__APPLE__) - tray_ = std::make_unique( - main_window_, "CrossDesk", localization_language_index_, APP_EXIT_EVENT); -#endif - - ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_); - ImGui_ImplSDLRenderer3_Init(main_renderer_); - - return 0; -} - -int Render::DestroyMainWindow() { - if (main_ctx_) { - ImGui::SetCurrentContext(main_ctx_); - } - - if (main_renderer_) { - SDL_DestroyRenderer(main_renderer_); - } - - if (main_window_) { - SDL_DestroyWindow(main_window_); - } - - return 0; -} - -int Render::CreateStreamWindow() { - if (stream_window_created_) { - return 0; - } - - stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_); - stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_); - - stream_ctx_ = ImGui::CreateContext(); - if (!stream_ctx_) { - LOG_ERROR("Stream context is null"); - return -1; - } - - ImGui::SetCurrentContext(stream_ctx_); - - if (!SDL_CreateWindowAndRenderer( - "CrossDesk Stream Window", (int)stream_window_width_, - (int)stream_window_height_, - SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS | - SDL_WINDOW_TRANSPARENT, - &stream_window_, &stream_renderer_)) { - LOG_ERROR("Error creating stream_window_ and stream_renderer_: {}", - SDL_GetError()); - return -1; - } - - stream_pixformat_ = SDL_PIXELFORMAT_NV12; - - SDL_SetWindowResizable(stream_window_, true); - - // for window region action - SDL_SetWindowHitTest(stream_window_, HitTestCallback, this); - - SDL_SetRenderDrawBlendMode(stream_renderer_, SDL_BLENDMODE_BLEND); - - SetupFontAndStyle(&stream_windows_system_chinese_font_); - - ImGuiStyle& style = ImGui::GetStyle(); - style.ScaleAllSizes(dpi_scale_); - style.FontScaleDpi = dpi_scale_; - - ImGui_ImplSDL3_InitForSDLRenderer(stream_window_, stream_renderer_); - ImGui_ImplSDLRenderer3_Init(stream_renderer_); - - // change props->stream_render_rect_ - SDL_Event event; - event.type = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED; - event.window.windowID = SDL_GetWindowID(stream_window_); - SDL_PushEvent(&event); - - stream_window_created_ = true; - just_created_ = true; - - stream_window_inited_ = true; - LOG_INFO("Stream window inited"); - - return 0; -} - -int Render::DestroyStreamWindow() { - stream_window_width_ = (float)stream_window_width_default_; - stream_window_height_ = (float)stream_window_height_default_; - - if (stream_ctx_) { - ImGui::SetCurrentContext(stream_ctx_); - } - - if (stream_renderer_) { - SDL_DestroyRenderer(stream_renderer_); - stream_renderer_ = nullptr; - } - - if (stream_window_) { - SDL_DestroyWindow(stream_window_); - stream_window_ = nullptr; - } - - stream_window_created_ = false; - focus_on_stream_window_ = false; - stream_window_grabbed_ = false; - control_mouse_ = false; - - return 0; -} - -int Render::CreateServerWindow() { - if (server_window_created_) { - return 0; - } - server_ctx_ = ImGui::CreateContext(); - if (!server_ctx_) { - LOG_ERROR("Server context is null"); - return -1; - } - ImGui::SetCurrentContext(server_ctx_); - if (!SDL_CreateWindowAndRenderer( - "CrossDesk Server Window", (int)server_window_width_, - (int)server_window_height_, - SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS | - SDL_WINDOW_TRANSPARENT, - &server_window_, &server_renderer_)) { - LOG_ERROR("Error creating server_window_ and server_renderer_: {}", - SDL_GetError()); - return -1; - } - -#if _WIN32 - // Hide server window from the taskbar by making it a tool window. - { - SDL_PropertiesID server_props = SDL_GetWindowProperties(server_window_); - HWND server_hwnd = (HWND)SDL_GetPointerProperty( - server_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); - - if (server_hwnd) { - LONG_PTR ex_style = GetWindowLongPtr(server_hwnd, GWL_EXSTYLE); - ex_style |= WS_EX_TOOLWINDOW; - ex_style &= ~WS_EX_APPWINDOW; - SetWindowLongPtr(server_hwnd, GWL_EXSTYLE, ex_style); - - // Keep the server window above normal windows. - SetWindowPos(server_hwnd, HWND_TOPMOST, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOACTIVATE); - } - } -#endif - -#if defined(__linux__) && !defined(__APPLE__) - // Best-effort keep above other windows on X11. - X11SetWindowAlwaysOnTop(server_window_); - // Best-effort hide from taskbar on X11. - X11SetWindowSkipTaskbar(server_window_); -#endif - -#if defined(__APPLE__) - // Best-effort keep above other windows on macOS. - MacSetWindowAlwaysOnTop(server_window_, true); - // Best-effort exclude from Window menu / window cycling. - MacSetWindowExcludedFromWindowMenu(server_window_, true); -#endif - - // Set window position to bottom-right corner - SDL_Rect display_bounds; - if (SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(server_window_), - &display_bounds)) { - int window_x = - display_bounds.x + display_bounds.w - (int)server_window_width_; - int window_y = - display_bounds.y + display_bounds.h - (int)server_window_height_; - SDL_SetWindowPosition(server_window_, window_x, window_y); - } - - SDL_SetWindowResizable(server_window_, false); - - SDL_SetRenderDrawBlendMode(server_renderer_, SDL_BLENDMODE_BLEND); - - // for window region action - SDL_SetWindowHitTest(server_window_, HitTestCallback, this); - - SetupFontAndStyle(&server_windows_system_chinese_font_); - - ImGuiStyle& style = ImGui::GetStyle(); - style.ScaleAllSizes(dpi_scale_); - style.FontScaleDpi = dpi_scale_; - - ImGui_ImplSDL3_InitForSDLRenderer(server_window_, server_renderer_); - ImGui_ImplSDLRenderer3_Init(server_renderer_); - - server_window_created_ = true; - server_window_inited_ = true; - - LOG_INFO("Server window inited"); - - return 0; -} - -int Render::DestroyServerWindow() { - if (server_ctx_) { - ImGui::SetCurrentContext(server_ctx_); - } - - if (server_renderer_) { - SDL_DestroyRenderer(server_renderer_); - server_renderer_ = nullptr; - } - - if (server_window_) { - SDL_DestroyWindow(server_window_); - server_window_ = nullptr; - } - - server_window_created_ = false; - server_window_inited_ = false; - - return 0; -} - -int Render::SetupFontAndStyle(ImFont** system_chinese_font_out) { - float font_size = 32.0f; - - // Setup Dear ImGui style - ImGuiIO& io = ImGui::GetIO(); - - io.IniFilename = NULL; // disable imgui.ini - - // Build one merged atlas: UI font + icon font + multilingual fallback fonts. - ImFontConfig config; - config.FontDataOwnedByAtlas = false; - config.MergeMode = false; - - if (system_chinese_font_out) { - *system_chinese_font_out = nullptr; - } - - ImFont* ui_font = nullptr; - const ImWchar* multilingual_ranges = GetMultilingualGlyphRanges(); - -#if defined(_WIN32) - const char* base_font_paths[] = { - "C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/msyhbd.ttc", - "C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf", - "C:/Windows/Fonts/simsun.ttc", nullptr}; -#elif defined(__APPLE__) - const char* base_font_paths[] = { - "/System/Library/Fonts/PingFang.ttc", - "/System/Library/Fonts/Supplemental/Arial Unicode.ttf", - "/System/Library/Fonts/Supplemental/Arial.ttf", - "/System/Library/Fonts/SFNS.ttf", nullptr}; -#else - const char* base_font_paths[] = { - "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", - "/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf", - "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", - "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", - "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", - nullptr}; -#endif - - for (int i = 0; base_font_paths[i] != nullptr && ui_font == nullptr; ++i) { - if (!CanReadFontFile(base_font_paths[i])) { - continue; - } - ui_font = io.Fonts->AddFontFromFileTTF(base_font_paths[i], font_size, - &config, multilingual_ranges); - if (ui_font != nullptr) { - LOG_INFO("Loaded base UI font: {}", base_font_paths[i]); - } - } - if (!ui_font) { - ui_font = io.Fonts->AddFontDefault(&config); - } - - if (!ui_font) { - LOG_WARN("Failed to initialize base UI font"); - ImGui::StyleColorsLight(); - return 0; - } - - ImFontConfig icon_config = config; - icon_config.MergeMode = true; - static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; - io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len, - font_size, &icon_config, icon_ranges); - - io.FontDefault = ui_font; - if (system_chinese_font_out) { - *system_chinese_font_out = ui_font; - } - - ImGui::StyleColorsLight(); - - return 0; -} - -int Render::DestroyMainWindowContext() { - if (!main_ctx_) { - return 0; - } - - ImGui::SetCurrentContext(main_ctx_); - ImGui_ImplSDLRenderer3_Shutdown(); - ImGui_ImplSDL3_Shutdown(); - ImGui::DestroyContext(main_ctx_); - main_ctx_ = nullptr; - - return 0; -} - -int Render::DestroyStreamWindowContext() { - if (!stream_ctx_) { - stream_window_inited_ = false; - return 0; - } - - stream_window_inited_ = false; - ImGui::SetCurrentContext(stream_ctx_); - ImGui_ImplSDLRenderer3_Shutdown(); - ImGui_ImplSDL3_Shutdown(); - ImGui::DestroyContext(stream_ctx_); - stream_ctx_ = nullptr; - - return 0; -} - -int Render::DestroyServerWindowContext() { - if (!server_ctx_) { - server_window_inited_ = false; - return 0; - } - - server_window_inited_ = false; - ImGui::SetCurrentContext(server_ctx_); - ImGui_ImplSDLRenderer3_Shutdown(); - ImGui_ImplSDL3_Shutdown(); - ImGui::DestroyContext(server_ctx_); - server_ctx_ = nullptr; - - return 0; -} - -int Render::DrawMainWindow() { - if (!main_ctx_) { - LOG_ERROR("Main context is null"); - return -1; - } - - ImGui::SetCurrentContext(main_ctx_); - ImGui_ImplSDLRenderer3_NewFrame(); - ImGui_ImplSDL3_NewFrame(); - ImGui::NewFrame(); - - ImGuiIO& io = ImGui::GetIO(); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_); - - ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y), - ImGuiCond_Always); - ImGui::Begin("MainRender", nullptr, - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_NoBringToFrontOnFocus | - ImGuiWindowFlags_NoDocking); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(); - - TitleBar(true); - - MainWindow(); - - UpdateNotificationWindow(); - -#if _WIN32 && CROSSDESK_PORTABLE - PortableServiceInstallWindow(); -#endif - -#ifdef __APPLE__ - if (show_request_permission_window_) { - RequestPermissionWindow(); - } -#endif - - ImGui::End(); - - // Rendering - (void)io; - ImGui::Render(); - SDL_SetRenderScale(main_renderer_, io.DisplayFramebufferScale.x, - io.DisplayFramebufferScale.y); - SDL_SetRenderDrawColor(main_renderer_, 0, 0, 0, 0); - SDL_RenderClear(main_renderer_); - ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), main_renderer_); - SDL_RenderPresent(main_renderer_); - - return 0; -} - -int Render::DrawStreamWindow() { - if (!stream_ctx_) { - LOG_ERROR("Stream context is null"); - return -1; - } - - ImGui::SetCurrentContext(stream_ctx_); - ImGui_ImplSDLRenderer3_NewFrame(); - ImGui_ImplSDL3_NewFrame(); - ImGui::NewFrame(); - - StreamWindow(); - - ImGuiIO& io = ImGui::GetIO(); - float stream_title_window_height = - fullscreen_button_pressed_ ? 0 : title_bar_height_; - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - // Set minimum window size to 0 to allow exact height control - ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); - ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, stream_title_window_height), - ImGuiCond_Always); - ImGui::Begin("StreamTitleWindow", nullptr, - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_NoBringToFrontOnFocus | - ImGuiWindowFlags_NoDocking); - ImGui::PopStyleVar(2); - ImGui::PopStyleColor(); - - if (!fullscreen_button_pressed_) { - TitleBar(false); - } - - ImGui::End(); - - // Rendering - (void)io; - ImGui::Render(); - SDL_SetRenderScale(stream_renderer_, io.DisplayFramebufferScale.x, - io.DisplayFramebufferScale.y); - SDL_SetRenderDrawColor(stream_renderer_, 0, 0, 0, 255); - SDL_RenderClear(stream_renderer_); - - // std::shared_lock lock(client_properties_mutex_); - for (auto& it : client_properties_) { - auto props = it.second; - if (props->tab_selected_) { - SDL_FRect render_rect_f = { - props->stream_render_rect_f_.x, props->stream_render_rect_f_.y, - props->stream_render_rect_f_.w, props->stream_render_rect_f_.h}; - SDL_RenderTexture(stream_renderer_, props->stream_texture_, NULL, - &render_rect_f); - } - } - ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), stream_renderer_); - SDL_RenderPresent(stream_renderer_); - - return 0; -} - -int Render::DrawServerWindow() { - if (!server_ctx_) { - LOG_ERROR("Server context is null"); - return -1; - } - ImGui::SetCurrentContext(server_ctx_); - ImGui_ImplSDLRenderer3_NewFrame(); - ImGui_ImplSDL3_NewFrame(); - ImGui::NewFrame(); - - ImGuiIO& io = ImGui::GetIO(); - server_window_width_ = io.DisplaySize.x; - server_window_height_ = io.DisplaySize.y; - - ServerWindow(); - ImGui::Render(); - SDL_SetRenderScale(server_renderer_, io.DisplayFramebufferScale.x, - io.DisplayFramebufferScale.y); - SDL_SetRenderDrawColor(server_renderer_, 0, 0, 0, 0); - SDL_RenderClear(server_renderer_); - ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), server_renderer_); - SDL_RenderPresent(server_renderer_); - return 0; -} - -int Render::Run() { - path_manager_ = std::make_unique("CrossDesk"); - if (path_manager_) { - exec_log_path_ = path_manager_->GetLogPath().string(); - dll_log_path_ = path_manager_->GetLogPath().string(); - cache_path_ = path_manager_->GetCachePath().string(); - config_center_ = - std::make_unique(cache_path_ + "/config.ini"); - strncpy(signal_server_ip_self_, - config_center_->GetSignalServerHost().c_str(), - sizeof(signal_server_ip_self_) - 1); - signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0'; - int signal_port_init = config_center_->GetSignalServerPort(); - if (signal_port_init > 0) { - strncpy(signal_server_port_self_, - std::to_string(signal_port_init).c_str(), - sizeof(signal_server_port_self_) - 1); - signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0'; - } else { - signal_server_port_self_[0] = '\0'; - } - } else { - std::cerr << "Failed to create PathManager" << std::endl; - return -1; - } - - InitializeLogger(); - LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION); - - latest_version_info_ = CheckUpdate(); - if (!latest_version_info_.empty()) { - std::string version; - if (latest_version_info_.contains("latest_version") && - latest_version_info_["latest_version"].is_string()) { - version = latest_version_info_["latest_version"].get(); - } else if (latest_version_info_.contains("version") && - latest_version_info_["version"].is_string()) { - version = latest_version_info_["version"].get(); - } - - if (!version.empty()) { - latest_version_ = 'v' + version; - } else { - latest_version_ = ""; - } - if (latest_version_info_.contains("releaseNotes") && - latest_version_info_["releaseNotes"].is_string()) { - release_notes_ = latest_version_info_["releaseNotes"].get(); - } else { - release_notes_ = ""; - } - update_available_ = - !version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_); - LOG_INFO("Update check: current={}, latest={}, available={}", - CROSSDESK_VERSION, latest_version_, update_available_); - if (update_available_) { - show_update_notification_window_ = true; - } - } else { - latest_version_ = ""; - update_available_ = false; - LOG_WARN( - "Update check skipped: version.json is empty or missing " - "latest_version"); - } - - InitializeSettings(); - InitializeSDL(); - InitializeModules(); - InitializeMainWindow(); - -#if _WIN32 && CROSSDESK_PORTABLE - CheckPortableWindowsService(); -#endif - - const int scaled_video_width_ = 160; - const int scaled_video_height_ = 90; - - MainLoop(); - - Cleanup(); - - return 0; -} - -void Render::InitializeLogger() { InitLogger(exec_log_path_); } - -void Render::InitializeSettings() { - LoadSettingsFromCacheFile(); - LoadRecentConnectionAliases(); - - localization_language_index_ = - localization::detail::ClampLanguageIndex(language_button_value_); - language_button_value_ = localization_language_index_; - - if (localization_language_index_ == 0) { - localization_language_ = ConfigCenter::LANGUAGE::CHINESE; - } else if (localization_language_index_ == 1) { - localization_language_ = ConfigCenter::LANGUAGE::ENGLISH; - } else { - localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN; - } -} - -void Render::InitializeSDL() { -#if defined(__linux__) && !defined(__APPLE__) - if (!getenv("SDL_AUDIODRIVER")) { - // Prefer PulseAudio first on Linux to avoid hard ALSA plugin dependency. - setenv("SDL_AUDIODRIVER", "pulseaudio", 0); - } -#endif - - if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { - LOG_ERROR("Error: {}", SDL_GetError()); - return; - } - - const SDL_DisplayMode* dm = SDL_GetCurrentDisplayMode(0); - if (dm) { - screen_width_ = dm->w; - screen_height_ = dm->h; - } - - const uint32_t custom_event_base = SDL_RegisterEvents(3); - if (custom_event_base == static_cast(-1)) { - LOG_ERROR("Failed to register custom SDL events"); - } else { - STREAM_REFRESH_EVENT = custom_event_base; - APP_EXIT_EVENT = custom_event_base + 1; - REMOTE_CLIPBOARD_EVENT = custom_event_base + 2; - } - - LOG_INFO("Screen resolution: [{}x{}]", screen_width_, screen_height_); -} - -void Render::InitializeModules() { - if (!modules_inited_) { - AudioDeviceInit(); - screen_capturer_factory_ = new ScreenCapturerFactory(); - speaker_capturer_factory_ = new SpeakerCapturerFactory(); - device_controller_factory_ = new DeviceControllerFactory(); - keyboard_capturer_ = (KeyboardCapturer*)device_controller_factory_->Create( - DeviceControllerFactory::Device::Keyboard); - CreateConnectionPeer(); - - modules_inited_ = true; - } -} - -void Render::InitializeClipboard() { - last_clipboard_text_.clear(); - - char* clipboard_text = SDL_GetClipboardText(); - if (clipboard_text) { - last_clipboard_text_.assign(clipboard_text); - SDL_free(clipboard_text); - } - - if (REMOTE_CLIPBOARD_EVENT == 0) { - LOG_ERROR("Clipboard synchronization disabled: SDL event is unavailable"); - return; - } - - clipboard_events_enabled_.store(true, std::memory_order_release); -} - -void Render::ShutdownClipboard() { - clipboard_events_enabled_.store(false, std::memory_order_release); - std::lock_guard lock(pending_clipboard_mutex_); - pending_remote_clipboard_text_.reset(); -} - -void Render::QueueRemoteClipboardText(const char* data, size_t size) { - if (!clipboard_events_enabled_.load(std::memory_order_acquire)) { - return; - } - if (!data || size == 0) { - return; - } - if (size > kMaxClipboardTextBytes) { - LOG_WARN("Ignore oversized remote clipboard text: {} bytes", size); - return; - } - if (std::memchr(data, '\0', size) != nullptr) { - LOG_WARN("Ignore remote clipboard text containing an embedded NUL byte"); - return; - } - - { - std::lock_guard lock(pending_clipboard_mutex_); - if (!clipboard_events_enabled_.load(std::memory_order_relaxed)) { - return; - } - // Only the newest clipboard value matters. Replacing it also bounds the - // amount of memory a busy or malicious peer can queue. - pending_remote_clipboard_text_ = std::string(data, size); - } - - SDL_Event event{}; - event.type = REMOTE_CLIPBOARD_EVENT; - if (!SDL_PushEvent(&event)) { - // MainLoop also drains the pending value after its wait timeout. - LOG_WARN("Failed to wake SDL loop for remote clipboard text: {}", - SDL_GetError()); - } -} - -void Render::ApplyPendingRemoteClipboardText() { - if (!clipboard_events_enabled_.load(std::memory_order_acquire)) { - return; - } - - std::optional pending_text; - { - std::lock_guard lock(pending_clipboard_mutex_); - pending_text.swap(pending_remote_clipboard_text_); - } - if (!pending_text || *pending_text == last_clipboard_text_) { - return; - } - - // SDL clipboard functions must run on the thread that initialized SDL. - if (!SDL_SetClipboardText(pending_text->c_str())) { - LOG_ERROR("Failed to set remote clipboard text: {}", SDL_GetError()); - return; - } - - // SDL will normally emit SDL_EVENT_CLIPBOARD_UPDATE for this write. Update - // the baseline first so that event cannot echo the text back to the sender. - last_clipboard_text_ = std::move(*pending_text); -} - -void Render::HandleClipboardUpdate() { - if (!clipboard_events_enabled_.load(std::memory_order_acquire)) { - return; - } - if (!SDL_HasClipboardText()) { - last_clipboard_text_.clear(); - return; - } - - char* clipboard_text = SDL_GetClipboardText(); - if (!clipboard_text) { - LOG_WARN("Failed to read local clipboard text: {}", SDL_GetError()); - return; - } - - std::string text(clipboard_text); - SDL_free(clipboard_text); - if (text == last_clipboard_text_) { - return; - } - - // Record the value before sending. Duplicate SDL notifications and a - // remote write of the same value must not create a clipboard feedback loop. - last_clipboard_text_ = text; - if (text.empty()) { - return; - } - if (text.size() > kMaxClipboardTextBytes) { - LOG_WARN("Ignore oversized local clipboard text: {} bytes", text.size()); - return; - } - - SendClipboardTextToPeers(text); -} - -int Render::SendClipboardTextToPeers(const std::string& text) { - std::shared_lock lock(client_properties_mutex_); - for (const auto& [remote_id, props] : client_properties_) { - if (!props || !props->peer_ || !props->connection_established_ || - !props->enable_mouse_control_) { - continue; - } - - const int ret = SendReliableDataFrame( - props->peer_, text.data(), text.size(), props->clipboard_label_.c_str()); - if (ret != 0) { - LOG_WARN("Failed to send clipboard data to peer [{}], ret={}", - remote_id.c_str(), ret); - return ret; - } - } - - if (peer_) { - const int ret = SendReliableDataFrame(peer_, text.data(), text.size(), - clipboard_label_.c_str()); - if (ret != 0) { - LOG_WARN("Failed to send clipboard data to peer [{}], ret={}", - remote_id_display_, ret); - return ret; - } - } - - return 0; -} - -void Render::InitializeMainWindow() { - CreateMainWindow(); - InitializeClipboard(); - if (SDL_WINDOW_HIDDEN & SDL_GetWindowFlags(main_window_)) { - SDL_ShowWindow(main_window_); - } -} - -void Render::MainLoop() { - while (!exit_) { - if (!peer_) { - CreateConnectionPeer(); - } - - SDL_Event event; - if (SDL_WaitEventTimeout(&event, sdl_refresh_ms_)) { - ProcessSdlEvent(event); - } - // Also drain the pending value after a timeout so a full SDL event queue - // cannot indefinitely delay a clipboard update received from the network. - ApplyPendingRemoteClipboardText(); - -#if _WIN32 - MSG msg; - while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } -#elif defined(__linux__) && !defined(__APPLE__) - if (tray_) { - tray_->ProcessEvents(); - } -#endif - - UpdateLabels(); - HandleRecentConnections(); - HandleConnectionStatusChange(); - HandlePendingPresenceProbe(); - HandleConnectionTimeouts(); - HandleStreamWindow(); - HandleServerWindow(); - HandleWindowsServiceIntegration(); - - const bool main_window_visible = - main_window_ && !(SDL_GetWindowFlags(main_window_) & SDL_WINDOW_HIDDEN); - if (main_window_visible) { - DrawMainWindow(); - } - if (stream_window_inited_) { - DrawStreamWindow(); - } - - if (is_server_mode_) { - DrawServerWindow(); - } - - UpdateInteractions(); - } -} - -bool Render::MinimizeMainWindowToTray() { - if (!enable_minimize_to_tray_ || !main_window_) { - return false; - } - -#if defined(_WIN32) || defined(__APPLE__) - if (!tray_) { - return false; - } - - tray_->MinimizeToTray(); - return true; -#elif defined(__linux__) && !defined(__APPLE__) - if (!tray_) { - return false; - } - - return tray_->MinimizeToTray(); -#else - return false; -#endif -} - -void Render::UpdateLabels() { - if (!label_inited_ || - localization_language_index_last_ != localization_language_index_) { - connect_button_label_ = - connect_button_pressed_ - ? localization::disconnect[localization_language_index_] - : localization::connect[localization_language_index_]; - label_inited_ = true; - localization_language_index_last_ = localization_language_index_; - } -} - -void Render::ResetRemoteServiceStatus(SubStreamWindowProperties& props) { - props.remote_service_status_received_ = false; - props.remote_service_available_ = false; - props.remote_interactive_stage_.clear(); -} - -void Render::ApplyRemoteServiceStatus(SubStreamWindowProperties& props, - const ServiceStatus& status) { - props.remote_service_status_received_ = true; - props.remote_service_available_ = status.available; - props.remote_interactive_stage_ = status.interactive_stage; -} - -Render::RemoteUnlockState Render::GetRemoteUnlockState( - const SubStreamWindowProperties& props) const { - if (!props.remote_service_status_received_) { - return RemoteUnlockState::none; - } - if (!props.remote_service_available_) { - return RemoteUnlockState::service_unavailable; - } - if (props.remote_interactive_stage_ == "credential-ui") { - return RemoteUnlockState::credential_ui; - } - if (props.remote_interactive_stage_ == "lock-screen") { - return RemoteUnlockState::lock_screen; - } - if (props.remote_interactive_stage_ == "secure-desktop") { - return RemoteUnlockState::secure_desktop; - } - return RemoteUnlockState::none; -} - -void Render::HandleWindowsServiceIntegration() { -#if _WIN32 - static bool last_logged_service_available = true; - static unsigned int last_logged_service_error_code = 0; - static std::string last_logged_service_error; - - if (!is_server_mode_ || peer_ == nullptr) { - ResetLocalWindowsServiceState(true); - return; - } - - const bool has_connected_remote = [&] { - std::shared_lock lock(connection_status_mutex_); - return std::any_of(connection_status_.begin(), connection_status_.end(), - [](const auto& entry) { - return entry.second == ConnectionStatus::Connected; - }); - }(); - if (!has_connected_remote) { - ResetLocalWindowsServiceState(false); - return; - } - - bool force_broadcast = false; - if (pending_windows_service_sas_.exchange(false, std::memory_order_relaxed)) { - const std::string response = - QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs); - auto json = nlohmann::json::parse(response, nullptr, false); - if (json.is_discarded() || !json.value("ok", false)) { - LOG_WARN("Remote SAS request failed: {}", response); - } else { - LOG_INFO("Remote SAS request forwarded to local Windows service"); - optimistic_windows_secure_desktop_until_tick_ = - static_cast(SDL_GetTicks()) + - kWindowsServiceSasSecureDesktopGraceMs; - local_service_status_received_ = true; - local_service_available_ = true; - local_interactive_stage_ = "secure-desktop"; - } - last_windows_service_status_tick_ = 0; - force_broadcast = true; - } - - const uint32_t now = static_cast(SDL_GetTicks()); - if (!force_broadcast && last_windows_service_status_tick_ != 0 && - now - last_windows_service_status_tick_ < - kWindowsServiceStatusIntervalMs) { - return; - } - last_windows_service_status_tick_ = now; - - WindowsServiceInteractiveStatus status; - const bool status_ok = QueryWindowsServiceInteractiveStatus(&status); - WindowsServiceInteractiveStatus broadcast_status = status; - const bool previous_secure_desktop_interaction = - IsSecureDesktopInteractionRequired(local_interactive_stage_); - const bool optimistic_secure_desktop_active = - optimistic_windows_secure_desktop_until_tick_ != 0 && - static_cast(optimistic_windows_secure_desktop_until_tick_ - - now) > 0; - const bool keep_optimistic_secure_desktop = - status_ok && status.available && optimistic_secure_desktop_active && - status.sas_secure_desktop_grace_active && - status.interactive_stage == "user-desktop"; - local_service_status_received_ = - status_ok || previous_secure_desktop_interaction; - local_service_available_ = status.available; - if (status.available) { - if (keep_optimistic_secure_desktop) { - local_interactive_stage_ = "secure-desktop"; - broadcast_status.interactive_stage = local_interactive_stage_; - } else { - local_interactive_stage_ = status.interactive_stage; - optimistic_windows_secure_desktop_until_tick_ = 0; - } - } else if (!previous_secure_desktop_interaction) { - local_interactive_stage_.clear(); - optimistic_windows_secure_desktop_until_tick_ = 0; - } - - if (status_ok) { - const bool availability_changed = - status.available != last_logged_service_available; - const bool error_changed = - !status.available && - (status.error != last_logged_service_error || - status.error_code != last_logged_service_error_code); - if (availability_changed || error_changed) { - if (status.available) { - LOG_INFO( - "Local Windows service available for secure desktop integration"); - } else if (IsTransientWindowsServiceStatusError(status.error)) { - LOG_INFO( - "Local Windows service temporarily unavailable, keeping last " - "secure desktop state: error={}, code={}", - status.error, status.error_code); - } else { - LOG_WARN( - "Local Windows service unavailable, secure desktop integration " - "disabled: error={}, code={}", - status.error, status.error_code); - } - last_logged_service_available = status.available; - last_logged_service_error = status.error; - last_logged_service_error_code = status.error_code; - } - } else if (last_logged_service_available || - last_logged_service_error != "invalid_service_status_json") { - LOG_WARN( - "Local Windows service status query failed, secure desktop integration " - "disabled"); - last_logged_service_available = false; - last_logged_service_error = "invalid_service_status_json"; - last_logged_service_error_code = 0; - } - - RemoteAction remote_action = - BuildWindowsServiceStatusAction(broadcast_status); - std::string msg = remote_action.to_json(); - int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(), - control_data_label_.c_str()); - if (ret != 0) { - LOG_WARN("Broadcast Windows service status failed, ret={}", ret); - } -#endif -} - -#if _WIN32 -void Render::ResetLocalWindowsServiceState(bool clear_pending_sas) { - last_windows_service_status_tick_ = 0; - if (clear_pending_sas) { - pending_windows_service_sas_.store(false, std::memory_order_relaxed); - } - local_service_status_received_ = false; - local_service_available_ = false; - local_interactive_stage_.clear(); - optimistic_windows_secure_desktop_until_tick_ = 0; -} -#endif - -void Render::HandleRecentConnections() { - if (reload_recent_connections_ && main_renderer_) { - uint32_t now_time = SDL_GetTicks(); - if (now_time - recent_connection_image_save_time_ >= 50) { - int ret = thumbnail_->LoadThumbnail(main_renderer_, recent_connections_, - &recent_connection_image_width_, - &recent_connection_image_height_); - if (!ret) { - LOG_INFO("Load recent connection thumbnails"); - } - reload_recent_connections_ = false; - - recent_connection_ids_.clear(); - for (const auto& conn : recent_connections_) { - recent_connection_ids_.push_back(conn.first); - } - need_to_send_recent_connections_ = true; - } - } -} - -void Render::HandleConnectionStatusChange() { - if (signal_connected_ && peer_ && need_to_send_recent_connections_) { - if (!recent_connection_ids_.empty()) { - nlohmann::json j; - j["type"] = "recent_connections_presence"; - j["user_id"] = client_id_; - j["devices"] = nlohmann::json::array(); - for (const auto& id : recent_connection_ids_) { - std::string pure_id = id; - size_t pos_y = pure_id.find('Y'); - size_t pos_n = pure_id.find('N'); - size_t pos = std::string::npos; - if (pos_y != std::string::npos && - (pos_n == std::string::npos || pos_y < pos_n)) { - pos = pos_y; - } else if (pos_n != std::string::npos) { - pos = pos_n; - } - if (pos != std::string::npos) { - pure_id = pure_id.substr(0, pos); - } - j["devices"].push_back(pure_id); - } - auto s = j.dump(); - SendSignalMessage(peer_, s.data(), s.size()); - } - } - need_to_send_recent_connections_ = false; -} - -void Render::HandlePendingPresenceProbe() { - bool has_action = false; - bool should_connect = false; - bool remember_password = false; - std::string remote_id; - std::string password; - - { - std::lock_guard lock(pending_presence_probe_mutex_); - if (!pending_presence_probe_ || !pending_presence_result_ready_) { - return; - } - - has_action = true; - should_connect = pending_presence_online_; - remote_id = pending_presence_remote_id_; - password = pending_presence_password_; - remember_password = pending_presence_remember_password_; - - pending_presence_probe_ = false; - pending_presence_result_ready_ = false; - pending_presence_online_ = false; - pending_presence_remote_id_.clear(); - pending_presence_password_.clear(); - pending_presence_remember_password_ = false; - } - - if (!has_action) { - return; - } - - if (should_connect) { - ConnectTo(remote_id, password.c_str(), remember_password, true); - return; - } - - offline_warning_text_ = - localization::device_offline[localization_language_index_]; - show_offline_warning_window_ = true; -} - -void Render::HandleConnectionTimeouts() { - const auto now = std::chrono::steady_clock::now(); - - bool presence_probe_timed_out = false; - std::string presence_remote_id; - { - std::lock_guard lock(pending_presence_probe_mutex_); - if (pending_presence_probe_ && !pending_presence_result_ready_ && - now - pending_presence_probe_started_at_ >= kPresenceProbeTimeout) { - presence_probe_timed_out = true; - presence_remote_id = pending_presence_remote_id_; - pending_presence_probe_ = false; - pending_presence_result_ready_ = false; - pending_presence_online_ = false; - pending_presence_remote_id_.clear(); - pending_presence_password_.clear(); - pending_presence_remember_password_ = false; - } - } - - if (presence_probe_timed_out) { - offline_warning_text_ = - localization::device_offline[localization_language_index_]; - show_offline_warning_window_ = true; - LOG_WARN("Presence probe timed out for [{}]", presence_remote_id); - } - - bool rejoin_state_changed = false; - for (auto& [_, props] : client_properties_) { - if (!props || !props->connection_attempt_active_.load()) { - continue; - } - - const ConnectionStatus status = props->connection_status_.load(); - if (!IsConnectionAttemptPending(status)) { - props->connection_attempt_active_.store(false); - continue; - } - - if (now - props->connection_attempt_started_at_ < - kConnectionAttemptTimeout) { - continue; - } - - LOG_WARN("Connection to [{}] timed out, status={}", props->remote_id_, - static_cast(status)); - props->connection_attempt_active_.store(false); - props->connection_established_ = false; - props->rejoin_ = false; - props->connection_status_.store(ConnectionStatus::Failed); - focused_remote_id_ = props->remote_id_; - show_connection_status_window_ = true; - rejoin_state_changed = true; - } - - if (rejoin_state_changed) { - need_to_rejoin_ = false; - for (const auto& [_, props] : client_properties_) { - if (props && props->rejoin_) { - need_to_rejoin_ = true; - break; - } - } - } -} - -int Render::RequestSingleDevicePresence(const std::string& remote_id, - const char* password, - bool remember_password) { - if (!signal_connected_ || !peer_) { - return -1; - } - - { - std::lock_guard lock(pending_presence_probe_mutex_); - pending_presence_probe_ = true; - pending_presence_result_ready_ = false; - pending_presence_online_ = false; - pending_presence_probe_started_at_ = std::chrono::steady_clock::now(); - pending_presence_remote_id_ = remote_id; - pending_presence_password_ = password ? password : ""; - pending_presence_remember_password_ = remember_password; - } - - nlohmann::json j; - j["type"] = "recent_connections_presence"; - j["user_id"] = client_id_; - j["devices"] = nlohmann::json::array({remote_id}); - auto s = j.dump(); - - int ret = SendSignalMessage(peer_, s.data(), s.size()); - if (ret != 0) { - std::lock_guard lock(pending_presence_probe_mutex_); - pending_presence_probe_ = false; - pending_presence_result_ready_ = false; - pending_presence_online_ = false; - pending_presence_remote_id_.clear(); - pending_presence_password_.clear(); - pending_presence_remember_password_ = false; - } - - return ret; -} - -void Render::HandleStreamWindow() { - if (need_to_create_stream_window_) { - CreateStreamWindow(); - need_to_create_stream_window_ = false; - } - - if (stream_window_inited_) { - if (!stream_window_grabbed_ && control_mouse_) { - SDL_SetWindowMouseGrab(stream_window_, true); - stream_window_grabbed_ = true; - } else if (stream_window_grabbed_ && !control_mouse_) { - SDL_SetWindowMouseGrab(stream_window_, false); - stream_window_grabbed_ = false; - } - } -} - -void Render::HandleServerWindow() { - if (need_to_create_server_window_) { - CreateServerWindow(); - need_to_create_server_window_ = false; - } - - if (need_to_destroy_server_window_) { - DestroyServerWindow(); - DestroyServerWindowContext(); - need_to_destroy_server_window_ = false; - } -} - -void Render::Cleanup() { - ShutdownClipboard(); - - if (mouse_controller_) { - mouse_controller_->Destroy(); - delete mouse_controller_; - mouse_controller_ = nullptr; - } - - if (screen_capturer_) { - screen_capturer_->Destroy(); - delete screen_capturer_; - screen_capturer_ = nullptr; - } - - if (speaker_capturer_) { - speaker_capturer_->Destroy(); - delete speaker_capturer_; - speaker_capturer_ = nullptr; - } - - if (keyboard_capturer_) { - delete keyboard_capturer_; - keyboard_capturer_ = nullptr; - } - - CleanupFactories(); - CleanupPeers(); - -#if _WIN32 && CROSSDESK_PORTABLE - JoinPortableWindowsServiceInstallThread(); -#endif - - WaitForThumbnailSaveTasks(); - - AudioDeviceDestroy(); -#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) - tray_.reset(); -#endif - - if (stream_window_created_) { - if (stream_window_) { - SDL_SetWindowMouseGrab(stream_window_, false); - } - DestroyStreamWindow(); - } - if (stream_ctx_) { - DestroyStreamWindowContext(); - } - - if (server_window_created_) { - DestroyServerWindow(); - } - if (server_ctx_) { - DestroyServerWindowContext(); - } - - DestroyMainWindowContext(); - DestroyMainWindow(); - SDL_Quit(); -} - -void Render::CleanupFactories() { - if (screen_capturer_factory_) { - delete screen_capturer_factory_; - screen_capturer_factory_ = nullptr; - } - - if (speaker_capturer_factory_) { - delete speaker_capturer_factory_; - speaker_capturer_factory_ = nullptr; - } - - if (device_controller_factory_) { - delete device_controller_factory_; - device_controller_factory_ = nullptr; - } -} - -void Render::CleanupPeer(std::shared_ptr props) { - SDL_FlushEvent(STREAM_REFRESH_EVENT); - - std::shared_ptr> frame_snapshot; - int video_width = 0; - int video_height = 0; - { - std::lock_guard lock(props->video_frame_mutex_); - frame_snapshot = props->front_frame_; - video_width = props->video_width_; - video_height = props->video_height_; - } - - if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 && - video_height > 0) { - std::vector buffer_copy(*frame_snapshot); - std::string remote_id = props->remote_id_; - std::string remote_host_name = props->remote_host_name_; - std::string password = - props->remember_password_ ? props->remote_password_ : ""; - - std::thread save_thread([buffer_copy, video_width, video_height, remote_id, - remote_host_name, password, - thumbnail = thumbnail_]() { - thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width, - video_height, remote_id, remote_host_name, - password); - }); - - { - std::lock_guard lock(thumbnail_save_threads_mutex_); - thumbnail_save_threads_.emplace_back(std::move(save_thread)); - } - } - - if (props->peer_) { - LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_); - LeaveConnection(props->peer_, props->remote_id_.c_str()); - LOG_INFO("Destroy peer [{}]", props->local_id_); - DestroyPeer(&props->peer_); - } -} - -void Render::CleanupPeers() { - if (peer_) { - LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_); - LeaveConnection(peer_, client_id_); - is_client_mode_ = false; - StopMouseController(); - StopScreenCapturer(); - StopSpeakerCapturer(); - StopKeyboardCapturer(); - LOG_INFO("Destroy peer [{}]", client_id_); - DestroyPeer(&peer_); - } - - { - // std::shared_lock lock(client_properties_mutex_); - for (auto& it : client_properties_) { - auto props = it.second; - CleanupPeer(props); - } - } - - { - // std::unique_lock lock(client_properties_mutex_); - client_properties_.clear(); - } -} - -void Render::WaitForThumbnailSaveTasks() { - std::vector threads_to_join; - - { - std::lock_guard lock(thumbnail_save_threads_mutex_); - threads_to_join.swap(thumbnail_save_threads_); - } - - if (threads_to_join.empty()) { - return; - } - - for (auto& thread : threads_to_join) { - if (thread.joinable()) { - thread.join(); - } - } -} - -void Render::CleanSubStreamWindowProperties( - std::shared_ptr props) { - if (props->stream_texture_) { - SDL_DestroyTexture(props->stream_texture_); - props->stream_texture_ = nullptr; - } - - { - std::lock_guard lock(props->video_frame_mutex_); - props->front_frame_.reset(); - props->back_frame_.reset(); - props->video_width_ = 0; - props->video_height_ = 0; - props->video_size_ = 0; - props->render_rect_dirty_ = true; - props->stream_cleanup_pending_ = false; - } -} - -std::shared_ptr -Render::GetSubStreamWindowPropertiesByRemoteId(const std::string& remote_id) { - if (remote_id.empty()) { - return nullptr; - } - - std::shared_lock lock(client_properties_mutex_); - auto it = client_properties_.find(remote_id); - if (it == client_properties_.end()) { - return nullptr; - } - return it->second; -} - -void Render::StartFileTransfer(std::shared_ptr props, - const std::filesystem::path& file_path, - const std::string& file_label, - const std::string& remote_id) { - const bool is_global = (props == nullptr); - PeerPtr* peer = is_global ? peer_ : props->peer_; - if (!peer) { - LOG_ERROR("StartFileTransfer: invalid peer"); - return; - } - - bool expected = false; - if (!(is_global ? file_transfer_.file_sending_ - : props->file_transfer_.file_sending_) - .compare_exchange_strong(expected, true)) { - // Already sending, this should not happen if called correctly - LOG_WARN( - "StartFileTransfer called but file_sending_ is already true, " - "file should have been queued: {}", - file_path.filename().string().c_str()); - return; - } - - auto props_weak = std::weak_ptr(props); - Render* render_ptr = this; - - std::thread([peer, file_path, file_label, props_weak, render_ptr, remote_id, - is_global]() { - auto props_locked = props_weak.lock(); - - FileTransferState* state = nullptr; - if (props_locked) { - state = &props_locked->file_transfer_; - } else if (is_global) { - state = &render_ptr->file_transfer_; - } else { - return; - } - - std::error_code ec; - uint64_t total_size = std::filesystem::file_size(file_path, ec); - if (ec) { - LOG_ERROR("Failed to get file size: {}", ec.message().c_str()); - state->file_sending_ = false; - return; - } - - state->file_sent_bytes_ = 0; - state->file_total_bytes_ = total_size; - state->file_send_rate_bps_ = 0; - state->file_transfer_window_visible_ = true; - { - std::lock_guard lock(state->file_transfer_mutex_); - state->file_send_start_time_ = std::chrono::steady_clock::now(); - state->file_send_last_update_time_ = state->file_send_start_time_; - state->file_send_last_bytes_ = 0; - } - - LOG_INFO( - "File transfer started: {} ({} bytes), file_sending_={}, " - "total_bytes_={}", - file_path.filename().string(), total_size, state->file_sending_.load(), - state->file_total_bytes_.load()); - - FileSender sender; - uint32_t file_id = FileSender::NextFileId(); - - if (props_locked) { - std::lock_guard lock( - render_ptr->file_id_to_props_mutex_); - render_ptr->file_id_to_props_[file_id] = props_weak; - } else { - std::lock_guard lock( - render_ptr->file_id_to_transfer_state_mutex_); - render_ptr->file_id_to_transfer_state_[file_id] = state; - } - - state->current_file_id_ = file_id; - - // Update file transfer list: mark as sending - // Find the queued file that matches the exact file path - { - std::lock_guard lock(state->file_transfer_list_mutex_); - for (auto& info : state->file_transfer_list_) { - if (info.file_path == file_path && - info.status == FileTransferState::FileTransferStatus::Queued) { - info.status = FileTransferState::FileTransferStatus::Sending; - info.file_id = file_id; - info.file_size = total_size; - info.sent_bytes = 0; - break; - } - } - } - - state->file_transfer_window_visible_ = true; - - // Progress will be updated via ACK from receiver - int ret = sender.SendFile( - file_path, file_path.filename().string(), - [peer, file_label, remote_id](const char* buf, size_t sz) -> int { - if (remote_id.empty()) { - return SendReliableDataFrame(peer, buf, sz, file_label.c_str()); - } else { - return SendReliableDataFrameToPeer( - peer, buf, sz, file_label.c_str(), remote_id.c_str(), - remote_id.size()); - } - }, - 64 * 1024, file_id); - - // file_sending_ should remain true until we receive the final ACK from - // receiver - // On error, set file_sending_ to false immediately to allow next file - if (ret != 0) { - state->file_sending_ = false; - state->file_transfer_window_visible_ = false; - state->file_sent_bytes_ = 0; - state->file_total_bytes_ = 0; - state->file_send_rate_bps_ = 0; - state->current_file_id_ = 0; - - // Unregister file_id mapping on error - if (props_locked) { - std::lock_guard lock( - render_ptr->file_id_to_props_mutex_); - render_ptr->file_id_to_props_.erase(file_id); - } else { - std::lock_guard lock( - render_ptr->file_id_to_transfer_state_mutex_); - render_ptr->file_id_to_transfer_state_.erase(file_id); - } - - // Update file transfer list: mark as failed - { - std::lock_guard lock(state->file_transfer_list_mutex_); - for (auto& info : state->file_transfer_list_) { - if (info.file_id == file_id) { - info.status = FileTransferState::FileTransferStatus::Failed; - break; - } - } - } - - LOG_ERROR("FileSender::SendFile failed for [{}], ret={}", - file_path.string().c_str(), ret); - - render_ptr->ProcessFileQueue(props_locked); - } - }).detach(); -} - -void Render::ProcessFileQueue( - std::shared_ptr props) { - FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_; - if (!state) { - return; - } - - if (state->file_sending_.load()) { - return; - } - - FileTransferState::QueuedFile queued_file; - { - std::lock_guard lock(state->file_queue_mutex_); - if (state->file_send_queue_.empty()) { - return; - } - queued_file = state->file_send_queue_.front(); - state->file_send_queue_.pop(); - } - - LOG_INFO("Processing next file in queue: {}", - queued_file.file_path.string().c_str()); - StartFileTransfer(props, queued_file.file_path, queued_file.file_label, - queued_file.remote_id); -} - -void Render::UpdateRenderRect() { - // std::shared_lock lock(client_properties_mutex_); - for (auto& [_, props] : client_properties_) { - if (!props->reset_control_bar_pos_) { - props->mouse_diff_control_bar_pos_x_ = 0; - props->mouse_diff_control_bar_pos_y_ = 0; - } - - if (!just_created_) { - props->reset_control_bar_pos_ = true; - } - - int stream_window_width, stream_window_height; - SDL_GetWindowSize(stream_window_, &stream_window_width, - &stream_window_height); - stream_window_width_ = (float)stream_window_width; - stream_window_height_ = (float)stream_window_height; - - float video_ratio = - (float)props->video_width_ / (float)props->video_height_; - float video_ratio_reverse = - (float)props->video_height_ / (float)props->video_width_; - - float render_area_width = props->render_window_width_; - float render_area_height = props->render_window_height_; - - props->stream_render_rect_last_ = props->stream_render_rect_; - - SDL_FRect rect_f{props->render_window_x_, props->render_window_y_, - render_area_width, render_area_height}; - if (render_area_width < render_area_height * video_ratio) { - rect_f.x = props->render_window_x_; - rect_f.y = std::abs(render_area_height - - render_area_width * video_ratio_reverse) / - 2.0f + - props->render_window_y_; - rect_f.w = render_area_width; - rect_f.h = render_area_width * video_ratio_reverse; - } else if (render_area_width > render_area_height * video_ratio) { - rect_f.x = - std::abs(render_area_width - render_area_height * video_ratio) / - 2.0f + - props->render_window_x_; - rect_f.y = props->render_window_y_; - rect_f.w = render_area_height * video_ratio; - rect_f.h = render_area_height; - } else { - rect_f.x = props->render_window_x_; - rect_f.y = props->render_window_y_; - rect_f.w = render_area_width; - rect_f.h = render_area_height; - } - - props->stream_render_rect_f_ = rect_f; - props->stream_render_rect_ = {static_cast(std::lround(rect_f.x)), - static_cast(std::lround(rect_f.y)), - static_cast(std::lround(rect_f.w)), - static_cast(std::lround(rect_f.h))}; - } -} - -void Render::ProcessSdlEvent(const SDL_Event& event) { - if (main_ctx_) { - ImGui::SetCurrentContext(main_ctx_); - ImGui_ImplSDL3_ProcessEvent(&event); - } else { - LOG_ERROR("Main context is null"); - return; - } - - if (stream_window_inited_) { - if (stream_ctx_) { - ImGui::SetCurrentContext(stream_ctx_); - ImGui_ImplSDL3_ProcessEvent(&event); - } else { - LOG_ERROR("Stream context is null"); - return; - } - } - - if (server_window_inited_) { - if (server_ctx_) { - ImGui::SetCurrentContext(server_ctx_); - ImGui_ImplSDL3_ProcessEvent(&event); - } else { - LOG_ERROR("Server context is null"); - return; - } - } - - if (APP_EXIT_EVENT != 0 && event.type == APP_EXIT_EVENT) { - LOG_INFO("Quit program from system tray"); - if (stream_window_) { - SDL_SetWindowMouseGrab(stream_window_, false); - } -#if defined(__linux__) && !defined(__APPLE__) - if (tray_) { - tray_->RemoveTrayIcon(); - } -#endif - exit_ = true; - return; - } - - if (REMOTE_CLIPBOARD_EVENT != 0 && - event.type == REMOTE_CLIPBOARD_EVENT) { - ApplyPendingRemoteClipboardText(); - return; - } - - switch (event.type) { - case SDL_EVENT_QUIT: - if (stream_window_inited_) { - LOG_INFO("Destroy stream window"); - SDL_SetWindowMouseGrab(stream_window_, false); - DestroyStreamWindow(); - DestroyStreamWindowContext(); - - { - // std::shared_lock lock(client_properties_mutex_); - for (auto& [host_name, props] : client_properties_) { - std::shared_ptr> frame_snapshot; - int video_width = 0; - int video_height = 0; - { - std::lock_guard lock(props->video_frame_mutex_); - frame_snapshot = props->front_frame_; - video_width = props->video_width_; - video_height = props->video_height_; - } - if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 && - video_height > 0) { - thumbnail_->SaveToThumbnail( - (char*)frame_snapshot->data(), video_width, video_height, - host_name, props->remote_host_name_, - props->remember_password_ ? props->remote_password_ : ""); - } - - if (props->peer_) { - std::string client_id = (host_name == client_id_) - ? "C-" + std::string(client_id_) - : client_id_; - LOG_INFO("[{}] Leave connection [{}]", client_id, host_name); - LeaveConnection(props->peer_, host_name.c_str()); - LOG_INFO("Destroy peer [{}]", client_id); - DestroyPeer(&props->peer_); - } - - props->streaming_ = false; - props->remember_password_ = false; - props->connection_established_ = false; - props->audio_capture_button_pressed_ = false; - - memset(&props->net_traffic_stats_, 0, - sizeof(props->net_traffic_stats_)); - SDL_SetWindowFullscreen(main_window_, false); - SDL_FlushEvents(STREAM_REFRESH_EVENT, STREAM_REFRESH_EVENT); - memset(audio_buffer_, 0, 720); - } - } - - { - // std::unique_lock lock(client_properties_mutex_); - client_properties_.clear(); - } - - rejoin_ = false; - is_client_mode_ = false; - reload_recent_connections_ = true; - fullscreen_button_pressed_ = false; - start_keyboard_capturer_ = false; - just_created_ = false; - recent_connection_image_save_time_ = SDL_GetTicks(); - } else { - LOG_INFO("Quit program"); - exit_ = true; - } - break; - - case SDL_EVENT_WINDOW_CLOSE_REQUESTED: - if (stream_window_ && - event.window.windowID == SDL_GetWindowID(stream_window_)) { - break; - } - - if (main_window_ && - event.window.windowID == SDL_GetWindowID(main_window_) && - MinimizeMainWindowToTray()) { - break; - } - - exit_ = true; - break; - - case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: - if (stream_window_created_ && - event.window.windowID == SDL_GetWindowID(stream_window_)) { - UpdateRenderRect(); - } - break; - - case SDL_EVENT_WINDOW_FOCUS_GAINED: - if (stream_window_ && - SDL_GetWindowID(stream_window_) == event.window.windowID) { - focus_on_stream_window_ = true; - } else if (main_window_ && - SDL_GetWindowID(main_window_) == event.window.windowID) { - foucs_on_main_window_ = true; - } - break; - - case SDL_EVENT_WINDOW_FOCUS_LOST: - if (stream_window_ && - SDL_GetWindowID(stream_window_) == event.window.windowID) { - ForceReleasePressedKeys(); - focus_on_stream_window_ = false; - } else if (main_window_ && - SDL_GetWindowID(main_window_) == event.window.windowID) { - foucs_on_main_window_ = false; - } - break; - case SDL_EVENT_DROP_FILE: - ProcessFileDropEvent(event); - break; - - case SDL_EVENT_CLIPBOARD_UPDATE: - HandleClipboardUpdate(); - break; - - case SDL_EVENT_MOUSE_MOTION: - case SDL_EVENT_MOUSE_BUTTON_DOWN: - case SDL_EVENT_MOUSE_BUTTON_UP: - case SDL_EVENT_MOUSE_WHEEL: { - Uint32 mouse_window_id = 0; - if (event.type == SDL_EVENT_MOUSE_MOTION) { - mouse_window_id = event.motion.windowID; - } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || - event.type == SDL_EVENT_MOUSE_BUTTON_UP) { - mouse_window_id = event.button.windowID; - } else if (event.type == SDL_EVENT_MOUSE_WHEEL) { - mouse_window_id = event.wheel.windowID; - } - - if (focus_on_stream_window_ && stream_window_ && - SDL_GetWindowID(stream_window_) == mouse_window_id) { - ProcessMouseEvent(event); - } - break; - } - - case SDL_EVENT_KEY_DOWN: - case SDL_EVENT_KEY_UP: - if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ && - focus_on_stream_window_ && stream_window_ && - SDL_GetWindowID(stream_window_) == event.key.windowID) { - ProcessKeyboardEvent(event); - } - break; - - default: - if (event.type == STREAM_REFRESH_EVENT) { - auto* props = static_cast(event.user.data1); - if (!props) { - break; - } - std::shared_ptr> frame_snapshot; - int video_width = 0; - int video_height = 0; - bool render_rect_dirty = false; - bool cleanup_pending = false; - { - std::lock_guard lock(props->video_frame_mutex_); - cleanup_pending = props->stream_cleanup_pending_; - if (!cleanup_pending) { - frame_snapshot = props->front_frame_; - video_width = props->video_width_; - video_height = props->video_height_; - } - render_rect_dirty = props->render_rect_dirty_; - } - - if (cleanup_pending) { - if (props->stream_texture_) { - SDL_DestroyTexture(props->stream_texture_); - props->stream_texture_ = nullptr; - } - { - std::lock_guard lock(props->video_frame_mutex_); - props->stream_cleanup_pending_ = false; - } - - if (render_rect_dirty) { - UpdateRenderRect(); - std::lock_guard lock(props->video_frame_mutex_); - props->render_rect_dirty_ = false; - } - break; - } - - if (video_width <= 0 || video_height <= 0) { - break; - } - if (!frame_snapshot || frame_snapshot->empty()) { - break; - } - - if (props->stream_texture_) { - if (video_width != props->texture_width_ || - video_height != props->texture_height_) { - props->texture_width_ = video_width; - props->texture_height_ = video_height; - - SDL_DestroyTexture(props->stream_texture_); - // props->stream_texture_ = SDL_CreateTexture( - // stream_renderer_, stream_pixformat_, - // SDL_TEXTUREACCESS_STREAMING, props->texture_width_, - // props->texture_height_); - - SDL_PropertiesID nvProps = SDL_CreateProperties(); - SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, - props->texture_width_); - SDL_SetNumberProperty(nvProps, - SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, - props->texture_height_); - SDL_SetNumberProperty(nvProps, - SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, - SDL_PIXELFORMAT_NV12); - SDL_SetNumberProperty(nvProps, - SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, - SDL_COLORSPACE_BT601_LIMITED); - props->stream_texture_ = - SDL_CreateTextureWithProperties(stream_renderer_, nvProps); - SDL_DestroyProperties(nvProps); - } - } else { - props->texture_width_ = video_width; - props->texture_height_ = video_height; - // props->stream_texture_ = SDL_CreateTexture( - // stream_renderer_, stream_pixformat_, - // SDL_TEXTUREACCESS_STREAMING, props->texture_width_, - // props->texture_height_); - - SDL_PropertiesID nvProps = SDL_CreateProperties(); - SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, - props->texture_width_); - SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, - props->texture_height_); - SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, - SDL_PIXELFORMAT_NV12); - SDL_SetNumberProperty(nvProps, - SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, - SDL_COLORSPACE_BT601_LIMITED); - props->stream_texture_ = - SDL_CreateTextureWithProperties(stream_renderer_, nvProps); - SDL_DestroyProperties(nvProps); - } - - SDL_UpdateTexture(props->stream_texture_, NULL, frame_snapshot->data(), - props->texture_width_); - - if (render_rect_dirty) { - UpdateRenderRect(); - std::lock_guard lock(props->video_frame_mutex_); - props->render_rect_dirty_ = false; - } - } - break; - } -} - -void Render::ProcessFileDropEvent(const SDL_Event& event) { - if (!((stream_window_ && - SDL_GetWindowID(stream_window_) == event.window.windowID) || - (server_window_ && - SDL_GetWindowID(server_window_) == event.window.windowID))) { - return; - } - - if (event.type != SDL_EVENT_DROP_FILE) { - return; - } - - if (SDL_GetWindowID(stream_window_) == event.window.windowID) { - if (!stream_window_inited_) { - return; - } - - std::shared_lock lock(client_properties_mutex_); - for (auto& [_, props] : client_properties_) { - if (props->tab_selected_) { - if (event.drop.data == nullptr) { - LOG_ERROR("ProcessFileDropEvent: drop event data is null"); - break; - } - - if (!props || !props->peer_) { - LOG_ERROR("ProcessFileDropEvent: invalid props or peer"); - break; - } - - std::string utf8_path = static_cast(event.drop.data); - std::filesystem::path file_path = std::filesystem::u8path(utf8_path); - - // Check if file exists - std::error_code ec; - if (!std::filesystem::exists(file_path, ec)) { - LOG_ERROR("ProcessFileDropEvent: file does not exist: {}", - file_path.string().c_str()); - break; - } - - // Check if it's a regular file - if (!std::filesystem::is_regular_file(file_path, ec)) { - LOG_ERROR("ProcessFileDropEvent: path is not a regular file: {}", - file_path.string().c_str()); - break; - } - - // Get file size - uint64_t file_size = std::filesystem::file_size(file_path, ec); - if (ec) { - LOG_ERROR("ProcessFileDropEvent: failed to get file size: {}", - ec.message().c_str()); - break; - } - - LOG_INFO("Drop file [{}] to send (size: {} bytes)", event.drop.data, - file_size); - - // Use ProcessSelectedFile to handle the file processing - ProcessSelectedFile(utf8_path, props, props->file_label_); - - break; - } - } - } else if (SDL_GetWindowID(server_window_) == event.window.windowID) { - if (!server_window_inited_) { - return; - } - - if (event.drop.data == nullptr) { - LOG_ERROR("ProcessFileDropEvent: drop event data is null"); - return; - } - - std::string utf8_path = static_cast(event.drop.data); - std::filesystem::path file_path = std::filesystem::u8path(utf8_path); - - // Check if file exists - std::error_code ec; - if (!std::filesystem::exists(file_path, ec)) { - LOG_ERROR("ProcessFileDropEvent: file does not exist: {}", - file_path.string().c_str()); - return; - } - - // Check if it's a regular file - if (!std::filesystem::is_regular_file(file_path, ec)) { - LOG_ERROR("ProcessFileDropEvent: path is not a regular file: {}", - file_path.string().c_str()); - return; - } - - // Get file size - uint64_t file_size = std::filesystem::file_size(file_path, ec); - if (ec) { - LOG_ERROR("ProcessFileDropEvent: failed to get file size: {}", - ec.message().c_str()); - return; - } - - LOG_INFO("Drop file [{}] on server window (size: {} bytes)", - event.drop.data, file_size); - - // Handle the dropped file on server window as needed - } -} -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/render.h b/src/gui/render.h index b6c423b..738ebb4 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -1,857 +1,28 @@ -/* - * @Author: DI JUNKUN - * @Date: 2024-05-29 - * Copyright (c) 2024 by DI JUNKUN, All Rights Reserved. - */ +#ifndef CROSSDESK_GUI_RENDER_H_ +#define CROSSDESK_GUI_RENDER_H_ -#ifndef _MAIN_WINDOW_H_ -#define _MAIN_WINDOW_H_ - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "IconsFontAwesome6.h" -#include "config_center.h" -#include "device_controller_factory.h" -#include "device_presence.h" -#include "imgui.h" -#include "imgui_impl_sdl3.h" -#include "imgui_impl_sdlrenderer3.h" -#include "imgui_internal.h" -#include "minirtc.h" -#include "path_manager.h" -#include "screen_capturer_factory.h" -#include "speaker_capturer_factory.h" -#include "thumbnail.h" - -#if _WIN32 -#include "win_tray.h" -#elif defined(__APPLE__) -#include "mac_tray.h" -#elif defined(__linux__) -#include "linux_tray.h" -#endif +#include namespace crossdesk { + +class GuiApplication; + +// Stable application-facing facade. The SDL application and feature +// controllers remain private implementation details. class Render { - public: - enum class RemoteUnlockState { - none, - service_unavailable, - lock_screen, - credential_ui, - secure_desktop, - }; - - struct FileTransferState { - std::atomic file_sending_ = false; - std::atomic file_sent_bytes_ = 0; - std::atomic file_total_bytes_ = 0; - std::atomic file_send_rate_bps_ = 0; - std::mutex file_transfer_mutex_; - std::chrono::steady_clock::time_point file_send_start_time_; - std::chrono::steady_clock::time_point file_send_last_update_time_; - uint64_t file_send_last_bytes_ = 0; - bool file_transfer_window_visible_ = false; - bool file_transfer_window_hovered_ = false; - std::atomic current_file_id_{0}; - - struct QueuedFile { - std::filesystem::path file_path; - std::string file_label; - std::string remote_id; - }; - std::queue file_send_queue_; - std::mutex file_queue_mutex_; - - enum class FileTransferStatus { Queued, Sending, Completed, Failed }; - - struct FileTransferInfo { - std::string file_name; - std::filesystem::path file_path; - uint64_t file_size = 0; - FileTransferStatus status = FileTransferStatus::Queued; - uint64_t sent_bytes = 0; - uint32_t file_id = 0; - uint32_t rate_bps = 0; - }; - std::vector file_transfer_list_; - std::mutex file_transfer_list_mutex_; - }; - - struct SubStreamWindowProperties { - Params params_; - PeerPtr* peer_ = nullptr; - std::string audio_label_ = "control_audio"; - std::string data_label_ = "data"; - std::string mouse_label_ = "mouse"; - std::string keyboard_label_ = "keyboard"; - std::string file_label_ = "file"; - std::string control_data_label_ = "control_data"; - std::string file_feedback_label_ = "file_feedback"; - std::string clipboard_label_ = "clipboard"; - std::string local_id_ = ""; - std::string remote_id_ = ""; - bool exit_ = false; - bool signal_connected_ = false; - SignalStatus signal_status_ = SignalStatus::SignalClosed; - bool connection_established_ = false; - bool rejoin_ = false; - std::atomic connection_attempt_active_ = false; - std::chrono::steady_clock::time_point connection_attempt_started_at_; - bool net_traffic_stats_button_pressed_ = false; - bool enable_mouse_control_ = true; - bool mouse_controller_is_started_ = false; - bool audio_capture_button_pressed_ = true; - bool control_mouse_ = true; - bool streaming_ = false; - bool is_control_bar_in_left_ = true; - bool control_bar_hovered_ = false; - bool display_selectable_hovered_ = false; - bool shortcut_selectable_hovered_ = false; - bool control_bar_expand_ = true; - bool reset_control_bar_pos_ = false; - bool control_window_width_is_changing_ = false; - bool control_window_height_is_changing_ = false; - bool p2p_mode_ = true; - bool remember_password_ = false; - char remote_password_[7] = ""; - float sub_stream_window_width_ = 1280; - float sub_stream_window_height_ = 720; - float control_window_min_width_ = 20; - float control_window_max_width_ = 300; - float control_window_min_height_ = 38; - float control_window_max_height_ = 180; - float control_window_width_ = 300; - float control_window_height_ = 38; - float control_bar_pos_x_ = 0; - float control_bar_pos_y_ = 30; - float mouse_diff_control_bar_pos_x_ = 0; - float mouse_diff_control_bar_pos_y_ = 0; - double control_bar_button_pressed_time_ = 0; - double net_traffic_stats_button_pressed_time_ = 0; - // Double-buffered NV12 frame storage. Written by decode callback thread, - // consumed by SDL main thread. - std::mutex video_frame_mutex_; - std::shared_ptr> front_frame_; - std::shared_ptr> back_frame_; - bool render_rect_dirty_ = false; - bool stream_cleanup_pending_ = false; - float mouse_pos_x_ = 0; - float mouse_pos_y_ = 0; - float mouse_pos_x_last_ = 0; - float mouse_pos_y_last_ = 0; - int texture_width_ = 1280; - int texture_height_ = 720; - int video_width_ = 0; - int video_height_ = 0; - int video_width_last_ = 0; - int video_height_last_ = 0; - int selected_display_ = 0; - size_t video_size_ = 0; - bool tab_selected_ = false; - bool tab_opened_ = true; - std::optional pos_x_before_docked_; - std::optional pos_y_before_docked_; - float render_window_x_ = 0; - float render_window_y_ = 0; - float render_window_width_ = 0; - float render_window_height_ = 0; - std::string fullscreen_button_label_ = "Fullscreen"; - std::string net_traffic_stats_button_label_ = "Show Net Traffic Stats"; - std::string mouse_control_button_label_ = "Mouse Control"; - std::string audio_capture_button_label_ = "Audio Capture"; - std::string remote_host_name_ = ""; - bool remote_service_status_received_ = false; - bool remote_service_available_ = false; - std::string remote_interactive_stage_ = ""; - std::vector display_info_list_; - SDL_Texture* stream_texture_ = nullptr; - uint8_t* argb_buffer_ = nullptr; - int argb_buffer_size_ = 0; - SDL_FRect stream_render_rect_f_ = {0.0f, 0.0f, 0.0f, 0.0f}; - SDL_Rect stream_render_rect_; - SDL_Rect stream_render_rect_last_; - ImVec2 control_window_pos_; - // Written from the minirtc/libnice callback thread (OnConnectionStatusCb) - // and the SDL main thread (remote_peer_panel connect button); read from - // the SDL main render thread (stream/control windows) and the SDL audio - // thread (SdlCaptureAudioIn). Atomic so those reads/writes are defined. - std::atomic connection_status_ = ConnectionStatus::Closed; - TraversalMode traversal_mode_ = TraversalMode::UnknownMode; - int fps_ = 0; - int frame_count_ = 0; - std::chrono::steady_clock::time_point last_time_; - XNetTrafficStats net_traffic_stats_; - - using QueuedFile = FileTransferState::QueuedFile; - using FileTransferStatus = FileTransferState::FileTransferStatus; - using FileTransferInfo = FileTransferState::FileTransferInfo; - FileTransferState file_transfer_; - }; - - public: +public: Render(); ~Render(); - public: + Render(const Render &) = delete; + Render &operator=(const Render &) = delete; + int Run(); - private: - void InitializeLogger(); - void InitializeSettings(); - void InitializeSDL(); - void InitializeModules(); - void InitializeMainWindow(); - void MainLoop(); - void UpdateLabels(); - void UpdateInteractions(); - void HandleRecentConnections(); - void HandleConnectionStatusChange(); - void HandlePendingPresenceProbe(); - void HandleConnectionTimeouts(); - void HandleStreamWindow(); - void HandleServerWindow(); - void Cleanup(); - void CleanupFactories(); - void CleanupPeer(std::shared_ptr props); - void CleanupPeers(); - void CleanSubStreamWindowProperties( - std::shared_ptr props); - void UpdateRenderRect(); - void ProcessSdlEvent(const SDL_Event& event); - void InitializeClipboard(); - void ShutdownClipboard(); - void QueueRemoteClipboardText(const char* data, size_t size); - void ApplyPendingRemoteClipboardText(); - void HandleClipboardUpdate(); - int SendClipboardTextToPeers(const std::string& text); - - void ProcessFileDropEvent(const SDL_Event& event); - - void ProcessSelectedFile( - const std::string& path, - const std::shared_ptr& props, - const std::string& file_label, const std::string& remote_id = ""); - - std::shared_ptr - GetSubStreamWindowPropertiesByRemoteId(const std::string& remote_id); - - private: - int CreateStreamRenderWindow(); - int TitleBar(bool main_window); - int MainWindow(); - int UpdateNotificationWindow(); - int StreamWindow(); - int ServerWindow(); - int RemoteClientInfoWindow(); - int LocalWindow(); - int RemoteWindow(); - int RecentConnectionsWindow(); - int SettingWindow(); - int SelfHostedServerWindow(); - int ControlWindow(std::shared_ptr& props); - int ControlBar(std::shared_ptr& props); - int AboutWindow(); - int StatusBar(); - bool ConnectionStatusWindow( - std::shared_ptr& props); - int ShowRecentConnections(); - bool OpenUrl(const std::string& url); - void Hyperlink(const std::string& label, const std::string& url, - const float window_width); - int FileTransferWindow(std::shared_ptr& props); - std::string OpenFileDialog(std::string title); - - private: - int ConnectTo(const std::string& remote_id, const char* password, - bool remember_password, bool bypass_presence_check = false); - int RequestSingleDevicePresence(const std::string& remote_id, - const char* password, bool remember_password); - int CreateMainWindow(); - int DestroyMainWindow(); - int CreateStreamWindow(); - int DestroyStreamWindow(); - int CreateServerWindow(); - int DestroyServerWindow(); - int SetupFontAndStyle(ImFont** system_chinese_font_out); - int DestroyMainWindowContext(); - int DestroyStreamWindowContext(); - int DestroyServerWindowContext(); - int DrawMainWindow(); - int DrawStreamWindow(); - int DrawServerWindow(); - int ConfirmDeleteConnection(); - int EditRecentConnectionAliasWindow(); - int OfflineWarningWindow(); - int NetTrafficStats(std::shared_ptr& props); - void DrawConnectionStatusText( - std::shared_ptr& props); - void DrawReceivingScreenText( - std::shared_ptr& props); - void ResetRemoteServiceStatus(SubStreamWindowProperties& props); - void ApplyRemoteServiceStatus(SubStreamWindowProperties& props, - const ServiceStatus& status); - RemoteUnlockState GetRemoteUnlockState( - const SubStreamWindowProperties& props) const; -#ifdef __APPLE__ - int RequestPermissionWindow(); - bool CheckScreenRecordingPermission(); - bool CheckAccessibilityPermission(); - void OpenScreenRecordingPreferences(); - void OpenAccessibilityPreferences(); - bool DrawToggleSwitch(const char* id, bool active, bool enabled); - void RefreshMacPermissionStatus(bool force); - bool EnsureMacScreenRecordingPermission(); - bool EnsureMacAccessibilityPermission(); -#endif - - public: - static void OnReceiveVideoBufferCb(const XVideoFrame* video_frame, - const char* user_id, size_t user_id_size, - const char* src_id, size_t src_id_size, - void* user_data); - - static void OnReceiveAudioBufferCb(const char* data, size_t size, - const char* user_id, size_t user_id_size, - const char* src_id, size_t src_id_size, - void* user_data); - - static void OnReceiveDataBufferCb(const char* data, size_t size, - const char* user_id, size_t user_id_size, - const char* src_id, size_t src_id_size, - void* user_data); - - static void OnSignalStatusCb(SignalStatus status, const char* user_id, - size_t user_id_size, void* user_data); - - static void OnSignalMessageCb(const char* message, size_t size, - void* user_data); - - static void OnConnectionStatusCb(ConnectionStatus status, const char* user_id, - size_t user_id_size, void* user_data); - - static void OnNetStatusReport(const char* client_id, size_t client_id_size, - TraversalMode mode, - const XNetTrafficStats* net_traffic_stats, - const char* user_id, const size_t user_id_size, - void* user_data); - - static SDL_HitTestResult HitTestCallback(SDL_Window* window, - const SDL_Point* area, void* data); - - static std::vector SerializeRemoteAction(const RemoteAction& action); - - static bool DeserializeRemoteAction(const char* data, size_t size, - RemoteAction& out); - - static void FreeRemoteAction(RemoteAction& action); - - private: - struct PressedKeyboardKey { - int key_code = 0; - uint32_t scan_code = 0; - bool extended = false; - }; - - struct RemoteKeyboardState { - std::unordered_map pressed_keys; - uint32_t last_seq = 0; - uint32_t last_seen_tick = 0; - bool keyboard_state_seen = false; - }; - - int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0, - bool extended = false); - static bool IsModifierVkKey(int key_code); - void TrackPressedKeyState(int key_code, bool is_down, uint32_t scan_code, - bool extended); - void ForceReleasePressedKeys(); - void SendKeyboardHeartbeat(bool force); - void ApplyRemoteKeyboardEvent(const std::string& remote_id, - const RemoteAction& remote_action); - void ApplyRemoteKeyboardState(const std::string& remote_id, - const RemoteAction& remote_action); - bool InjectRemoteKeyboardKey(int key_code, bool is_down, uint32_t scan_code, - bool extended); - void ReleaseRemotePressedKeys(const std::string& remote_id, - const char* reason); - void CheckRemoteKeyboardTimeouts(); - int ProcessKeyboardEvent(const SDL_Event& event); - int ProcessMouseEvent(const SDL_Event& event); - - static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len); - static void SdlCaptureAudioOut(void* userdata, Uint8* stream, int len); - - private: - int SaveSettingsIntoCacheFile(); - int LoadSettingsFromCacheFile(); - int LoadRecentConnectionAliases(); - int SaveRecentConnectionAliases() const; - std::string GetRecentConnectionDisplayName( - const Thumbnail::RecentConnection& connection) const; - void BeginEditRecentConnectionAlias( - const Thumbnail::RecentConnection& connection); - - int ScreenCapturerInit(); - int StartScreenCapturer(); - int StopScreenCapturer(); - - int StartSpeakerCapturer(); - int StopSpeakerCapturer(); - - int StartMouseController(); - int StopMouseController(); - - int StartKeyboardCapturer(); - int StopKeyboardCapturer(); - - int CreateConnectionPeer(); - - // File transfer helper functions - void StartFileTransfer(std::shared_ptr props, - const std::filesystem::path& file_path, - const std::string& file_label, - const std::string& remote_id = ""); - void ProcessFileQueue(std::shared_ptr props); - - int AudioDeviceInit(); - int AudioDeviceDestroy(); - void HandleWindowsServiceIntegration(); - bool MinimizeMainWindowToTray(); -#if _WIN32 - void ResetLocalWindowsServiceState(bool clear_pending_sas); -#if CROSSDESK_PORTABLE - enum class PortableServiceInstallState { - idle, - installing, - succeeded, - failed, - }; - - void CheckPortableWindowsService(); - int PortableServiceInstallWindow(); - void StartPortableWindowsServiceInstall(); - void JoinPortableWindowsServiceInstallThread(); -#endif -#endif - - private: - struct CDCache { - char client_id_with_password[17]; - int language; - int video_quality; - int video_frame_rate; - int video_encode_format; - bool enable_hardware_video_codec; - bool enable_turn; - bool enable_srtp; - - unsigned char key[16]; - unsigned char iv[16]; - }; - - struct CDCacheV2 { - char client_id_with_password[17]; - int language; - int video_quality; - int video_frame_rate; - int video_encode_format; - bool enable_hardware_video_codec; - bool enable_turn; - bool enable_srtp; - - unsigned char key[16]; - unsigned char iv[16]; - - char self_hosted_id[17]; - }; - - private: - CDCache cd_cache_; - CDCacheV2 cd_cache_v2_; - std::mutex cd_cache_mutex_; - std::unique_ptr config_center_; - ConfigCenter::LANGUAGE localization_language_ = - ConfigCenter::LANGUAGE::CHINESE; - std::unique_ptr path_manager_; - std::string exec_log_path_; - std::string dll_log_path_; - std::string cache_path_; - int localization_language_index_ = -1; - int localization_language_index_last_ = -1; - bool modules_inited_ = false; - /* ------ all windows property start ------ */ - float title_bar_width_ = 640; - float title_bar_height_ = 30; - float title_bar_button_width_ = 30; - float title_bar_button_height_ = 30; - /* ------ all windows property end ------ */ - - /* ------ main window property start ------ */ - // thumbnail - unsigned char aes128_key_[16]; - unsigned char aes128_iv_[16]; - std::shared_ptr thumbnail_; - - // recent connections - std::vector> - recent_connections_; - std::vector recent_connection_ids_; - int recent_connection_image_width_ = 160; - int recent_connection_image_height_ = 90; - uint32_t recent_connection_image_save_time_ = 0; - DevicePresence device_presence_; - bool need_to_send_recent_connections_ = true; - - // main window render - SDL_Window* main_window_ = nullptr; - SDL_Renderer* main_renderer_ = nullptr; - ImGuiContext* main_ctx_ = nullptr; - ImFont* main_windows_system_chinese_font_ = nullptr; - ImFont* stream_windows_system_chinese_font_ = nullptr; - ImFont* server_windows_system_chinese_font_ = nullptr; - bool exit_ = false; - const int sdl_refresh_ms_ = 16; // ~60 FPS -#if _WIN32 - std::unique_ptr tray_; -#elif defined(__APPLE__) - std::unique_ptr tray_; -#elif defined(__linux__) - std::unique_ptr tray_; -#endif - - // main window properties - nlohmann::json latest_version_info_ = nlohmann::json{}; - bool update_available_ = false; - std::string latest_version_ = ""; - std::string release_notes_ = ""; - bool start_mouse_controller_ = false; - bool mouse_controller_is_started_ = false; - bool start_screen_capturer_ = false; - bool screen_capturer_is_started_ = false; - bool start_speaker_capturer_ = false; - bool speaker_capturer_is_started_ = false; - bool start_keyboard_capturer_ = false; - bool show_cursor_ = false; - bool keyboard_capturer_is_started_ = false; - bool keyboard_capturer_uses_sdl_events_ = false; - bool foucs_on_main_window_ = false; - bool focus_on_stream_window_ = false; - bool main_window_minimized_ = false; - uint32_t last_main_minimize_request_tick_ = 0; - uint32_t last_stream_minimize_request_tick_ = 0; - bool audio_capture_ = false; - int main_window_width_real_ = 720; - int main_window_height_real_ = 540; - float main_window_dpi_scaling_w_ = 1.0f; - float main_window_dpi_scaling_h_ = 1.0f; - float dpi_scale_ = 1.0f; - float main_window_width_default_ = 640; - float main_window_height_default_ = 480; - float main_window_width_ = 640; - float main_window_height_ = 480; - float main_window_width_last_ = 640; - float main_window_height_last_ = 480; - float local_window_width_ = 320; - float local_window_height_ = 235; - float remote_window_width_ = 320; - float remote_window_height_ = 235; - float local_child_window_width_ = 266; - float local_child_window_height_ = 180; - float remote_child_window_width_ = 266; - float remote_child_window_height_ = 180; - float main_window_text_y_padding_ = 10; - float main_child_window_x_padding_ = 27; - float main_child_window_y_padding_ = 45; - float status_bar_height_ = 22; - float connection_status_window_width_ = 200; - float connection_status_window_height_ = 150; - float notification_window_width_ = 200; - float notification_window_height_ = 80; - float about_window_width_ = 300; - float about_window_height_ = 170; - float update_notification_window_width_ = 400; - float update_notification_window_height_ = 320; - int screen_width_ = 1280; - int screen_height_ = 720; - int selected_display_ = 0; - std::string connect_button_label_ = "Connect"; - char input_password_tmp_[7] = ""; - char input_password_[7] = ""; - std::string random_password_ = ""; - char new_password_[7] = ""; - char remote_id_display_[12] = ""; - unsigned char audio_buffer_[720]; - int audio_len_ = 0; - bool audio_buffer_fresh_ = false; - bool need_to_rejoin_ = false; - std::chrono::steady_clock::time_point last_rejoin_check_time_; - bool just_created_ = false; - std::string controlled_remote_id_ = ""; - std::string focused_remote_id_ = ""; - std::string remote_client_id_ = ""; - std::unordered_map pressed_keyboard_keys_; - std::mutex pressed_keyboard_keys_mutex_; - uint32_t keyboard_state_seq_ = 0; - uint32_t last_keyboard_heartbeat_tick_ = 0; - std::unordered_map remote_keyboard_states_; - std::mutex remote_keyboard_states_mutex_; - SDL_Event last_mouse_event{}; - SDL_AudioStream* output_stream_ = nullptr; - uint32_t STREAM_REFRESH_EVENT = 0; - uint32_t APP_EXIT_EVENT = 0; - uint32_t REMOTE_CLIPBOARD_EVENT = 0; - std::atomic clipboard_events_enabled_{false}; - std::mutex pending_clipboard_mutex_; - std::optional pending_remote_clipboard_text_; - std::string last_clipboard_text_; -#if _WIN32 - std::atomic pending_windows_service_sas_{false}; - bool local_service_status_received_ = false; - bool local_service_available_ = false; - std::string local_interactive_stage_; - uint32_t last_local_secure_input_block_log_tick_ = 0; - uint32_t last_windows_service_status_tick_ = 0; - uint32_t optimistic_windows_secure_desktop_until_tick_ = 0; -#if CROSSDESK_PORTABLE - bool portable_service_prompt_checked_ = false; - bool show_portable_service_install_window_ = false; - bool show_portable_service_prompt_suppressed_window_ = false; - bool portable_service_do_not_remind_ = false; - bool portable_service_prompt_suppressed_ = false; - std::atomic portable_service_install_state_{ - PortableServiceInstallState::idle}; - std::thread portable_service_install_thread_; -#endif -#endif - - // stream window render - SDL_Window* stream_window_ = nullptr; - SDL_Renderer* stream_renderer_ = nullptr; - ImGuiContext* stream_ctx_ = nullptr; - - // stream window properties - bool need_to_create_stream_window_ = false; - bool stream_window_created_ = false; - bool stream_window_inited_ = false; - bool window_maximized_ = false; - bool stream_window_grabbed_ = false; - bool control_mouse_ = false; - int stream_window_width_default_ = 1280; - int stream_window_height_default_ = 720; - float stream_window_width_ = 1280; - float stream_window_height_ = 720; - SDL_PixelFormat stream_pixformat_ = SDL_PIXELFORMAT_NV12; - int stream_window_width_real_ = 1280; - int stream_window_height_real_ = 720; - float stream_window_dpi_scaling_w_ = 1.0f; - float stream_window_dpi_scaling_h_ = 1.0f; - - // server window render - SDL_Window* server_window_ = nullptr; - SDL_Renderer* server_renderer_ = nullptr; - ImGuiContext* server_ctx_ = nullptr; - - // server window properties - bool need_to_create_server_window_ = false; - bool need_to_destroy_server_window_ = false; - bool server_window_created_ = false; - bool server_window_inited_ = false; - int server_window_width_default_ = 250; - int server_window_height_default_ = 150; - float server_window_width_ = 250; - float server_window_height_ = 150; - float server_window_title_bar_height_ = 30.0f; - SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12; - int server_window_normal_width_ = 250; - int server_window_normal_height_ = 150; - float server_window_dpi_scaling_w_ = 1.0f; - float server_window_dpi_scaling_h_ = 1.0f; - float window_rounding_ = 6.0f; - float window_rounding_default_ = 6.0f; - - // server window collapsed mode - bool server_window_collapsed_ = false; - bool server_window_collapsed_dragging_ = false; - float server_window_collapsed_drag_start_mouse_x_ = 0.0f; - float server_window_collapsed_drag_start_mouse_y_ = 0.0f; - int server_window_collapsed_drag_start_win_x_ = 0; - int server_window_collapsed_drag_start_win_y_ = 0; - - // server window drag normal mode - bool server_window_dragging_ = false; - float server_window_drag_start_mouse_x_ = 0.0f; - float server_window_drag_start_mouse_y_ = 0.0f; - int server_window_drag_start_win_x_ = 0; - int server_window_drag_start_win_y_ = 0; - - bool label_inited_ = false; - bool connect_button_pressed_ = false; - bool password_validating_ = false; - uint32_t password_validating_time_ = 0; - bool show_settings_window_ = false; - bool show_self_hosted_server_config_window_ = false; - bool rejoin_ = false; - bool local_id_copied_ = false; - bool show_password_ = true; - bool show_about_window_ = false; - bool show_connection_status_window_ = false; - bool show_reset_password_window_ = false; - bool show_update_notification_window_ = false; - bool fullscreen_button_pressed_ = false; - bool focus_on_input_widget_ = true; - bool is_client_mode_ = false; - bool is_server_mode_ = false; - bool reload_recent_connections_ = true; - bool show_confirm_delete_connection_ = false; - bool show_edit_connection_alias_window_ = false; - bool show_offline_warning_window_ = false; - bool delete_connection_ = false; - bool is_tab_bar_hovered_ = false; - std::string delete_connection_name_ = ""; - std::unordered_map recent_connection_aliases_; - std::string edit_connection_alias_remote_id_ = ""; - char edit_connection_alias_[128] = ""; - std::string offline_warning_text_ = ""; - bool re_enter_remote_id_ = false; - double copy_start_time_ = 0; - SignalStatus signal_status_ = SignalStatus::SignalClosed; - std::string signal_status_str_ = ""; - bool signal_connected_ = false; - PeerPtr* peer_ = nullptr; - PeerPtr* peer_reserved_ = nullptr; - std::string video_primary_label_ = "primary_display"; - std::string video_secondary_label_ = "secondary_display"; - std::string audio_label_ = "audio"; - std::string data_label_ = "data"; - std::string mouse_label_ = "mouse"; - std::string keyboard_label_ = "keyboard"; - std::string info_label_ = "info"; - std::string control_data_label_ = "control_data"; - std::string file_label_ = "file"; - std::string file_feedback_label_ = "file_feedback"; - std::string clipboard_label_ = "clipboard"; - Params params_; - // Map file_id to props for tracking file transfer progress via ACK - std::unordered_map> - file_id_to_props_; - std::shared_mutex file_id_to_props_mutex_; - - // Map file_id to FileTransferState for global file transfer (props == null) - std::unordered_map file_id_to_transfer_state_; - std::shared_mutex file_id_to_transfer_state_mutex_; - SDL_AudioDeviceID input_dev_ = 0; - SDL_AudioDeviceID output_dev_ = 0; - ScreenCapturerFactory* screen_capturer_factory_ = nullptr; - ScreenCapturer* screen_capturer_ = nullptr; - SpeakerCapturerFactory* speaker_capturer_factory_ = nullptr; - SpeakerCapturer* speaker_capturer_ = nullptr; - DeviceControllerFactory* device_controller_factory_ = nullptr; - MouseController* mouse_controller_ = nullptr; - KeyboardCapturer* keyboard_capturer_ = nullptr; - std::vector display_info_list_; - uint64_t last_frame_time_ = 0; - std::string last_video_frame_stream_id_; - bool show_new_version_icon_ = false; - bool show_new_version_icon_in_menu_ = true; - double new_version_icon_last_trigger_time_ = 0.0; - double new_version_icon_render_start_time_ = 0.0; -#ifdef __APPLE__ - bool show_request_permission_window_ = true; - bool mac_permission_status_initialized_ = false; - uint32_t mac_permission_last_check_tick_ = 0; - bool mac_screen_recording_permission_granted_ = false; - bool mac_accessibility_permission_granted_ = false; - bool mac_screen_recording_permission_requested_ = false; - bool mac_accessibility_permission_requested_ = false; -#endif - char client_id_[10] = ""; - char client_id_display_[12] = ""; - char client_id_with_password_[17] = ""; - char password_saved_[7] = ""; - char self_hosted_id_[17] = ""; - char self_hosted_user_id_[17] = ""; - int language_button_value_ = 0; - int video_quality_button_value_ = 2; - int video_frame_rate_button_value_ = 1; - int video_encode_format_button_value_ = 0; - bool enable_hardware_video_codec_ = true; - bool enable_turn_ = true; - bool enable_srtp_ = false; - char signal_server_ip_[256] = "api.crossdesk.cn"; - char signal_server_port_[6] = "9099"; - char coturn_server_port_[6] = "3478"; - bool enable_self_hosted_ = false; - int language_button_value_last_ = 0; - int video_quality_button_value_last_ = 0; - int video_frame_rate_button_value_last_ = 0; - int video_encode_format_button_value_last_ = 0; - bool enable_hardware_video_codec_last_ = false; - bool enable_turn_last_ = true; - bool enable_srtp_last_ = false; - bool enable_self_hosted_last_ = false; - bool enable_autostart_ = false; - bool enable_autostart_last_ = false; - bool enable_daemon_ = false; - bool enable_daemon_last_ = false; - bool enable_minimize_to_tray_ = false; - bool enable_minimize_to_tray_last_ = false; - char file_transfer_save_path_buf_[512] = ""; - std::string file_transfer_save_path_last_ = ""; - char signal_server_ip_self_[256] = ""; - char signal_server_port_self_[6] = ""; - char coturn_server_port_self_[6] = ""; - bool settings_window_pos_reset_ = true; - bool self_hosted_server_config_window_pos_reset_ = true; - std::string selected_current_file_path_ = ""; - bool show_file_browser_ = true; - /* ------ main window property end ------ */ - - /* ------ sub stream window property start ------ */ - std::unordered_map> - client_properties_; - std::shared_mutex client_properties_mutex_; - void CloseTab(decltype(client_properties_)::iterator& it); - /* ------ stream window property end ------ */ - - /* ------ async thumbnail save tasks ------ */ - std::vector thumbnail_save_threads_; - std::mutex thumbnail_save_threads_mutex_; - void WaitForThumbnailSaveTasks(); - - /* ------ server mode ------ */ - // connection_status_ / connection_host_names_ are read on the main - // render thread (DrawServerWindow, HandleWindowsServiceIntegration) and - // written from minirtc/libnice callback threads (OnConnectionStatusCb, - // OnReceiveDataBufferCb). Guard every access with this shared mutex. - std::shared_mutex connection_status_mutex_; - std::unordered_map connection_status_; - std::unordered_map connection_host_names_; - std::string selected_server_remote_id_ = ""; - std::string selected_server_remote_hostname_ = ""; - std::mutex pending_presence_probe_mutex_; - bool pending_presence_probe_ = false; - bool pending_presence_result_ready_ = false; - bool pending_presence_online_ = false; - std::chrono::steady_clock::time_point pending_presence_probe_started_at_; - std::string pending_presence_remote_id_ = ""; - std::string pending_presence_password_ = ""; - bool pending_presence_remember_password_ = false; - FileTransferState file_transfer_; +private: + std::unique_ptr application_; }; -} // namespace crossdesk -#endif + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_RENDER_H_ diff --git a/src/gui/render_callback.cpp b/src/gui/render_callback.cpp deleted file mode 100644 index 986f1fe..0000000 --- a/src/gui/render_callback.cpp +++ /dev/null @@ -1,1898 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "device_controller.h" -#include "file_transfer.h" -#include "localization.h" -#include "minirtc.h" -#include "platform.h" -#include "rd_log.h" -#include "render.h" -#include "windows_key_metadata.h" -#if _WIN32 -#include "interactive_state.h" -#include "service_host.h" -#endif - -#define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2 - -namespace crossdesk { - -namespace { -constexpr uint32_t kKeyboardHeartbeatIntervalMs = 500; -constexpr uint32_t kRemoteKeyboardReleaseTimeoutMs = 2500; - -int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent& event) { - const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0; - - switch (event.scancode) { - case SDL_SCANCODE_NUMLOCKCLEAR: - return 0x90; - case SDL_SCANCODE_KP_ENTER: - return 0x0D; - case SDL_SCANCODE_KP_0: - if (!numlock_enabled) { - return 0x2D; - } - return 0x60; - case SDL_SCANCODE_KP_1: - if (!numlock_enabled) { - return 0x23; - } - return 0x61; - case SDL_SCANCODE_KP_2: - if (!numlock_enabled) { - return 0x28; - } - return 0x62; - case SDL_SCANCODE_KP_3: - if (!numlock_enabled) { - return 0x22; - } - return 0x63; - case SDL_SCANCODE_KP_4: - if (!numlock_enabled) { - return 0x25; - } - return 0x64; - case SDL_SCANCODE_KP_5: - return 0x65; - case SDL_SCANCODE_KP_6: - if (!numlock_enabled) { - return 0x27; - } - return 0x66; - case SDL_SCANCODE_KP_7: - if (!numlock_enabled) { - return 0x24; - } - return 0x67; - case SDL_SCANCODE_KP_8: - if (!numlock_enabled) { - return 0x26; - } - return 0x68; - case SDL_SCANCODE_KP_9: - if (!numlock_enabled) { - return 0x21; - } - return 0x69; - case SDL_SCANCODE_KP_PERIOD: - case SDL_SCANCODE_KP_COMMA: - if (!numlock_enabled) { - return 0x2E; - } - return 0x6E; - case SDL_SCANCODE_KP_DIVIDE: - return 0x6F; - case SDL_SCANCODE_KP_MULTIPLY: - return 0x6A; - case SDL_SCANCODE_KP_MINUS: - return 0x6D; - case SDL_SCANCODE_KP_PLUS: - return 0x6B; - case SDL_SCANCODE_KP_EQUALS: - return 0xBB; - default: - return -1; - } -} - -int TranslateSdlKeyboardEventToVk(const SDL_KeyboardEvent& event) { - const int keypad_key_code = TranslateSdlKeypadScancodeToVk(event); - if (keypad_key_code >= 0) { - return keypad_key_code; - } - - const int key = static_cast(event.key); - if (key >= 'a' && key <= 'z') { - return key - 'a' + 0x41; - } - if (key >= 'A' && key <= 'Z') { - return key; - } - if (key >= '0' && key <= '9') { - return key; - } - - switch (key) { - case ';': - return 0xBA; - case '\'': - return 0xDE; - case '`': - return 0xC0; - case ',': - return 0xBC; - case '.': - return 0xBE; - case '/': - return 0xBF; - case '\\': - return 0xDC; - case '[': - return 0xDB; - case ']': - return 0xDD; - case '-': - return 0xBD; - case '=': - return 0xBB; - default: - break; - } - - switch (event.scancode) { - case SDL_SCANCODE_ESCAPE: - return 0x1B; - case SDL_SCANCODE_RETURN: - return 0x0D; - case SDL_SCANCODE_SPACE: - return 0x20; - case SDL_SCANCODE_BACKSPACE: - return 0x08; - case SDL_SCANCODE_TAB: - return 0x09; - case SDL_SCANCODE_PRINTSCREEN: - return 0x2C; - case SDL_SCANCODE_SCROLLLOCK: - return 0x91; - case SDL_SCANCODE_PAUSE: - return 0x13; - case SDL_SCANCODE_INSERT: - return 0x2D; - case SDL_SCANCODE_DELETE: - return 0x2E; - case SDL_SCANCODE_HOME: - return 0x24; - case SDL_SCANCODE_END: - return 0x23; - case SDL_SCANCODE_PAGEUP: - return 0x21; - case SDL_SCANCODE_PAGEDOWN: - return 0x22; - case SDL_SCANCODE_LEFT: - return 0x25; - case SDL_SCANCODE_RIGHT: - return 0x27; - case SDL_SCANCODE_UP: - return 0x26; - case SDL_SCANCODE_DOWN: - return 0x28; - case SDL_SCANCODE_F1: - return 0x70; - case SDL_SCANCODE_F2: - return 0x71; - case SDL_SCANCODE_F3: - return 0x72; - case SDL_SCANCODE_F4: - return 0x73; - case SDL_SCANCODE_F5: - return 0x74; - case SDL_SCANCODE_F6: - return 0x75; - case SDL_SCANCODE_F7: - return 0x76; - case SDL_SCANCODE_F8: - return 0x77; - case SDL_SCANCODE_F9: - return 0x78; - case SDL_SCANCODE_F10: - return 0x79; - case SDL_SCANCODE_F11: - return 0x7A; - case SDL_SCANCODE_F12: - return 0x7B; - case SDL_SCANCODE_CAPSLOCK: - return 0x14; - case SDL_SCANCODE_LSHIFT: - return 0xA0; - case SDL_SCANCODE_RSHIFT: - return 0xA1; - case SDL_SCANCODE_LCTRL: - return 0xA2; - case SDL_SCANCODE_RCTRL: - return 0xA3; - case SDL_SCANCODE_LALT: - return 0xA4; - case SDL_SCANCODE_RALT: - return 0xA5; - case SDL_SCANCODE_LGUI: - return 0x5B; - case SDL_SCANCODE_RGUI: - return 0x5C; - default: - return -1; - } -} - -int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code, - bool extended) { -#if _WIN32 - if (key_code != 0x10 && key_code != 0x11 && key_code != 0x12) { - return key_code; - } - - UINT scan_code_with_prefix = static_cast(scan_code & 0xFF); - if (extended) { - scan_code_with_prefix |= 0xE000; - } - - const UINT normalized_vk = - MapVirtualKeyW(scan_code_with_prefix, MAPVK_VSC_TO_VK_EX); - return normalized_vk != 0 ? static_cast(normalized_vk) : key_code; -#else - (void)scan_code; - (void)extended; - return key_code; -#endif -} - -void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out, - bool* extended_out) { - if (scan_code_out == nullptr || extended_out == nullptr) { - return; - } - -#if _WIN32 - const UINT scan_code = - MapVirtualKeyW(static_cast(key_code), MAPVK_VK_TO_VSC_EX); - if (scan_code == 0) { - LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out); - return; - } - - *scan_code_out = static_cast(scan_code & 0xFF); - *extended_out = (scan_code & 0xFF00) != 0; -#else - LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out); -#endif -} - -#if _WIN32 -constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000; - -bool BuildAbsoluteMousePosition(const std::vector& displays, - int display_index, float normalized_x, - float normalized_y, int* absolute_x_out, - int* absolute_y_out) { - if (absolute_x_out == nullptr || absolute_y_out == nullptr || - display_index < 0 || display_index >= static_cast(displays.size())) { - return false; - } - - const DisplayInfo& display = displays[display_index]; - if (display.width <= 0 || display.height <= 0) { - return false; - } - - const float clamped_x = std::clamp(normalized_x, 0.0f, 1.0f); - const float clamped_y = std::clamp(normalized_y, 0.0f, 1.0f); - *absolute_x_out = static_cast(clamped_x * display.width) + display.left; - *absolute_y_out = static_cast(clamped_y * display.height) + display.top; - return true; -} - -void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* side, - const char* stage) { - if (last_tick == nullptr) { - return; - } - - const uint32_t now = static_cast(SDL_GetTicks()); - if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) { - return; - } - - *last_tick = now; - LOG_WARN( - "{} secure-desktop input blocked, stage={}, normal SendInput path " - "cannot drive the Windows password UI", - side != nullptr ? side : "unknown", stage != nullptr ? stage : ""); -} - -bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response, - const RemoteAction& action) { - if (!response.is_object()) { - return false; - } - if (response.value("error", std::string()) != "send_input_failed") { - return false; - } - if (response.value("code", 0u) != ERROR_ACCESS_DENIED) { - return false; - } - - return action.type == ControlType::keyboard && - action.k.flag == KeyFlag::key_up; -} -#endif - -} // namespace - -void Render::OnSignalMessageCb(const char* message, size_t size, - void* user_data) { - Render* render = (Render*)user_data; - if (!render || !message || size == 0) { - return; - } - std::string s(message, size); - auto j = nlohmann::json::parse(s, nullptr, false); - if (j.is_discarded() || !j.contains("type") || !j["type"].is_string()) { - return; - } - std::string type = j["type"].get(); - if (type == "presence") { - if (j.contains("devices") && j["devices"].is_array()) { - for (auto& dev : j["devices"]) { - if (!dev.is_object()) { - continue; - } - if (!dev.contains("id") || !dev["id"].is_string()) { - continue; - } - if (!dev.contains("online") || !dev["online"].is_boolean()) { - continue; - } - std::string id = dev["id"].get(); - bool online = dev["online"].get(); - render->device_presence_.SetOnline(id, online); - { - std::lock_guard lock( - render->pending_presence_probe_mutex_); - if (render->pending_presence_probe_ && - render->pending_presence_remote_id_ == id) { - render->pending_presence_result_ready_ = true; - render->pending_presence_online_ = online; - } - } - } - } - } else if (type == "presence_update") { - if (j.contains("id") && j["id"].is_string() && j.contains("online") && - j["online"].is_boolean()) { - std::string id = j["id"].get(); - bool online = j["online"].get(); - if (!id.empty()) { - render->device_presence_.SetOnline(id, online); - { - std::lock_guard lock( - render->pending_presence_probe_mutex_); - if (render->pending_presence_probe_ && - render->pending_presence_remote_id_ == id) { - render->pending_presence_result_ready_ = true; - render->pending_presence_online_ = online; - } - } - } - } - } -} - -bool Render::IsModifierVkKey(int key_code) { - switch (key_code) { - case 0x10: // VK_SHIFT - case 0x11: // VK_CONTROL - case 0x12: // VK_MENU(ALT) - case 0x5B: // VK_LWIN - case 0x5C: // VK_RWIN - case 0xA0: // VK_LSHIFT - case 0xA1: // VK_RSHIFT - case 0xA2: // VK_LCONTROL - case 0xA3: // VK_RCONTROL - case 0xA4: // VK_LMENU - case 0xA5: // VK_RMENU - return true; - default: - return false; - } -} - -void Render::TrackPressedKeyState(int key_code, bool is_down, - uint32_t scan_code, bool extended) { - std::lock_guard lock(pressed_keyboard_keys_mutex_); - if (is_down) { - pressed_keyboard_keys_[key_code] = - PressedKeyboardKey{key_code, scan_code, extended}; - } else { - pressed_keyboard_keys_.erase(key_code); - } -} - -void Render::ForceReleasePressedKeys() { - std::vector pressed_keys; - { - std::lock_guard lock(pressed_keyboard_keys_mutex_); - pressed_keys.reserve(pressed_keyboard_keys_.size()); - for (const auto& [_, key] : pressed_keyboard_keys_) { - pressed_keys.push_back(key); - } - pressed_keyboard_keys_.clear(); - } - - for (const PressedKeyboardKey& key : pressed_keys) { - SendKeyCommand(key.key_code, false, key.scan_code, key.extended); - } - SendKeyboardHeartbeat(true); -} - -void Render::SendKeyboardHeartbeat(bool force) { - const uint32_t now = static_cast(SDL_GetTicks()); - if (!force && now - last_keyboard_heartbeat_tick_ < - kKeyboardHeartbeatIntervalMs) { - return; - } - - RemoteAction remote_action{}; - remote_action.type = ControlType::keyboard_state; - remote_action.ks.seq = ++keyboard_state_seq_; - - { - std::lock_guard lock(pressed_keyboard_keys_mutex_); - size_t idx = 0; - for (const auto& [_, key] : pressed_keyboard_keys_) { - if (idx >= kMaxKeyboardStateKeys) { - LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}", - pressed_keyboard_keys_.size()); - break; - } - remote_action.ks.pressed_keys[idx].key_value = - static_cast(key.key_code); - remote_action.ks.pressed_keys[idx].scan_code = key.scan_code; - remote_action.ks.pressed_keys[idx].extended = key.extended; - ++idx; - } - remote_action.ks.pressed_count = idx; - } - - const std::string target_id = controlled_remote_id_.empty() - ? focused_remote_id_ - : controlled_remote_id_; - if (target_id.empty()) { - last_keyboard_heartbeat_tick_ = now; - return; - } - - auto props_it = client_properties_.find(target_id); - if (props_it == client_properties_.end()) { - last_keyboard_heartbeat_tick_ = now; - return; - } - - const auto props = props_it->second; - if (props->connection_status_.load() != ConnectionStatus::Connected || - !props->peer_) { - last_keyboard_heartbeat_tick_ = now; - return; - } - - const std::string msg = remote_action.to_json(); - const int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(), - props->keyboard_label_.c_str()); - if (ret != 0) { - LOG_WARN("Send keyboard heartbeat failed, remote_id={}, ret={}", target_id, - ret); - } - last_keyboard_heartbeat_tick_ = now; -} - -int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code, - bool extended) { - RemoteAction remote_action{}; - remote_action.type = ControlType::keyboard; - if (is_down) { - remote_action.k.flag = KeyFlag::key_down; - } else { - remote_action.k.flag = KeyFlag::key_up; - } - - if (scan_code == 0) { - PopulateWindowsKeyMetadataFromVk(key_code, &scan_code, &extended); - } -#if _WIN32 - key_code = NormalizeWindowsModifierVk(key_code, scan_code, extended); -#endif - - remote_action.k.key_value = key_code; - remote_action.k.scan_code = scan_code; - remote_action.k.extended = extended; - - std::string target_id = controlled_remote_id_.empty() ? focused_remote_id_ - : controlled_remote_id_; - if (!target_id.empty()) { - if (client_properties_.find(target_id) != client_properties_.end()) { - auto props = client_properties_[target_id]; - if (props->connection_status_.load() == ConnectionStatus::Connected && - props->peer_) { - std::string msg = remote_action.to_json(); - int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(), - props->keyboard_label_.c_str()); - if (ret != 0) { - LOG_WARN("Send keyboard command failed, remote_id={}, ret={}", - target_id, ret); - } - } - } - } - - TrackPressedKeyState(key_code, is_down, scan_code, extended); - - return 0; -} - -int Render::ProcessKeyboardEvent(const SDL_Event& event) { - if (event.type != SDL_EVENT_KEY_DOWN && event.type != SDL_EVENT_KEY_UP) { - return -1; - } - - if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) { - return 0; - } - - const int key_code = TranslateSdlKeyboardEventToVk(event.key); - if (key_code < 0) { - return 0; - } - - return SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN); -} - -bool Render::InjectRemoteKeyboardKey(int key_code, bool is_down, - uint32_t scan_code, bool extended) { -#if _WIN32 - if (local_service_status_received_ && - IsSecureDesktopInteractionRequired(local_interactive_stage_)) { - const std::string response = SendCrossDeskSecureDesktopKeyInput( - key_code, is_down, scan_code, extended, 1000); - auto json = nlohmann::json::parse(response, nullptr, false); - if (json.is_discarded() || !json.value("ok", false)) { - RemoteAction action{}; - action.type = ControlType::keyboard; - action.k.key_value = static_cast(key_code); - action.k.scan_code = scan_code; - action.k.extended = extended; - action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up; - if (!json.is_discarded() && - IsTransientSecureDesktopInputFailure(json, action)) { - LOG_INFO( - "Secure desktop keyboard injection transient failure, " - "key_code={}, is_down={}, response={}", - key_code, is_down, response); - return true; - } - - LogSecureDesktopInputBlocked(&last_local_secure_input_block_log_tick_, - "local", - local_interactive_stage_.c_str()); - LOG_WARN( - "Secure desktop keyboard injection failed, key_code={}, is_down={}, " - "response={}", - key_code, is_down, response); - return false; - } - return true; - } -#endif - - if (!keyboard_capturer_) { - return false; - } - - return keyboard_capturer_->SendKeyboardCommand(key_code, is_down, scan_code, - extended) == 0; -} - -void Render::ApplyRemoteKeyboardEvent(const std::string& remote_id, - const RemoteAction& remote_action) { - const int key_code = static_cast(remote_action.k.key_value); - const bool is_down = remote_action.k.flag == KeyFlag::key_down; - const uint32_t scan_code = remote_action.k.scan_code; - const bool extended = remote_action.k.extended; - const bool injected = - InjectRemoteKeyboardKey(key_code, is_down, scan_code, extended); - - std::lock_guard lock(remote_keyboard_states_mutex_); - auto& state = remote_keyboard_states_[remote_id]; - state.last_seen_tick = static_cast(SDL_GetTicks()); - if (is_down) { - if (injected) { - state.pressed_keys[key_code] = - PressedKeyboardKey{key_code, scan_code, extended}; - } - } else if (injected) { - state.pressed_keys.erase(key_code); - } -} - -void Render::ApplyRemoteKeyboardState(const std::string& remote_id, - const RemoteAction& remote_action) { - std::vector keys_to_release; - std::vector keys_to_press; - - { - std::lock_guard lock(remote_keyboard_states_mutex_); - auto& state = remote_keyboard_states_[remote_id]; - if (remote_action.ks.seq != 0 && state.last_seq != 0 && - static_cast(remote_action.ks.seq - state.last_seq) <= 0) { - return; - } - - state.last_seq = remote_action.ks.seq; - state.last_seen_tick = static_cast(SDL_GetTicks()); - state.keyboard_state_seen = true; - - std::unordered_map desired_keys; - const size_t pressed_count = - remote_action.ks.pressed_count < kMaxKeyboardStateKeys - ? remote_action.ks.pressed_count - : kMaxKeyboardStateKeys; - for (size_t idx = 0; idx < pressed_count; ++idx) { - const auto& key = remote_action.ks.pressed_keys[idx]; - const int key_code = static_cast(key.key_value); - desired_keys[key_code] = - PressedKeyboardKey{key_code, key.scan_code, key.extended}; - } - - for (const auto& [key_code, key] : state.pressed_keys) { - if (desired_keys.find(key_code) == desired_keys.end()) { - keys_to_release.push_back(key); - } - } - - for (const auto& [key_code, key] : desired_keys) { - if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) { - keys_to_press.push_back(key); - } - } - } - - for (const PressedKeyboardKey& key : keys_to_release) { - if (InjectRemoteKeyboardKey(key.key_code, false, key.scan_code, - key.extended)) { - std::lock_guard lock(remote_keyboard_states_mutex_); - auto state_it = remote_keyboard_states_.find(remote_id); - if (state_it != remote_keyboard_states_.end()) { - state_it->second.pressed_keys.erase(key.key_code); - } - } - } - - for (const PressedKeyboardKey& key : keys_to_press) { - if (InjectRemoteKeyboardKey(key.key_code, true, key.scan_code, - key.extended)) { - std::lock_guard lock(remote_keyboard_states_mutex_); - auto& state = remote_keyboard_states_[remote_id]; - state.pressed_keys[key.key_code] = key; - } - } -} - -void Render::ReleaseRemotePressedKeys(const std::string& remote_id, - const char* reason) { - std::vector keys_to_release; - { - std::lock_guard lock(remote_keyboard_states_mutex_); - auto state_it = remote_keyboard_states_.find(remote_id); - if (state_it == remote_keyboard_states_.end()) { - return; - } - - keys_to_release.reserve(state_it->second.pressed_keys.size()); - for (const auto& [_, key] : state_it->second.pressed_keys) { - keys_to_release.push_back(key); - } - remote_keyboard_states_.erase(state_it); - } - - if (!keys_to_release.empty()) { - LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}", - keys_to_release.size(), remote_id, reason ? reason : "unknown"); - } - for (const PressedKeyboardKey& key : keys_to_release) { - InjectRemoteKeyboardKey(key.key_code, false, key.scan_code, key.extended); - } -} - -void Render::CheckRemoteKeyboardTimeouts() { - const uint32_t now = static_cast(SDL_GetTicks()); - std::vector timed_out_remotes; - { - std::lock_guard lock(remote_keyboard_states_mutex_); - for (const auto& [remote_id, state] : remote_keyboard_states_) { - if (state.keyboard_state_seen && !state.pressed_keys.empty() && - state.last_seen_tick != 0 && - now - state.last_seen_tick > kRemoteKeyboardReleaseTimeoutMs) { - timed_out_remotes.push_back(remote_id); - } - } - } - - for (const std::string& remote_id : timed_out_remotes) { - ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout"); - } -} - -int Render::ProcessMouseEvent(const SDL_Event& event) { - controlled_remote_id_ = ""; - RemoteAction remote_action{}; - float cursor_x = last_mouse_event.motion.x; - float cursor_y = last_mouse_event.motion.y; - - auto normalize_cursor_to_window_space = [&](float* x, float* y) { - if (!x || !y || !stream_window_) { - return; - } - - int window_width = 0; - int window_height = 0; - int pixel_width = 0; - int pixel_height = 0; - SDL_GetWindowSize(stream_window_, &window_width, &window_height); - SDL_GetWindowSizeInPixels(stream_window_, &pixel_width, &pixel_height); - - if (window_width <= 0 || window_height <= 0 || pixel_width <= 0 || - pixel_height <= 0) { - return; - } - - if ((window_width != pixel_width || window_height != pixel_height) && - (*x > static_cast(window_width) + 1.0f || - *y > static_cast(window_height) + 1.0f)) { - const float scale_x = - static_cast(window_width) / static_cast(pixel_width); - const float scale_y = - static_cast(window_height) / static_cast(pixel_height); - *x *= scale_x; - *y *= scale_y; - - static bool logged_pixel_to_window_conversion = false; - if (!logged_pixel_to_window_conversion) { - LOG_INFO( - "Mouse coordinate space converted from pixels to window units: " - "window={}x{}, pixels={}x{}, scale=({:.4f},{:.4f})", - window_width, window_height, pixel_width, pixel_height, scale_x, - scale_y); - logged_pixel_to_window_conversion = true; - } - } - }; - - if (event.type == SDL_EVENT_MOUSE_MOTION) { - cursor_x = event.motion.x; - cursor_y = event.motion.y; - normalize_cursor_to_window_space(&cursor_x, &cursor_y); - } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || - event.type == SDL_EVENT_MOUSE_BUTTON_UP) { - cursor_x = event.button.x; - cursor_y = event.button.y; - normalize_cursor_to_window_space(&cursor_x, &cursor_y); - } else if (event.type == SDL_EVENT_MOUSE_WHEEL) { - cursor_x = last_mouse_event.motion.x; - cursor_y = last_mouse_event.motion.y; - } - - const bool is_pointer_position_event = - (event.type == SDL_EVENT_MOUSE_MOTION || - event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || - event.type == SDL_EVENT_MOUSE_BUTTON_UP); - - // std::shared_lock lock(client_properties_mutex_); - for (auto& it : client_properties_) { - auto props = it.second; - if (!props->control_mouse_) { - continue; - } - - const bool file_transfer_window_hovered = - props->file_transfer_.file_transfer_window_hovered_; - const bool overlay_hovered = - props->control_bar_hovered_ || props->display_selectable_hovered_ || - props->shortcut_selectable_hovered_ || file_transfer_window_hovered; - - const SDL_FRect render_rect = props->stream_render_rect_f_; - if (render_rect.w <= 1.0f || render_rect.h <= 1.0f) { - continue; - } - - if (is_pointer_position_event && cursor_x >= render_rect.x && - cursor_x <= render_rect.x + render_rect.w && - cursor_y >= render_rect.y && - cursor_y <= render_rect.y + render_rect.h) { - controlled_remote_id_ = it.first; - last_mouse_event.motion.x = cursor_x; - last_mouse_event.motion.y = cursor_y; - last_mouse_event.button.x = cursor_x; - last_mouse_event.button.y = cursor_y; - - remote_action.m.x = (cursor_x - render_rect.x) / render_rect.w; - remote_action.m.y = (cursor_y - render_rect.y) / render_rect.h; - remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f); - remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f); - - if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) { - remote_action.type = ControlType::mouse; - if (SDL_BUTTON_LEFT == event.button.button) { - remote_action.m.flag = MouseFlag::left_down; - } else if (SDL_BUTTON_RIGHT == event.button.button) { - remote_action.m.flag = MouseFlag::right_down; - } else if (SDL_BUTTON_MIDDLE == event.button.button) { - remote_action.m.flag = MouseFlag::middle_down; - } - } else if (SDL_EVENT_MOUSE_BUTTON_UP == event.type) { - remote_action.type = ControlType::mouse; - if (SDL_BUTTON_LEFT == event.button.button) { - remote_action.m.flag = MouseFlag::left_up; - } else if (SDL_BUTTON_RIGHT == event.button.button) { - remote_action.m.flag = MouseFlag::right_up; - } else if (SDL_BUTTON_MIDDLE == event.button.button) { - remote_action.m.flag = MouseFlag::middle_up; - } - } else if (SDL_EVENT_MOUSE_MOTION == event.type) { - remote_action.type = ControlType::mouse; - remote_action.m.flag = MouseFlag::move; - } - - if (overlay_hovered) { - break; - } - if (props->peer_) { - std::string msg = remote_action.to_json(); - SendDataFrame(props->peer_, msg.c_str(), msg.size(), - props->mouse_label_.c_str()); - } - } else if (SDL_EVENT_MOUSE_WHEEL == event.type && - last_mouse_event.button.x >= render_rect.x && - last_mouse_event.button.x <= render_rect.x + render_rect.w && - last_mouse_event.button.y >= render_rect.y && - last_mouse_event.button.y <= render_rect.y + render_rect.h) { - float scroll_x = event.wheel.x; - float scroll_y = event.wheel.y; - if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) { - scroll_x = -scroll_x; - scroll_y = -scroll_y; - } - - remote_action.type = ControlType::mouse; - - auto roundUp = [](float value) -> int { - if (value > 0) { - return static_cast(std::ceil(value)); - } else if (value < 0) { - return static_cast(std::floor(value)); - } - return 0; - }; - - if (std::abs(scroll_y) >= std::abs(scroll_x)) { - remote_action.m.flag = MouseFlag::wheel_vertical; - remote_action.m.s = roundUp(scroll_y); - } else { - remote_action.m.flag = MouseFlag::wheel_horizontal; - remote_action.m.s = roundUp(scroll_x); - } - - remote_action.m.x = (last_mouse_event.button.x - render_rect.x) / - (std::max)(render_rect.w, 1.0f); - remote_action.m.y = (last_mouse_event.button.y - render_rect.y) / - (std::max)(render_rect.h, 1.0f); - remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f); - remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f); - - if (overlay_hovered) { - continue; - } - if (props->peer_) { - std::string msg = remote_action.to_json(); - SendDataFrame(props->peer_, msg.c_str(), msg.size(), - props->mouse_label_.c_str()); - } - } - } - - return 0; -} - -void Render::SdlCaptureAudioIn(void* userdata, Uint8* stream, int len) { - Render* render = (Render*)userdata; - if (!render) { - return; - } - - if (1) { - std::shared_lock lock(render->client_properties_mutex_); - for (const auto& it : render->client_properties_) { - auto props = it.second; - if (props->connection_status_.load() == ConnectionStatus::Connected) { - if (props->peer_) { - SendAudioFrame(props->peer_, (const char*)stream, len, - render->audio_label_.c_str()); - } - } - } - - } else { - memcpy(render->audio_buffer_, stream, len); - render->audio_len_ = len; - SDL_Delay(10); - render->audio_buffer_fresh_ = true; - } -} - -void Render::SdlCaptureAudioOut([[maybe_unused]] void* userdata, - [[maybe_unused]] Uint8* stream, - [[maybe_unused]] int len) { - // Render *render = (Render *)userdata; - // for (auto it : render->client_properties_) { - // auto props = it.second; - // if (props->connection_status_ == SignalStatus::SignalConnected) { - // SendAudioFrame(props->peer_, (const char *)stream, len); - // } - // } - - // if (!render->audio_buffer_fresh_) { - // return; - // } - - // SDL_memset(stream, 0, len); - - // if (render->audio_len_ == 0) { - // return; - // } else { - // } - - // len = (len > render->audio_len_ ? render->audio_len_ : len); - // SDL_MixAudioFormat(stream, render->audio_buffer_, AUDIO_S16LSB, len, - // SDL_MIX_MAXVOLUME); - // render->audio_buffer_fresh_ = false; -} - -void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame, - const char* user_id, size_t user_id_size, - const char* src_id, size_t src_id_size, - void* user_data) { - Render* render = (Render*)user_data; - if (!render) { - return; - } - - std::string remote_id(user_id, user_id_size); - // std::shared_lock lock(render->client_properties_mutex_); - if (render->client_properties_.find(remote_id) == - render->client_properties_.end()) { - return; - } - SubStreamWindowProperties* props = - render->client_properties_.find(remote_id)->second.get(); - - if (props->connection_established_) { - { - std::lock_guard lock(props->video_frame_mutex_); - - if (!props->back_frame_) { - props->back_frame_ = - std::make_shared>(video_frame->size); - } - if (props->back_frame_->size() != video_frame->size) { - props->back_frame_->resize(video_frame->size); - } - - std::memcpy(props->back_frame_->data(), video_frame->data, - video_frame->size); - - 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; - - props->front_frame_.swap(props->back_frame_); - } - - SDL_Event event; - event.type = render->STREAM_REFRESH_EVENT; - event.user.data1 = props; - SDL_PushEvent(&event); - props->streaming_ = true; - - if (props->net_traffic_stats_button_pressed_) { - props->frame_count_++; - auto now = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration_cast( - now - props->last_time_) - .count(); - - if (elapsed >= 1000) { - props->fps_ = props->frame_count_ * 1000 / elapsed; - props->frame_count_ = 0; - props->last_time_ = now; - } - } - } -} - -void Render::OnReceiveAudioBufferCb(const char* data, size_t size, - const char* user_id, size_t user_id_size, - const char* src_id, size_t src_id_size, - void* user_data) { - Render* render = (Render*)user_data; - if (!render) { - return; - } - - render->audio_buffer_fresh_ = true; - - if (render->output_stream_) { - int pushed = SDL_PutAudioStreamData( - render->output_stream_, (const Uint8*)data, static_cast(size)); - if (pushed < 0) { - LOG_ERROR("Failed to push audio data: {}", SDL_GetError()); - } - } -} - -void Render::OnReceiveDataBufferCb(const char* data, size_t size, - const char* user_id, size_t user_id_size, - const char* src_id, size_t src_id_size, - void* user_data) { - Render* render = (Render*)user_data; - if (!render) { - return; - } - - std::string source_id = std::string(src_id, src_id_size); - if (source_id == render->file_label_) { - std::string remote_user_id = std::string(user_id, user_id_size); - - static FileReceiver receiver; - // Update output directory from config - std::string configured_path = - render->config_center_->GetFileTransferSavePath(); - if (!configured_path.empty()) { - receiver.SetOutputDir(std::filesystem::u8path(configured_path)); - } else if (receiver.OutputDir().empty()) { - receiver = FileReceiver(); // re-init with default desktop path - } - receiver.SetOnSendAck([render, - remote_user_id](const FileTransferAck& ack) -> int { - bool is_server_sending = remote_user_id.rfind("C-", 0) != 0; - if (is_server_sending) { - auto props = - render->GetSubStreamWindowPropertiesByRemoteId(remote_user_id); - if (props) { - PeerPtr* peer = props->peer_; - return SendReliableDataFrame( - peer, reinterpret_cast(&ack), - sizeof(FileTransferAck), render->file_feedback_label_.c_str()); - } - } - - return SendReliableDataFrame( - render->peer_, reinterpret_cast(&ack), - sizeof(FileTransferAck), render->file_feedback_label_.c_str()); - }); - - receiver.OnData(data, size); - return; - } else if (source_id == render->clipboard_label_) { - if (size > 0) { - std::string remote_user_id(user_id, user_id_size); - auto props = - render->GetSubStreamWindowPropertiesByRemoteId(remote_user_id); - if (props && !props->enable_mouse_control_) { - return; - } - - render->QueueRemoteClipboardText(data, size); - } - return; - } else if (source_id == render->file_feedback_label_) { - if (size < sizeof(FileTransferAck)) { - LOG_ERROR("FileTransferAck: buffer too small, size={}", size); - return; - } - - FileTransferAck ack{}; - memcpy(&ack, data, sizeof(FileTransferAck)); - - if (ack.magic != kFileAckMagic) { - LOG_ERROR( - "FileTransferAck: invalid magic, got 0x{:08X}, expected 0x{:08X}", - ack.magic, kFileAckMagic); - return; - } - - std::shared_ptr props = nullptr; - { - std::shared_lock lock(render->file_id_to_props_mutex_); - auto it = render->file_id_to_props_.find(ack.file_id); - if (it != render->file_id_to_props_.end()) { - props = it->second.lock(); - } - } - - Render::FileTransferState* state = nullptr; - if (!props) { - { - std::shared_lock lock(render->file_id_to_transfer_state_mutex_); - auto it = render->file_id_to_transfer_state_.find(ack.file_id); - if (it != render->file_id_to_transfer_state_.end()) { - state = it->second; - } - } - - if (!state) { - LOG_WARN("FileTransferAck: no props/state found for file_id={}", - ack.file_id); - return; - } - } else { - state = &props->file_transfer_; - } - - // Update progress based on ACK - state->file_sent_bytes_ = ack.acked_offset; - state->file_total_bytes_ = ack.total_size; - - uint32_t rate_bps = 0; - { - if (props) { - uint32_t data_channel_bitrate = - props->net_traffic_stats_.data_outbound_stats.bitrate; - - if (data_channel_bitrate > 0 && state->file_sending_.load()) { - rate_bps = static_cast(data_channel_bitrate * 0.99f); - - uint32_t current_rate = state->file_send_rate_bps_.load(); - if (current_rate > 0) { - // 70% old + 30% new for smoother display - rate_bps = - static_cast(current_rate * 0.7 + rate_bps * 0.3); - } - } else { - rate_bps = state->file_send_rate_bps_.load(); - } - } else { - // Global transfer: no per-connection bitrate available. - // Estimate send rate from ACKed bytes delta over time. - const uint32_t current_rate = state->file_send_rate_bps_.load(); - uint32_t estimated_rate_bps = 0; - const auto now = std::chrono::steady_clock::now(); - - uint64_t last_bytes = 0; - std::chrono::steady_clock::time_point last_time; - { - std::lock_guard lock(state->file_transfer_mutex_); - last_bytes = state->file_send_last_bytes_; - last_time = state->file_send_last_update_time_; - } - - if (state->file_sending_.load() && ack.acked_offset >= last_bytes) { - const uint64_t delta_bytes = ack.acked_offset - last_bytes; - const double delta_seconds = - std::chrono::duration(now - last_time).count(); - - if (delta_seconds > 0.0 && delta_bytes > 0) { - const double bps = - (static_cast(delta_bytes) * 8.0) / delta_seconds; - if (bps > 0.0) { - const double capped = - (std::min)(bps, static_cast( - (std::numeric_limits::max)())); - estimated_rate_bps = static_cast(capped); - } - } - } - - if (estimated_rate_bps > 0 && current_rate > 0) { - // 70% old + 30% new for smoother display - rate_bps = static_cast(current_rate * 0.7 + - estimated_rate_bps * 0.3); - } else if (estimated_rate_bps > 0) { - rate_bps = estimated_rate_bps; - } else { - rate_bps = current_rate; - } - } - - state->file_send_rate_bps_ = rate_bps; - state->file_send_last_bytes_ = ack.acked_offset; - auto now = std::chrono::steady_clock::now(); - state->file_send_last_update_time_ = now; - } - - // Update file transfer list: update progress and rate - { - std::lock_guard lock(state->file_transfer_list_mutex_); - for (auto& info : state->file_transfer_list_) { - if (info.file_id == ack.file_id) { - info.sent_bytes = ack.acked_offset; - info.file_size = ack.total_size; - info.rate_bps = rate_bps; - break; - } - } - } - - // Check if transfer is completed - if ((ack.flags & 0x01) != 0) { - // Transfer completed - receiver has finished receiving the file - // Reopen window if it was closed by user - state->file_transfer_window_visible_ = true; - state->file_sending_ = false; // Mark sending as finished - LOG_INFO( - "File transfer completed via ACK, file_id={}, total_size={}, " - "acked_offset={}", - ack.file_id, ack.total_size, ack.acked_offset); - - // Update file transfer list: mark as completed - { - std::lock_guard lock(state->file_transfer_list_mutex_); - for (auto& info : state->file_transfer_list_) { - if (info.file_id == ack.file_id) { - info.status = - Render::FileTransferState::FileTransferStatus::Completed; - info.sent_bytes = ack.total_size; - break; - } - } - } - - // Unregister file_id mapping after completion - { - if (props) { - std::lock_guard lock( - render->file_id_to_props_mutex_); - render->file_id_to_props_.erase(ack.file_id); - } else { - std::lock_guard lock( - render->file_id_to_transfer_state_mutex_); - render->file_id_to_transfer_state_.erase(ack.file_id); - } - } - - // Process next file in queue - render->ProcessFileQueue(props); - } - - return; - } - - std::string json_str(data, size); - RemoteAction remote_action{}; - if (!remote_action.from_json(json_str)) { - LOG_ERROR("Failed to parse RemoteAction JSON payload"); - return; - } - - std::string remote_id(user_id, user_id_size); - if (remote_action.type == ControlType::service_status) { - auto props_it = render->client_properties_.find(remote_id); - if (props_it != render->client_properties_.end()) { - render->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss); - } - return; - } - - if (remote_action.type == ControlType::service_command) { -#if _WIN32 - if (remote_action.c.flag == ServiceCommandFlag::send_sas) { - render->pending_windows_service_sas_.store(true, - std::memory_order_relaxed); - } else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) { - if (!LockWorkStation()) { - LOG_WARN("Remote lock workstation request failed, error={}", - GetLastError()); - } - } -#endif - return; - } - - if (remote_action.type == ControlType::host_infomation) { - bool is_client_mode = false; - std::shared_ptr props; - { - std::shared_lock lock(render->client_properties_mutex_); - auto props_it = render->client_properties_.find(remote_id); - if (props_it != render->client_properties_.end()) { - is_client_mode = true; - props = props_it->second; - } - } - - if (is_client_mode) { - // client mode - if (props && props->remote_host_name_.empty()) { - props->remote_host_name_ = std::string(remote_action.i.host_name, - remote_action.i.host_name_size); - LOG_INFO("Remote hostname: [{}]", props->remote_host_name_); - - for (int i = 0; i < remote_action.i.display_num; i++) { - props->display_info_list_.push_back( - DisplayInfo(remote_action.i.display_list[i], - remote_action.i.left[i], remote_action.i.top[i], - remote_action.i.right[i], remote_action.i.bottom[i])); - } - } - FreeRemoteAction(remote_action); - } else { - // server mode - std::string host_name(remote_action.i.host_name, - remote_action.i.host_name_size); - { - std::unique_lock lock(render->connection_status_mutex_); - render->connection_host_names_[remote_id] = host_name; - } - LOG_INFO("Remote hostname: [{}]", host_name); - FreeRemoteAction(remote_action); - } - } else { - // remote -#if _WIN32 - if (render->local_service_status_received_ && - IsSecureDesktopInteractionRequired(render->local_interactive_stage_) && - remote_action.type != ControlType::keyboard && - remote_action.type != ControlType::keyboard_state) { - if (remote_action.type == ControlType::mouse) { - int absolute_x = 0; - int absolute_y = 0; - if (!BuildAbsoluteMousePosition(render->display_info_list_, - render->selected_display_, - remote_action.m.x, remote_action.m.y, - &absolute_x, &absolute_y)) { - LOG_WARN( - "Secure desktop mouse injection skipped, invalid display " - "mapping: display_index={}, x={}, y={}", - render->selected_display_, remote_action.m.x, remote_action.m.y); - return; - } - - const std::string response = SendCrossDeskSecureDesktopMouseInput( - absolute_x, absolute_y, remote_action.m.s, - static_cast(remote_action.m.flag), 1000); - auto json = nlohmann::json::parse(response, nullptr, false); - if (json.is_discarded() || !json.value("ok", false)) { - LogSecureDesktopInputBlocked( - &render->last_local_secure_input_block_log_tick_, "local", - render->local_interactive_stage_.c_str()); - LOG_WARN( - "Secure desktop mouse injection failed, x={}, y={}, wheel={}, " - "flag={}, response={}", - absolute_x, absolute_y, remote_action.m.s, - static_cast(remote_action.m.flag), response); - } - return; - } - } -#endif - if (remote_action.type == ControlType::mouse && render->mouse_controller_) { - render->mouse_controller_->SendMouseCommand(remote_action, - render->selected_display_); - } else if (remote_action.type == ControlType::audio_capture) { - if (remote_action.a && !render->start_speaker_capturer_) - render->StartSpeakerCapturer(); - else if (!remote_action.a && render->start_speaker_capturer_) - render->StopSpeakerCapturer(); - } else if (remote_action.type == ControlType::keyboard) { - render->ApplyRemoteKeyboardEvent(remote_id, remote_action); - } else if (remote_action.type == ControlType::keyboard_state) { - render->ApplyRemoteKeyboardState(remote_id, remote_action); - } else if (remote_action.type == ControlType::display_id && - render->screen_capturer_) { - const int ret = render->screen_capturer_->SwitchTo(remote_action.d); - if (ret == 0) { - render->selected_display_ = remote_action.d; - } else { - LOG_WARN("Display switch skipped, invalid display_id={}", - remote_action.d); - } - } - } -} - -void Render::OnSignalStatusCb(SignalStatus status, const char* user_id, - size_t user_id_size, void* user_data) { - Render* render = (Render*)user_data; - if (!render) { - return; - } - - std::string client_id(user_id, user_id_size); - if (client_id == render->client_id_) { - render->signal_status_ = status; - if (SignalStatus::SignalConnecting == status) { - render->signal_connected_ = false; - } else if (SignalStatus::SignalConnected == status) { - render->signal_connected_ = true; - render->need_to_send_recent_connections_ = true; - LOG_INFO("[{}] connected to signal server", client_id); - } else if (SignalStatus::SignalFailed == status) { - render->signal_connected_ = false; - } else if (SignalStatus::SignalClosed == status) { - render->signal_connected_ = false; - } else if (SignalStatus::SignalReconnecting == status) { - render->signal_connected_ = false; - } else if (SignalStatus::SignalServerClosed == status) { - render->signal_connected_ = false; - } else if (SignalStatus::SignalTlsCertError == status) { - render->signal_connected_ = false; - } - } else { - if (client_id.rfind("C-", 0) != 0) { - return; - } - - std::string remote_id(client_id.begin() + 2, client_id.end()); - // std::shared_lock lock(render->client_properties_mutex_); - if (render->client_properties_.find(remote_id) == - render->client_properties_.end()) { - return; - } - auto props = render->client_properties_.find(remote_id)->second; - props->signal_status_ = status; - if (SignalStatus::SignalConnecting == status) { - props->signal_connected_ = false; - } else if (SignalStatus::SignalConnected == status) { - props->signal_connected_ = true; - LOG_INFO("[{}] connected to signal server", remote_id); - } else if (SignalStatus::SignalFailed == status) { - props->signal_connected_ = false; - } else if (SignalStatus::SignalClosed == status) { - props->signal_connected_ = false; - } else if (SignalStatus::SignalReconnecting == status) { - props->signal_connected_ = false; - } else if (SignalStatus::SignalServerClosed == status) { - props->signal_connected_ = false; - } else if (SignalStatus::SignalTlsCertError == status) { - props->signal_connected_ = false; - } - } -} - -void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id, - const size_t user_id_size, void* user_data) { - Render* render = (Render*)user_data; - if (!render) return; - - std::string remote_id(user_id, user_id_size); - std::shared_ptr props; - { - std::shared_lock lock(render->client_properties_mutex_); - auto it = render->client_properties_.find(remote_id); - if (it != render->client_properties_.end()) { - props = it->second; - } - } - - if (props) { - render->is_client_mode_ = true; - render->show_connection_status_window_ = true; - props->connection_status_.store(status); - if (status != ConnectionStatus::Connecting && - status != ConnectionStatus::Gathering) { - props->connection_attempt_active_.store(false); - } - - switch (status) { - case ConnectionStatus::Connected: { - render->ResetRemoteServiceStatus(*props); - { - RemoteAction remote_action; - remote_action.i.display_num = render->display_info_list_.size(); - remote_action.i.display_list = - (char**)malloc(remote_action.i.display_num * sizeof(char*)); - remote_action.i.left = - (int*)malloc(remote_action.i.display_num * sizeof(int)); - remote_action.i.top = - (int*)malloc(remote_action.i.display_num * sizeof(int)); - remote_action.i.right = - (int*)malloc(remote_action.i.display_num * sizeof(int)); - remote_action.i.bottom = - (int*)malloc(remote_action.i.display_num * sizeof(int)); - for (int i = 0; i < remote_action.i.display_num; i++) { - LOG_INFO("Local display [{}:{}]", i + 1, - render->display_info_list_[i].name); - remote_action.i.display_list[i] = - (char*)malloc(render->display_info_list_[i].name.length() + 1); - strncpy(remote_action.i.display_list[i], - render->display_info_list_[i].name.c_str(), - render->display_info_list_[i].name.length()); - remote_action.i - .display_list[i][render->display_info_list_[i].name.length()] = - '\0'; - remote_action.i.left[i] = render->display_info_list_[i].left; - remote_action.i.top[i] = render->display_info_list_[i].top; - remote_action.i.right[i] = render->display_info_list_[i].right; - remote_action.i.bottom[i] = render->display_info_list_[i].bottom; - } - - std::string host_name = GetHostName(); - remote_action.type = ControlType::host_infomation; - memcpy(&remote_action.i.host_name, host_name.data(), - host_name.size()); - remote_action.i.host_name[host_name.size()] = '\0'; - remote_action.i.host_name_size = host_name.size(); - - std::string msg = remote_action.to_json(); - int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(), - render->control_data_label_.c_str()); - FreeRemoteAction(remote_action); - } - - if (!render->need_to_create_stream_window_ && - !render->client_properties_.empty()) { - render->need_to_create_stream_window_ = true; - } - props->connection_established_ = true; - props->stream_render_rect_ = { - 0, (int)render->title_bar_height_, - (int)render->stream_window_width_, - (int)(render->stream_window_height_ - render->title_bar_height_)}; - props->stream_render_rect_f_ = { - 0.0f, render->title_bar_height_, render->stream_window_width_, - render->stream_window_height_ - render->title_bar_height_}; - render->start_keyboard_capturer_ = true; - break; - } - case ConnectionStatus::Disconnected: - case ConnectionStatus::Failed: - case ConnectionStatus::Closed: { - render->ReleaseRemotePressedKeys(remote_id, "connection_closed"); - props->connection_established_ = false; - props->enable_mouse_control_ = false; - render->ResetRemoteServiceStatus(*props); - - { - std::lock_guard lock(props->video_frame_mutex_); - props->front_frame_.reset(); - props->back_frame_.reset(); - props->video_width_ = 0; - props->video_height_ = 0; - props->video_size_ = 0; - props->render_rect_dirty_ = true; - props->stream_cleanup_pending_ = true; - } - - SDL_Event event; - event.type = render->STREAM_REFRESH_EVENT; - event.user.data1 = props.get(); - SDL_PushEvent(&event); - - render->focus_on_stream_window_ = false; - - break; - } - case ConnectionStatus::IncorrectPassword: { - render->password_validating_ = false; - render->password_validating_time_++; - if (render->connect_button_pressed_) { - render->connect_button_pressed_ = false; - props->connection_established_ = false; - render->connect_button_label_ = - localization::connect[render->localization_language_index_]; - } - break; - } - case ConnectionStatus::NoSuchTransmissionId: - case ConnectionStatus::RemoteUnavailable: { - if (render->connect_button_pressed_) { - props->connection_established_ = false; - render->connect_button_label_ = - localization::connect[render->localization_language_index_]; - } - break; - } - default: - break; - } - } else { - render->is_client_mode_ = false; - render->show_connection_status_window_ = true; - { - std::unique_lock lock(render->connection_status_mutex_); - render->connection_status_[remote_id] = status; - } - - switch (status) { - case ConnectionStatus::Connected: { -#if _WIN32 - render->last_windows_service_status_tick_ = 0; -#endif - { - RemoteAction remote_action; - remote_action.i.display_num = render->display_info_list_.size(); - remote_action.i.display_list = - (char**)malloc(remote_action.i.display_num * sizeof(char*)); - remote_action.i.left = - (int*)malloc(remote_action.i.display_num * sizeof(int)); - remote_action.i.top = - (int*)malloc(remote_action.i.display_num * sizeof(int)); - remote_action.i.right = - (int*)malloc(remote_action.i.display_num * sizeof(int)); - remote_action.i.bottom = - (int*)malloc(remote_action.i.display_num * sizeof(int)); - for (int i = 0; i < remote_action.i.display_num; i++) { - LOG_INFO("Local display [{}:{}]", i + 1, - render->display_info_list_[i].name); - remote_action.i.display_list[i] = - (char*)malloc(render->display_info_list_[i].name.length() + 1); - strncpy(remote_action.i.display_list[i], - render->display_info_list_[i].name.c_str(), - render->display_info_list_[i].name.length()); - remote_action.i - .display_list[i][render->display_info_list_[i].name.length()] = - '\0'; - remote_action.i.left[i] = render->display_info_list_[i].left; - remote_action.i.top[i] = render->display_info_list_[i].top; - remote_action.i.right[i] = render->display_info_list_[i].right; - remote_action.i.bottom[i] = render->display_info_list_[i].bottom; - } - - std::string host_name = GetHostName(); - remote_action.type = ControlType::host_infomation; - memcpy(&remote_action.i.host_name, host_name.data(), - host_name.size()); - remote_action.i.host_name[host_name.size()] = '\0'; - remote_action.i.host_name_size = host_name.size(); - - std::string msg = remote_action.to_json(); - int ret = SendReliableDataFrame(render->peer_, msg.data(), msg.size(), - render->control_data_label_.c_str()); - FreeRemoteAction(remote_action); - } - - render->need_to_create_server_window_ = true; - render->is_server_mode_ = true; - render->start_screen_capturer_ = true; - render->start_speaker_capturer_ = true; - render->remote_client_id_ = remote_id; - render->start_mouse_controller_ = true; - { - std::shared_lock lock(render->connection_status_mutex_); - if (std::all_of(render->connection_status_.begin(), - render->connection_status_.end(), [](const auto& kv) { - return kv.first.find("web") != std::string::npos; - })) { - render->show_cursor_ = true; - } - } - - break; - } - case ConnectionStatus::Disconnected: - case ConnectionStatus::Failed: - case ConnectionStatus::Closed: { - render->ReleaseRemotePressedKeys(remote_id, "connection_closed"); - bool all_disconnected = false; - { - std::shared_lock lock(render->connection_status_mutex_); - all_disconnected = std::all_of( - render->connection_status_.begin(), - render->connection_status_.end(), [](const auto& kv) { - return kv.second == ConnectionStatus::Closed || - kv.second == ConnectionStatus::Failed || - kv.second == ConnectionStatus::Disconnected; - }); - } - if (all_disconnected) { - render->need_to_destroy_server_window_ = true; - render->is_server_mode_ = false; -#if defined(__linux__) && !defined(__APPLE__) - if (IsWaylandSession()) { - // Keep Wayland capture session warm to avoid black screen on - // subsequent reconnects. - render->start_screen_capturer_ = true; - LOG_INFO( - "Keeping Wayland screen capturer running after " - "disconnect to preserve reconnect stability"); - } else { - render->start_screen_capturer_ = false; - } -#else - render->start_screen_capturer_ = false; -#endif - render->start_speaker_capturer_ = false; - render->start_mouse_controller_ = false; - render->start_keyboard_capturer_ = false; - render->remote_client_id_ = ""; - if (props) props->connection_established_ = false; - if (render->audio_capture_) { - render->StopSpeakerCapturer(); - render->audio_capture_ = false; - } - - { - std::unique_lock lock(render->connection_status_mutex_); - render->connection_status_.erase(remote_id); - render->connection_host_names_.erase(remote_id); - } - if (render->screen_capturer_) { - render->screen_capturer_->ResetToInitialMonitor(); - } - } - - { - std::shared_lock lock(render->connection_status_mutex_); - if (std::all_of(render->connection_status_.begin(), - render->connection_status_.end(), [](const auto& kv) { - return kv.first.find("web") == std::string::npos; - })) { - render->show_cursor_ = false; - } - } - - break; - } - default: - break; - } - } -} - -void Render::OnNetStatusReport(const char* client_id, size_t client_id_size, - TraversalMode mode, - const XNetTrafficStats* net_traffic_stats, - const char* user_id, const size_t user_id_size, - void* user_data) { - Render* render = (Render*)user_data; - if (!render) { - return; - } - - if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) { - std::string id, password; - const char* at_pos = strchr(client_id, '@'); - if (at_pos == nullptr) { - id = client_id; - password.clear(); - } else { - id.assign(client_id, at_pos - client_id); - password = at_pos + 1; - } - - bool is_self_hosted = render->config_center_->IsSelfHosted(); - - if (is_self_hosted) { - memset(&render->client_id_, 0, sizeof(render->client_id_)); - strncpy(render->client_id_, id.c_str(), sizeof(render->client_id_) - 1); - render->client_id_[sizeof(render->client_id_) - 1] = '\0'; - - memset(&render->password_saved_, 0, sizeof(render->password_saved_)); - strncpy(render->password_saved_, password.c_str(), - sizeof(render->password_saved_) - 1); - render->password_saved_[sizeof(render->password_saved_) - 1] = '\0'; - - memset(&render->self_hosted_id_, 0, sizeof(render->self_hosted_id_)); - strncpy(render->self_hosted_id_, client_id, - sizeof(render->self_hosted_id_) - 1); - render->self_hosted_id_[sizeof(render->self_hosted_id_) - 1] = '\0'; - - LOG_INFO("Use self-hosted client id [{}] and save to cache file", id); - - render->cd_cache_mutex_.lock(); - - std::ifstream v2_file_read(render->cache_path_ + "/secure_cache_v2.enc", - std::ios::binary); - if (v2_file_read.good()) { - v2_file_read.read(reinterpret_cast(&render->cd_cache_v2_), - sizeof(CDCacheV2)); - v2_file_read.close(); - } else { - memset(&render->cd_cache_v2_, 0, sizeof(CDCacheV2)); - memset(&render->cd_cache_v2_.client_id_with_password, 0, - sizeof(render->cd_cache_v2_.client_id_with_password)); - strncpy(render->cd_cache_v2_.client_id_with_password, - render->client_id_with_password_, - sizeof(render->cd_cache_v2_.client_id_with_password)); - memcpy(&render->cd_cache_v2_.key, &render->aes128_key_, - sizeof(render->cd_cache_v2_.key)); - memcpy(&render->cd_cache_v2_.iv, &render->aes128_iv_, - sizeof(render->cd_cache_v2_.iv)); - } - - memset(&render->cd_cache_v2_.self_hosted_id, 0, - sizeof(render->cd_cache_v2_.self_hosted_id)); - strncpy(render->cd_cache_v2_.self_hosted_id, client_id, - sizeof(render->cd_cache_v2_.self_hosted_id) - 1); - render->cd_cache_v2_ - .self_hosted_id[sizeof(render->cd_cache_v2_.self_hosted_id) - 1] = - '\0'; - - memset(&render->cd_cache_v2_.client_id_with_password, 0, - sizeof(render->cd_cache_v2_.client_id_with_password)); - strncpy(render->cd_cache_v2_.client_id_with_password, - render->client_id_with_password_, - sizeof(render->cd_cache_v2_.client_id_with_password)); - memcpy(&render->cd_cache_v2_.key, &render->aes128_key_, - sizeof(render->cd_cache_v2_.key)); - memcpy(&render->cd_cache_v2_.iv, &render->aes128_iv_, - sizeof(render->cd_cache_v2_.iv)); - std::ofstream cd_cache_v2_file( - render->cache_path_ + "/secure_cache_v2.enc", std::ios::binary); - if (cd_cache_v2_file) { - cd_cache_v2_file.write(reinterpret_cast(&render->cd_cache_v2_), - sizeof(CDCacheV2)); - cd_cache_v2_file.close(); - } - - render->cd_cache_mutex_.unlock(); - } else { - memset(&render->client_id_, 0, sizeof(render->client_id_)); - strncpy(render->client_id_, id.c_str(), sizeof(render->client_id_) - 1); - render->client_id_[sizeof(render->client_id_) - 1] = '\0'; - - memset(&render->password_saved_, 0, sizeof(render->password_saved_)); - strncpy(render->password_saved_, password.c_str(), - sizeof(render->password_saved_) - 1); - render->password_saved_[sizeof(render->password_saved_) - 1] = '\0'; - - memset(&render->client_id_with_password_, 0, - sizeof(render->client_id_with_password_)); - strncpy(render->client_id_with_password_, client_id, - sizeof(render->client_id_with_password_) - 1); - render - ->client_id_with_password_[sizeof(render->client_id_with_password_) - - 1] = '\0'; - - LOG_INFO("Use client id [{}] and save id into cache file", id); - render->SaveSettingsIntoCacheFile(); - } - } - - std::string remote_id(user_id, user_id_size); - // std::shared_lock lock(render->client_properties_mutex_); - if (render->client_properties_.find(remote_id) == - render->client_properties_.end()) { - return; - } - auto props = render->client_properties_.find(remote_id)->second; - if (props->traversal_mode_ != mode) { - props->traversal_mode_ = mode; - LOG_INFO("Net mode: [{}]", int(props->traversal_mode_)); - } - - if (!net_traffic_stats) { - return; - } - - // only display client side net status if connected to itself - if (!(render->peer_reserved_ && !strstr(client_id, "C-"))) { - props->net_traffic_stats_ = *net_traffic_stats; - } -} -} // namespace crossdesk diff --git a/src/gui/runtime/connection_runtime.cpp b/src/gui/runtime/connection_runtime.cpp new file mode 100644 index 0000000..8bb6ea2 --- /dev/null +++ b/src/gui/runtime/connection_runtime.cpp @@ -0,0 +1,439 @@ +#include "runtime/gui_runtime.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "localization.h" +#include "rd_log.h" + +namespace crossdesk { + +namespace { +constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5); +constexpr auto kConnectionAttemptTimeout = std::chrono::seconds(20); +bool IsConnectionAttemptPending(ConnectionStatus status) { + return status == ConnectionStatus::Connecting || + status == ConnectionStatus::Gathering; +} +} // namespace + +void GuiRuntime::HandleConnectionStatusChange() { + if (signal_connected_ && peer_ && need_to_send_recent_connections_) { + if (!recent_connection_ids_.empty()) { + nlohmann::json j; + j["type"] = "recent_connections_presence"; + j["user_id"] = client_id_; + j["devices"] = nlohmann::json::array(); + for (const auto &id : recent_connection_ids_) { + std::string pure_id = id; + size_t pos_y = pure_id.find('Y'); + size_t pos_n = pure_id.find('N'); + size_t pos = std::string::npos; + if (pos_y != std::string::npos && + (pos_n == std::string::npos || pos_y < pos_n)) { + pos = pos_y; + } else if (pos_n != std::string::npos) { + pos = pos_n; + } + if (pos != std::string::npos) { + pure_id = pure_id.substr(0, pos); + } + j["devices"].push_back(pure_id); + } + auto s = j.dump(); + SendSignalMessage(peer_, s.data(), s.size()); + } + } + need_to_send_recent_connections_ = false; +} + +void GuiRuntime::HandlePendingPresenceProbe() { + bool has_action = false; + bool should_connect = false; + bool remember_password = false; + std::string remote_id; + std::string password; + + { + std::lock_guard lock(pending_presence_probe_mutex_); + if (!pending_presence_probe_ || !pending_presence_result_ready_) { + return; + } + + has_action = true; + should_connect = pending_presence_online_; + remote_id = pending_presence_remote_id_; + password = pending_presence_password_; + remember_password = pending_presence_remember_password_; + + pending_presence_probe_ = false; + pending_presence_result_ready_ = false; + pending_presence_online_ = false; + pending_presence_remote_id_.clear(); + pending_presence_password_.clear(); + pending_presence_remember_password_ = false; + } + + if (!has_action) { + return; + } + + if (should_connect) { + ConnectTo(remote_id, password.c_str(), remember_password, true); + return; + } + + offline_warning_text_ = + localization::device_offline[localization_language_index_]; + show_offline_warning_window_ = true; +} + +void GuiRuntime::HandleConnectionTimeouts() { + const auto now = std::chrono::steady_clock::now(); + + bool presence_probe_timed_out = false; + std::string presence_remote_id; + { + std::lock_guard lock(pending_presence_probe_mutex_); + if (pending_presence_probe_ && !pending_presence_result_ready_ && + now - pending_presence_probe_started_at_ >= kPresenceProbeTimeout) { + presence_probe_timed_out = true; + presence_remote_id = pending_presence_remote_id_; + pending_presence_probe_ = false; + pending_presence_result_ready_ = false; + pending_presence_online_ = false; + pending_presence_remote_id_.clear(); + pending_presence_password_.clear(); + pending_presence_remember_password_ = false; + } + } + + if (presence_probe_timed_out) { + offline_warning_text_ = + localization::device_offline[localization_language_index_]; + show_offline_warning_window_ = true; + LOG_WARN("Presence probe timed out for [{}]", presence_remote_id); + } + + bool rejoin_state_changed = false; + for (auto &[_, props] : remote_sessions_) { + if (!props || !props->connection_attempt_active_.load()) { + continue; + } + + const ConnectionStatus status = props->connection_status_.load(); + if (!IsConnectionAttemptPending(status)) { + props->connection_attempt_active_.store(false); + continue; + } + + if (now - props->connection_attempt_started_at_ < + kConnectionAttemptTimeout) { + continue; + } + + LOG_WARN("Connection to [{}] timed out, status={}", props->remote_id_, + static_cast(status)); + props->connection_attempt_active_.store(false); + props->connection_established_ = false; + props->rejoin_ = false; + props->connection_status_.store(ConnectionStatus::Failed); + focused_remote_id_ = props->remote_id_; + show_connection_status_window_ = true; + rejoin_state_changed = true; + } + + if (rejoin_state_changed) { + need_to_rejoin_ = false; + for (const auto &[_, props] : remote_sessions_) { + if (props && props->rejoin_) { + need_to_rejoin_ = true; + break; + } + } + } +} + +int GuiRuntime::RequestSingleDevicePresence(const std::string &remote_id, + const char *password, + bool remember_password) { + if (!signal_connected_ || !peer_) { + return -1; + } + + { + std::lock_guard lock(pending_presence_probe_mutex_); + pending_presence_probe_ = true; + pending_presence_result_ready_ = false; + pending_presence_online_ = false; + pending_presence_probe_started_at_ = std::chrono::steady_clock::now(); + pending_presence_remote_id_ = remote_id; + pending_presence_password_ = password ? password : ""; + pending_presence_remember_password_ = remember_password; + } + + nlohmann::json j; + j["type"] = "recent_connections_presence"; + j["user_id"] = client_id_; + j["devices"] = nlohmann::json::array({remote_id}); + auto s = j.dump(); + + int ret = SendSignalMessage(peer_, s.data(), s.size()); + if (ret != 0) { + std::lock_guard lock(pending_presence_probe_mutex_); + pending_presence_probe_ = false; + pending_presence_result_ready_ = false; + pending_presence_online_ = false; + pending_presence_remote_id_.clear(); + pending_presence_password_.clear(); + pending_presence_remember_password_ = false; + } + + return ret; +} + +void GuiRuntime::CloseRemoteSession(std::shared_ptr props) { + SDL_FlushEvent(STREAM_REFRESH_EVENT); + + std::shared_ptr> frame_snapshot; + int video_width = 0; + int video_height = 0; + { + std::lock_guard lock(props->video_frame_mutex_); + frame_snapshot = props->front_frame_; + video_width = props->video_width_; + video_height = props->video_height_; + } + + if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 && + video_height > 0) { + std::vector buffer_copy(*frame_snapshot); + std::string remote_id = props->remote_id_; + std::string remote_host_name = props->remote_host_name_; + std::string password = + props->remember_password_ ? props->remote_password_ : ""; + + std::thread save_thread([buffer_copy, video_width, video_height, remote_id, + remote_host_name, password, + thumbnail = thumbnail_]() { + thumbnail->SaveToThumbnail((char *)buffer_copy.data(), video_width, + video_height, remote_id, remote_host_name, + password); + }); + + { + std::lock_guard lock(thumbnail_save_threads_mutex_); + thumbnail_save_threads_.emplace_back(std::move(save_thread)); + } + } + + if (props->peer_) { + LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_); + LeaveConnection(props->peer_, props->remote_id_.c_str()); + LOG_INFO("Destroy peer [{}]", props->local_id_); + DestroyPeer(&props->peer_); + } +} + +void GuiRuntime::CloseAllRemoteSessions() { + if (peer_) { + LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_); + LeaveConnection(peer_, client_id_); + is_client_mode_ = false; + devices_.StopMouseController(); + devices_.StopScreenCapturer(); + devices_.StopSpeakerCapturer(); + devices_.StopKeyboardCapturer(); + LOG_INFO("Destroy peer [{}]", client_id_); + DestroyPeer(&peer_); + } + + { + // std::shared_lock lock(remote_sessions_mutex_); + for (auto &it : remote_sessions_) { + auto props = it.second; + CloseRemoteSession(props); + } + } + + { + // std::unique_lock lock(remote_sessions_mutex_); + remote_sessions_.clear(); + } +} + +void GuiRuntime::WaitForThumbnailSaveTasks() { + std::vector threads_to_join; + + { + std::lock_guard lock(thumbnail_save_threads_mutex_); + threads_to_join.swap(thumbnail_save_threads_); + } + + if (threads_to_join.empty()) { + return; + } + + for (auto &thread : threads_to_join) { + if (thread.joinable()) { + thread.join(); + } + } +} + +void GuiRuntime::ResetRemoteSessionResources( + std::shared_ptr props) { + if (props->stream_texture_) { + SDL_DestroyTexture(props->stream_texture_); + props->stream_texture_ = nullptr; + } + + { + std::lock_guard lock(props->video_frame_mutex_); + props->front_frame_.reset(); + props->back_frame_.reset(); + props->video_width_ = 0; + props->video_height_ = 0; + props->video_size_ = 0; + props->render_rect_dirty_ = true; + props->stream_cleanup_pending_ = false; + } +} + +std::shared_ptr +GuiRuntime::FindRemoteSession( + const std::string &remote_id) { + if (remote_id.empty()) { + return nullptr; + } + + std::shared_lock lock(remote_sessions_mutex_); + auto it = remote_sessions_.find(remote_id); + if (it == remote_sessions_.end()) { + return nullptr; + } + return it->second; +} + + +int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password, + bool remember_password, bool bypass_presence_check) { + if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) { + int ret = + RequestSingleDevicePresence(remote_id, password, remember_password); + if (ret != 0) { + offline_warning_text_ = + localization::device_offline[localization_language_index_]; + show_offline_warning_window_ = true; + LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret); + } else { + LOG_INFO("Presence probe requested for [{}] before connect", remote_id); + } + return -1; + } + + LOG_INFO("Connect to [{}]", remote_id); + focused_remote_id_ = remote_id; + + // std::shared_lock shared_lock(remote_sessions_mutex_); + bool exists = + (remote_sessions_.find(remote_id) != remote_sessions_.end()); + // shared_lock.unlock(); + + if (!exists) { + PeerPtr *peer_to_init = nullptr; + std::string local_id; + + { + // std::unique_lock unique_lock(remote_sessions_mutex_); + if (remote_sessions_.find(remote_id) == remote_sessions_.end()) { + remote_sessions_[remote_id] = + std::make_shared(); + auto props = remote_sessions_[remote_id]; + props->local_id_ = "C-" + std::string(client_id_); + props->remote_id_ = remote_id; + memcpy(&props->params_, ¶ms_, sizeof(Params)); + props->params_.user_id = props->local_id_.c_str(); + props->peer_ = CreatePeer(&props->params_); + + props->control_window_width_ = title_bar_height_ * 10.0f; + props->control_window_height_ = title_bar_height_ * 1.3f; + props->control_window_min_width_ = title_bar_height_ * 0.65f; + props->control_window_min_height_ = title_bar_height_ * 1.3f; + props->control_window_max_width_ = title_bar_height_ * 10.0f; + props->control_window_max_height_ = title_bar_height_ * 7.0f; + + props->connection_status_.store(ConnectionStatus::Connecting); + show_connection_status_window_ = true; + + if (!props->peer_) { + LOG_INFO("Create peer [{}] instance failed", props->local_id_); + return -1; + } + + for (const auto &display_info : devices_.display_info_list()) { + AddVideoStream(props->peer_, display_info.name.c_str()); + } + AddAudioStream(props->peer_, props->audio_label_.c_str()); + AddDataStream(props->peer_, props->data_label_.c_str(), false); + AddDataStream(props->peer_, props->mouse_label_.c_str(), false); + AddDataStream(props->peer_, props->keyboard_label_.c_str(), true); + AddDataStream(props->peer_, props->control_data_label_.c_str(), true); + AddDataStream(props->peer_, props->file_label_.c_str(), true); + AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true); + AddDataStream(props->peer_, props->clipboard_label_.c_str(), true); + + props->connection_status_.store(ConnectionStatus::Connecting); + + peer_to_init = props->peer_; + local_id = props->local_id_; + } + } + + if (peer_to_init) { + LOG_INFO("[{}] Create peer instance successful", local_id); + Init(peer_to_init); + LOG_INFO("[{}] Peer init finish", local_id); + } + } + + int ret = -1; + // std::shared_lock read_lock(remote_sessions_mutex_); + auto props = remote_sessions_[remote_id]; + if (!props->connection_established_) { + props->connection_status_.store(ConnectionStatus::Connecting); + props->connection_attempt_active_.store(true); + props->connection_attempt_started_at_ = std::chrono::steady_clock::now(); + show_connection_status_window_ = true; + + props->remember_password_ = remember_password; + if (strcmp(password, "") != 0 && + strcmp(password, props->remote_password_) != 0) { + strncpy(props->remote_password_, password, + sizeof(props->remote_password_) - 1); + props->remote_password_[sizeof(props->remote_password_) - 1] = '\0'; + } + + std::string remote_id_with_pwd = remote_id + "@" + password; + if (props->peer_) { + ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str()); + if (0 == ret) { + props->rejoin_ = false; + } else { + props->rejoin_ = true; + need_to_rejoin_ = true; + } + } + } + // read_lock.unlock(); + + return 0; +} +} // namespace crossdesk diff --git a/src/gui/device_presence.h b/src/gui/runtime/device_presence_cache.h similarity index 51% rename from src/gui/device_presence.h rename to src/gui/runtime/device_presence_cache.h index e50bdf5..c1d3ee6 100644 --- a/src/gui/device_presence.h +++ b/src/gui/runtime/device_presence_cache.h @@ -4,23 +4,26 @@ * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */ -#ifndef _DEVICE_PRESENCE_H_ -#define _DEVICE_PRESENCE_H_ +#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_ +#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_ #include #include #include -class DevicePresence { - public: - void SetOnline(const std::string& device_id, bool online) { +namespace crossdesk { + +class DevicePresenceCache { +public: + void SetOnline(const std::string &device_id, bool online) { std::lock_guard lock(mutex_); cache_[device_id] = online; } - bool IsOnline(const std::string& device_id) const { + bool IsOnline(const std::string &device_id) const { std::lock_guard lock(mutex_); - return cache_.count(device_id) > 0 && cache_.at(device_id); + const auto it = cache_.find(device_id); + return it != cache_.end() && it->second; } void Clear() { @@ -28,9 +31,11 @@ class DevicePresence { cache_.clear(); } - private: +private: std::unordered_map cache_; mutable std::mutex mutex_; }; -#endif \ No newline at end of file +} // namespace crossdesk + +#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_ diff --git a/src/gui/runtime/gui_runtime.cpp b/src/gui/runtime/gui_runtime.cpp new file mode 100644 index 0000000..41f6996 --- /dev/null +++ b/src/gui/runtime/gui_runtime.cpp @@ -0,0 +1,246 @@ +#include "runtime/gui_runtime.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "localization.h" +#include "rd_log.h" + +namespace crossdesk { + +GuiRuntime::GuiRuntime() + : clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this), + keyboard_(*this), peer_events_(*this) {} + +GuiRuntime::~GuiRuntime() = default; + +int GuiRuntime::CreateConnectionPeer() { + params_.use_cfg_file = false; + + std::string signal_server_ip; + int signal_server_port; + int coturn_server_port; + + if (config_center_->IsSelfHosted()) { + signal_server_ip = config_center_->GetSignalServerHost(); + signal_server_port = config_center_->GetSignalServerPort(); + coturn_server_port = config_center_->GetCoturnServerPort(); + + std::string current_self_hosted_ip = config_center_->GetSignalServerHost(); + const bool use_cached_id = settings_.LoadCachedSelfHostedIdentity(); + if (!use_cached_id) { + LOG_INFO( + "secure_cache_v2.enc not found, will use empty id to get new id from " + "server"); + } + + if (use_cached_id && strlen(self_hosted_id_) > 0) { + memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_)); + strncpy(self_hosted_user_id_, self_hosted_id_, + sizeof(self_hosted_user_id_) - 1); + self_hosted_user_id_[sizeof(self_hosted_user_id_) - 1] = '\0'; + params_.user_id = self_hosted_user_id_; + } else { + memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_)); + params_.user_id = self_hosted_user_id_; + LOG_INFO( + "Using empty id for self-hosted server, server will assign new id"); + } + } else { + signal_server_ip = config_center_->GetDefaultServerHost(); + signal_server_port = config_center_->GetDefaultSignalServerPort(); + coturn_server_port = config_center_->GetDefaultCoturnServerPort(); + params_.user_id = client_id_with_password_; + } + + // self hosted server config + strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(), + sizeof(signal_server_ip_self_) - 1); + signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0'; + int signal_port = config_center_->GetSignalServerPort(); + if (signal_port > 0) { + strncpy(signal_server_port_self_, std::to_string(signal_port).c_str(), + sizeof(signal_server_port_self_) - 1); + signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0'; + } else { + signal_server_port_self_[0] = '\0'; + } + int coturn_port = config_center_->GetCoturnServerPort(); + if (coturn_port > 0) { + strncpy(coturn_server_port_self_, std::to_string(coturn_port).c_str(), + sizeof(coturn_server_port_self_) - 1); + coturn_server_port_self_[sizeof(coturn_server_port_self_) - 1] = '\0'; + } else { + coturn_server_port_self_[0] = '\0'; + } + + // peer config + strncpy((char *)params_.signal_server_ip, signal_server_ip.c_str(), + sizeof(params_.signal_server_ip) - 1); + params_.signal_server_ip[sizeof(params_.signal_server_ip) - 1] = '\0'; + params_.signal_server_port = signal_server_port; + strncpy((char *)params_.stun_server_ip, signal_server_ip.c_str(), + sizeof(params_.stun_server_ip) - 1); + params_.stun_server_ip[sizeof(params_.stun_server_ip) - 1] = '\0'; + params_.stun_server_port = coturn_server_port; + strncpy((char *)params_.turn_server_ip, signal_server_ip.c_str(), + sizeof(params_.turn_server_ip) - 1); + params_.turn_server_ip[sizeof(params_.turn_server_ip) - 1] = '\0'; + params_.turn_server_port = coturn_server_port; + strncpy((char *)params_.turn_server_username, "crossdesk", + sizeof(params_.turn_server_username) - 1); + params_.turn_server_username[sizeof(params_.turn_server_username) - 1] = '\0'; + strncpy((char *)params_.turn_server_password, "crossdeskpw", + sizeof(params_.turn_server_password) - 1); + params_.turn_server_password[sizeof(params_.turn_server_password) - 1] = '\0'; + + strncpy(params_.log_path, dll_log_path_.c_str(), + sizeof(params_.log_path) - 1); + params_.log_path[sizeof(params_.log_path) - 1] = '\0'; + params_.hardware_acceleration = config_center_->IsHardwareVideoCodec(); + params_.av1_encoding = config_center_->GetVideoEncodeFormat() == + ConfigCenter::VIDEO_ENCODE_FORMAT::AV1 + ? true + : false; + params_.turn_mode = static_cast(config_center_->GetTurnMode()); + params_.enable_srtp = config_center_->IsEnableSrtp(); + params_.video_quality = + static_cast(config_center_->GetVideoQuality()); + params_.on_receive_video_buffer = nullptr; + params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer; + params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer; + + params_.on_receive_video_frame = PeerEventHandler::OnReceiveVideoBuffer; + + params_.on_signal_status = PeerEventHandler::OnSignalStatus; + params_.on_signal_message = PeerEventHandler::OnSignalMessage; + params_.on_connection_status = PeerEventHandler::OnConnectionStatus; + params_.on_net_status_report = PeerEventHandler::OnNetStatusReport; + + params_.user_data = &peer_events_; + + peer_ = CreatePeer(¶ms_); + if (peer_) { + LOG_INFO("Create peer instance [{}] successful", client_id_); + Init(peer_); + LOG_INFO("Peer [{}] init finish", client_id_); + } else { + LOG_INFO("Create peer [{}] instance failed", client_id_); + } + + if (0 == devices_.InitializeScreenCapturer()) { + for (const auto &display_info : devices_.display_info_list()) { + AddVideoStream(peer_, display_info.name.c_str()); + } + + AddAudioStream(peer_, audio_label_.c_str()); + AddDataStream(peer_, data_label_.c_str(), false); + AddDataStream(peer_, mouse_label_.c_str(), false); + AddDataStream(peer_, keyboard_label_.c_str(), true); + AddDataStream(peer_, control_data_label_.c_str(), true); + AddDataStream(peer_, file_label_.c_str(), true); + AddDataStream(peer_, file_feedback_label_.c_str(), true); + AddDataStream(peer_, clipboard_label_.c_str(), true); + return 0; + } else { + return -1; + } +} + +void GuiRuntime::UpdateLabels() { + if (!label_inited_ || + localization_language_index_last_ != localization_language_index_) { + connect_button_label_ = + connect_button_pressed_ + ? localization::disconnect[localization_language_index_] + : localization::connect[localization_language_index_]; + label_inited_ = true; + localization_language_index_last_ = localization_language_index_; + } +} + + +void GuiRuntime::HandleRecentConnections() { + if (reload_recent_connections_ && main_renderer_) { + uint32_t now_time = SDL_GetTicks(); + if (now_time - recent_connection_image_save_time_ >= 50) { + int ret = thumbnail_->LoadThumbnail(main_renderer_, recent_connections_, + &recent_connection_image_width_, + &recent_connection_image_height_); + if (!ret) { + LOG_INFO("Load recent connection thumbnails"); + } + reload_recent_connections_ = false; + + recent_connection_ids_.clear(); + for (const auto &conn : recent_connections_) { + recent_connection_ids_.push_back(conn.first); + } + need_to_send_recent_connections_ = true; + } + } +} + + +void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) { + GuiRuntime *runtime = static_cast(userdata); + if (!runtime) { + return; + } + + if (1) { + std::shared_lock lock(runtime->remote_sessions_mutex_); + for (const auto &it : runtime->remote_sessions_) { + auto props = it.second; + if (props->connection_status_.load() == ConnectionStatus::Connected) { + if (props->peer_) { + SendAudioFrame(props->peer_, (const char *)stream, len, + runtime->audio_label_.c_str()); + } + } + } + + } else { + memcpy(runtime->audio_buffer_, stream, len); + runtime->audio_len_ = len; + SDL_Delay(10); + runtime->audio_buffer_fresh_ = true; + } +} + +void GuiRuntime::SdlCaptureAudioOut([[maybe_unused]] void *userdata, + [[maybe_unused]] Uint8 *stream, + [[maybe_unused]] int len) { + // GuiApplication *runtime = (GuiApplication *)userdata; + // for (auto it : runtime->remote_sessions_) { + // auto props = it.second; + // if (props->connection_status_ == SignalStatus::SignalConnected) { + // SendAudioFrame(props->peer_, (const char *)stream, len); + // } + // } + + // if (!runtime->audio_buffer_fresh_) { + // return; + // } + + // SDL_memset(stream, 0, len); + + // if (runtime->audio_len_ == 0) { + // return; + // } else { + // } + + // len = (len > runtime->audio_len_ ? runtime->audio_len_ : len); + // SDL_MixAudioFormat(stream, runtime->audio_buffer_, AUDIO_S16LSB, len, + // SDL_MIX_MAXVOLUME); + // runtime->audio_buffer_fresh_ = false; +} + +} // namespace crossdesk diff --git a/src/gui/runtime/gui_runtime.h b/src/gui/runtime/gui_runtime.h new file mode 100644 index 0000000..781d803 --- /dev/null +++ b/src/gui/runtime/gui_runtime.h @@ -0,0 +1,96 @@ +#ifndef CROSSDESK_GUI_RUNTIME_H_ +#define CROSSDESK_GUI_RUNTIME_H_ + +#include +#include + +#include "features/clipboard/clipboard_controller.h" +#include "features/devices/session_device_manager.h" +#include "features/file_transfer/file_transfer_manager.h" +#include "features/input/keyboard_controller.h" +#include "runtime/gui_state.h" +#include "runtime/peer_event_handler.h" +#include "features/settings/settings_manager.h" + +namespace crossdesk { + +// 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 { +protected: + using FileTransferState = gui_detail::FileTransferState; + using RemoteSession = gui_detail::RemoteSession; + + enum class RemoteUnlockState { + none, + service_unavailable, + lock_screen, + credential_ui, + secure_desktop, + }; + + GuiRuntime(); + ~GuiRuntime(); + + static void SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len); + static void SdlCaptureAudioOut(void *userdata, Uint8 *stream, int len); + + int CreateConnectionPeer(); + int ConnectTo(const std::string &remote_id, const char *password, + bool remember_password, bool bypass_presence_check = false); + int RequestSingleDevicePresence(const std::string &remote_id, + const char *password, bool remember_password); + + void UpdateLabels(); + void HandleRecentConnections(); + void HandleConnectionStatusChange(); + void HandlePendingPresenceProbe(); + void HandleConnectionTimeouts(); + void HandleWindowsServiceIntegration(); + + void CloseRemoteSession(std::shared_ptr props); + void CloseAllRemoteSessions(); + void ResetRemoteSessionResources( + std::shared_ptr props); + void WaitForThumbnailSaveTasks(); + std::shared_ptr + FindRemoteSession(const std::string &remote_id); + + void ResetRemoteServiceStatus(RemoteSession &props); + void ApplyRemoteServiceStatus(RemoteSession &props, + const ServiceStatus &status); + RemoteUnlockState + GetRemoteUnlockState(const RemoteSession &props) const; +#if _WIN32 + void ResetLocalWindowsServiceState(bool clear_pending_sas); +#endif + +#ifdef __APPLE__ + bool CheckScreenRecordingPermission(); + bool CheckAccessibilityPermission(); + void OpenScreenRecordingPreferences(); + void OpenAccessibilityPreferences(); + void RefreshMacPermissionStatus(bool force); + bool EnsureMacScreenRecordingPermission(); + bool EnsureMacAccessibilityPermission(); +#endif + + ClipboardController clipboard_; + SessionDeviceManager devices_; + FileTransferManager transfers_; + SettingsManager settings_; + KeyboardController keyboard_; + PeerEventHandler peer_events_; + +private: + friend class ClipboardController; + friend class SessionDeviceManager; + friend class FileTransferManager; + friend class SettingsManager; + friend class KeyboardController; + friend class PeerEventHandler; +}; + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_RUNTIME_H_ diff --git a/src/gui/runtime/gui_state.h b/src/gui/runtime/gui_state.h new file mode 100644 index 0000000..1d9d922 --- /dev/null +++ b/src/gui/runtime/gui_state.h @@ -0,0 +1,21 @@ +/* + * Aggregate state visible to GuiRuntime and GuiApplication. + * + * Definitions live beside their owners: SDL/window state in application and + * connection/session state in runtime. This header is intentionally only the + * composition point. + */ + +#ifndef CROSSDESK_GUI_STATE_H_ +#define CROSSDESK_GUI_STATE_H_ + +#include "application/application_state.h" +#include "runtime/runtime_state.h" + +namespace crossdesk::gui_detail { + +struct GuiState : ApplicationState, RuntimeState {}; + +} // namespace crossdesk::gui_detail + +#endif // CROSSDESK_GUI_STATE_H_ diff --git a/src/gui/runtime/mac_permission_runtime.mm b/src/gui/runtime/mac_permission_runtime.mm new file mode 100644 index 0000000..0d69dc6 --- /dev/null +++ b/src/gui/runtime/mac_permission_runtime.mm @@ -0,0 +1,113 @@ +#include "runtime/gui_runtime.h" + +#include "rd_log.h" + +#include +#include +#import +#include +#include + +namespace crossdesk { + +namespace { +constexpr uint32_t kPermissionRefreshIntervalVisibleMs = 500; + +void OpenPrivacyPreferences(const char *pane) { + if (pane == nullptr || pane[0] == '\0') { + return; + } + + std::string command = + "open \"x-apple.systempreferences:com.apple.preference.security?"; + command += pane; + command += "\""; + system(command.c_str()); +} +} // namespace + +bool GuiRuntime::CheckScreenRecordingPermission() { + // CGPreflightScreenCaptureAccess is available on macOS 10.15+ + if (@available(macOS 10.15, *)) { + bool granted = CGPreflightScreenCaptureAccess(); + return granted; + } + // for older macOS versions, assume permission is granted + return true; +} + +bool GuiRuntime::CheckAccessibilityPermission() { + NSDictionary *options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @NO}; + bool trusted = + AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); + return trusted; +} + +void GuiRuntime::OpenAccessibilityPreferences() { + if (!mac_accessibility_permission_requested_) { + NSDictionary *options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES}; + AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); + } else { + OpenPrivacyPreferences("Privacy_Accessibility"); + } +} + +void GuiRuntime::OpenScreenRecordingPreferences() { + if (@available(macOS 10.15, *)) { + if (!mac_screen_recording_permission_requested_) { + CGRequestScreenCaptureAccess(); + } else { + OpenPrivacyPreferences("Privacy_ScreenCapture"); + } + } else { + OpenPrivacyPreferences("Privacy_ScreenCapture"); + } +} + +void GuiRuntime::RefreshMacPermissionStatus(bool force) { + const uint32_t now = static_cast(SDL_GetTicks()); + if (!force && mac_permission_status_initialized_ && + now - mac_permission_last_check_tick_ < + kPermissionRefreshIntervalVisibleMs) { + return; + } + + const bool old_screen_recording_granted = + mac_screen_recording_permission_granted_; + const bool old_accessibility_granted = mac_accessibility_permission_granted_; + + mac_screen_recording_permission_granted_ = CheckScreenRecordingPermission(); + mac_accessibility_permission_granted_ = CheckAccessibilityPermission(); + mac_permission_last_check_tick_ = now; + mac_permission_status_initialized_ = true; + + if (old_screen_recording_granted != + mac_screen_recording_permission_granted_ || + old_accessibility_granted != mac_accessibility_permission_granted_) { + LOG_INFO("macOS permission status: screen_recording={}, accessibility={}", + mac_screen_recording_permission_granted_, + mac_accessibility_permission_granted_); + } +} + +bool GuiRuntime::EnsureMacScreenRecordingPermission() { + RefreshMacPermissionStatus(false); + if (mac_screen_recording_permission_granted_) { + return true; + } + + show_request_permission_window_ = true; + return false; +} + +bool GuiRuntime::EnsureMacAccessibilityPermission() { + RefreshMacPermissionStatus(false); + if (mac_accessibility_permission_granted_) { + return true; + } + + show_request_permission_window_ = true; + return false; +} + +} // namespace crossdesk diff --git a/src/gui/runtime/peer_data_callbacks.cpp b/src/gui/runtime/peer_data_callbacks.cpp new file mode 100644 index 0000000..65be3f4 --- /dev/null +++ b/src/gui/runtime/peer_data_callbacks.cpp @@ -0,0 +1,267 @@ +#include "runtime/peer_event_handler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "device_controller.h" +#include "file_transfer.h" +#include "localization.h" +#include "platform.h" +#include "rd_log.h" +#include "runtime/gui_runtime.h" +#include "runtime/remote_action_codec.h" + +#if _WIN32 +#include "interactive_state.h" +#include "service_host.h" +#endif + +namespace crossdesk { + +namespace { +#if _WIN32 +constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000; + +bool BuildAbsoluteMousePosition(const std::vector &displays, + int display_index, float normalized_x, + float normalized_y, int *absolute_x_out, + int *absolute_y_out) { + if (absolute_x_out == nullptr || absolute_y_out == nullptr || + display_index < 0 || display_index >= static_cast(displays.size())) { + return false; + } + + const DisplayInfo &display = displays[display_index]; + if (display.width <= 0 || display.height <= 0) { + return false; + } + + const float clamped_x = std::clamp(normalized_x, 0.0f, 1.0f); + const float clamped_y = std::clamp(normalized_y, 0.0f, 1.0f); + *absolute_x_out = static_cast(clamped_x * display.width) + display.left; + *absolute_y_out = static_cast(clamped_y * display.height) + display.top; + return true; +} + +void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *side, + const char *stage) { + if (last_tick == nullptr) { + return; + } + + const uint32_t now = static_cast(SDL_GetTicks()); + if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) { + return; + } + + *last_tick = now; + LOG_WARN("{} secure-desktop input blocked, stage={}, normal SendInput path " + "cannot drive the Windows password UI", + side != nullptr ? side : "unknown", stage != nullptr ? stage : ""); +} + +#endif + +} // namespace + +void PeerEventHandler::OnReceiveDataBuffer( + const char *data, size_t size, const char *user_id, size_t user_id_size, + const char *src_id, size_t src_id_size, void *user_data) { + auto *handler = static_cast(user_data); + GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; + if (!runtime) { + return; + } + + std::string source_id = std::string(src_id, src_id_size); + if (source_id == runtime->file_label_) { + std::string remote_user_id = std::string(user_id, user_id_size); + + static FileReceiver receiver; + // Update output directory from config + std::string configured_path = + runtime->config_center_->GetFileTransferSavePath(); + if (!configured_path.empty()) { + receiver.SetOutputDir(std::filesystem::u8path(configured_path)); + } else if (receiver.OutputDir().empty()) { + receiver = FileReceiver(); // re-init with default desktop path + } + receiver.SetOnSendAck([runtime, + remote_user_id](const FileTransferAck &ack) -> int { + bool is_server_sending = remote_user_id.rfind("C-", 0) != 0; + if (is_server_sending) { + auto props = + runtime->FindRemoteSession(remote_user_id); + if (props) { + PeerPtr *peer = props->peer_; + return SendReliableDataFrame( + peer, reinterpret_cast(&ack), + sizeof(FileTransferAck), runtime->file_feedback_label_.c_str()); + } + } + + return SendReliableDataFrame( + runtime->peer_, reinterpret_cast(&ack), + sizeof(FileTransferAck), runtime->file_feedback_label_.c_str()); + }); + + receiver.OnData(data, size); + return; + } else if (source_id == runtime->clipboard_label_) { + if (size > 0) { + std::string remote_user_id(user_id, user_id_size); + auto props = + runtime->FindRemoteSession(remote_user_id); + if (props && !props->enable_mouse_control_) { + return; + } + + runtime->clipboard_.QueueRemoteText(data, size); + } + return; + } else if (source_id == runtime->file_feedback_label_) { + runtime->transfers_.HandleAck(data, size); + return; + } + + std::string json_str(data, size); + RemoteAction remote_action{}; + if (!remote_action.from_json(json_str)) { + LOG_ERROR("Failed to parse RemoteAction JSON payload"); + return; + } + + std::string remote_id(user_id, user_id_size); + if (remote_action.type == ControlType::service_status) { + auto props_it = runtime->remote_sessions_.find(remote_id); + if (props_it != runtime->remote_sessions_.end()) { + runtime->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss); + } + return; + } + + if (remote_action.type == ControlType::service_command) { +#if _WIN32 + if (remote_action.c.flag == ServiceCommandFlag::send_sas) { + runtime->pending_windows_service_sas_.store(true, + std::memory_order_relaxed); + } else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) { + if (!LockWorkStation()) { + LOG_WARN("Remote lock workstation request failed, error={}", + GetLastError()); + } + } +#endif + return; + } + + if (remote_action.type == ControlType::host_infomation) { + bool is_client_mode = false; + std::shared_ptr props; + { + std::shared_lock lock(runtime->remote_sessions_mutex_); + auto props_it = runtime->remote_sessions_.find(remote_id); + if (props_it != runtime->remote_sessions_.end()) { + is_client_mode = true; + props = props_it->second; + } + } + + if (is_client_mode) { + // client mode + if (props && props->remote_host_name_.empty()) { + props->remote_host_name_ = std::string(remote_action.i.host_name, + remote_action.i.host_name_size); + LOG_INFO("Remote hostname: [{}]", props->remote_host_name_); + + for (int i = 0; i < remote_action.i.display_num; i++) { + props->display_info_list_.push_back( + DisplayInfo(remote_action.i.display_list[i], + remote_action.i.left[i], remote_action.i.top[i], + remote_action.i.right[i], remote_action.i.bottom[i])); + } + } + remote_action_codec::Free(remote_action); + } else { + // server mode + std::string host_name(remote_action.i.host_name, + remote_action.i.host_name_size); + { + std::unique_lock lock(runtime->connection_status_mutex_); + runtime->connection_host_names_[remote_id] = host_name; + } + LOG_INFO("Remote hostname: [{}]", host_name); + remote_action_codec::Free(remote_action); + } + } else { + // remote +#if _WIN32 + if (runtime->local_service_status_received_ && + IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) && + remote_action.type != ControlType::keyboard && + remote_action.type != ControlType::keyboard_state) { + if (remote_action.type == ControlType::mouse) { + int absolute_x = 0; + int absolute_y = 0; + if (!BuildAbsoluteMousePosition(runtime->devices_.display_info_list(), + runtime->selected_display_, + remote_action.m.x, remote_action.m.y, + &absolute_x, &absolute_y)) { + LOG_WARN("Secure desktop mouse injection skipped, invalid display " + "mapping: display_index={}, x={}, y={}", + runtime->selected_display_, remote_action.m.x, + remote_action.m.y); + return; + } + + const std::string response = SendCrossDeskSecureDesktopMouseInput( + absolute_x, absolute_y, remote_action.m.s, + static_cast(remote_action.m.flag), 1000); + auto json = nlohmann::json::parse(response, nullptr, false); + if (json.is_discarded() || !json.value("ok", false)) { + LogSecureDesktopInputBlocked( + &runtime->last_local_secure_input_block_log_tick_, "local", + runtime->local_interactive_stage_.c_str()); + LOG_WARN( + "Secure desktop mouse injection failed, x={}, y={}, wheel={}, " + "flag={}, response={}", + absolute_x, absolute_y, remote_action.m.s, + static_cast(remote_action.m.flag), response); + } + return; + } + } +#endif + if (remote_action.type == ControlType::mouse) { + runtime->devices_.SendMouseCommand(remote_action, + runtime->selected_display_); + } else if (remote_action.type == ControlType::audio_capture) { + if (remote_action.a && !runtime->start_speaker_capturer_) + runtime->devices_.StartSpeakerCapturer(); + else if (!remote_action.a && runtime->start_speaker_capturer_) + runtime->devices_.StopSpeakerCapturer(); + } else if (remote_action.type == ControlType::keyboard) { + runtime->keyboard_.ApplyRemoteEvent(remote_id, remote_action); + } else if (remote_action.type == ControlType::keyboard_state) { + runtime->keyboard_.ApplyRemoteState(remote_id, remote_action); + } else if (remote_action.type == ControlType::display_id) { + const int ret = runtime->devices_.SwitchDisplay(remote_action.d); + if (ret == 0) { + runtime->selected_display_ = remote_action.d; + } else { + LOG_WARN("Display switch skipped, invalid display_id={}", + remote_action.d); + } + } + } +} + +} // namespace crossdesk diff --git a/src/gui/runtime/peer_event_handler.cpp b/src/gui/runtime/peer_event_handler.cpp new file mode 100644 index 0000000..9e8d22b --- /dev/null +++ b/src/gui/runtime/peer_event_handler.cpp @@ -0,0 +1,524 @@ +#include "runtime/peer_event_handler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "device_controller.h" +#include "file_transfer.h" +#include "localization.h" +#include "platform.h" +#include "rd_log.h" +#include "runtime/gui_runtime.h" +#include "runtime/remote_action_codec.h" + +#if _WIN32 +#include "interactive_state.h" +#include "service_host.h" +#endif + +namespace crossdesk { + +PeerEventHandler::PeerEventHandler(GuiRuntime &owner) : owner_(owner) {} + +void PeerEventHandler::OnSignalMessage(const char *message, size_t size, + void *user_data) { + auto *handler = static_cast(user_data); + GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; + if (!runtime || !message || size == 0) { + return; + } + std::string s(message, size); + auto j = nlohmann::json::parse(s, nullptr, false); + if (j.is_discarded() || !j.contains("type") || !j["type"].is_string()) { + return; + } + std::string type = j["type"].get(); + if (type == "presence") { + if (j.contains("devices") && j["devices"].is_array()) { + for (auto &dev : j["devices"]) { + if (!dev.is_object()) { + continue; + } + if (!dev.contains("id") || !dev["id"].is_string()) { + continue; + } + if (!dev.contains("online") || !dev["online"].is_boolean()) { + continue; + } + std::string id = dev["id"].get(); + bool online = dev["online"].get(); + runtime->device_presence_cache_.SetOnline(id, online); + { + std::lock_guard lock( + runtime->pending_presence_probe_mutex_); + if (runtime->pending_presence_probe_ && + runtime->pending_presence_remote_id_ == id) { + runtime->pending_presence_result_ready_ = true; + runtime->pending_presence_online_ = online; + } + } + } + } + } else if (type == "presence_update") { + if (j.contains("id") && j["id"].is_string() && j.contains("online") && + j["online"].is_boolean()) { + std::string id = j["id"].get(); + bool online = j["online"].get(); + if (!id.empty()) { + runtime->device_presence_cache_.SetOnline(id, online); + { + std::lock_guard lock( + runtime->pending_presence_probe_mutex_); + if (runtime->pending_presence_probe_ && + runtime->pending_presence_remote_id_ == id) { + runtime->pending_presence_result_ready_ = true; + runtime->pending_presence_online_ = online; + } + } + } + } + } +} + + +void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id, + size_t user_id_size, void *user_data) { + auto *handler = static_cast(user_data); + GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; + if (!runtime) { + return; + } + + std::string client_id(user_id, user_id_size); + if (client_id == runtime->client_id_) { + runtime->signal_status_ = status; + if (SignalStatus::SignalConnecting == status) { + runtime->signal_connected_ = false; + } else if (SignalStatus::SignalConnected == status) { + runtime->signal_connected_ = true; + runtime->need_to_send_recent_connections_ = true; + LOG_INFO("[{}] connected to signal server", client_id); + } else if (SignalStatus::SignalFailed == status) { + runtime->signal_connected_ = false; + } else if (SignalStatus::SignalClosed == status) { + runtime->signal_connected_ = false; + } else if (SignalStatus::SignalReconnecting == status) { + runtime->signal_connected_ = false; + } else if (SignalStatus::SignalServerClosed == status) { + runtime->signal_connected_ = false; + } else if (SignalStatus::SignalTlsCertError == status) { + runtime->signal_connected_ = false; + } + } else { + if (client_id.rfind("C-", 0) != 0) { + return; + } + + std::string remote_id(client_id.begin() + 2, client_id.end()); + // std::shared_lock lock(runtime->remote_sessions_mutex_); + if (runtime->remote_sessions_.find(remote_id) == + runtime->remote_sessions_.end()) { + return; + } + auto props = runtime->remote_sessions_.find(remote_id)->second; + props->signal_status_ = status; + if (SignalStatus::SignalConnecting == status) { + props->signal_connected_ = false; + } else if (SignalStatus::SignalConnected == status) { + props->signal_connected_ = true; + LOG_INFO("[{}] connected to signal server", remote_id); + } else if (SignalStatus::SignalFailed == status) { + props->signal_connected_ = false; + } else if (SignalStatus::SignalClosed == status) { + props->signal_connected_ = false; + } else if (SignalStatus::SignalReconnecting == status) { + props->signal_connected_ = false; + } else if (SignalStatus::SignalServerClosed == status) { + props->signal_connected_ = false; + } else if (SignalStatus::SignalTlsCertError == status) { + props->signal_connected_ = false; + } + } +} + +void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, + const char *user_id, + const size_t user_id_size, + void *user_data) { + auto *handler = static_cast(user_data); + GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; + if (!runtime) + return; + + std::string remote_id(user_id, user_id_size); + std::shared_ptr props; + { + std::shared_lock lock(runtime->remote_sessions_mutex_); + auto it = runtime->remote_sessions_.find(remote_id); + if (it != runtime->remote_sessions_.end()) { + props = it->second; + } + } + + if (props) { + runtime->is_client_mode_ = true; + runtime->show_connection_status_window_ = true; + props->connection_status_.store(status); + if (status != ConnectionStatus::Connecting && + status != ConnectionStatus::Gathering) { + props->connection_attempt_active_.store(false); + } + + switch (status) { + case ConnectionStatus::Connected: { + runtime->ResetRemoteServiceStatus(*props); + { + RemoteAction remote_action; + remote_action.i.display_num = + runtime->devices_.display_info_list().size(); + remote_action.i.display_list = + (char **)malloc(remote_action.i.display_num * sizeof(char *)); + remote_action.i.left = + (int *)malloc(remote_action.i.display_num * sizeof(int)); + remote_action.i.top = + (int *)malloc(remote_action.i.display_num * sizeof(int)); + remote_action.i.right = + (int *)malloc(remote_action.i.display_num * sizeof(int)); + remote_action.i.bottom = + (int *)malloc(remote_action.i.display_num * sizeof(int)); + for (int i = 0; i < remote_action.i.display_num; i++) { + LOG_INFO("Local display [{}:{}]", i + 1, + runtime->devices_.display_info_list()[i].name); + remote_action.i.display_list[i] = (char *)malloc( + runtime->devices_.display_info_list()[i].name.length() + 1); + strncpy(remote_action.i.display_list[i], + runtime->devices_.display_info_list()[i].name.c_str(), + runtime->devices_.display_info_list()[i].name.length()); + remote_action.i.display_list + [i][runtime->devices_.display_info_list()[i].name.length()] = '\0'; + remote_action.i.left[i] = + runtime->devices_.display_info_list()[i].left; + remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top; + remote_action.i.right[i] = + runtime->devices_.display_info_list()[i].right; + remote_action.i.bottom[i] = + runtime->devices_.display_info_list()[i].bottom; + } + + std::string host_name = GetHostName(); + remote_action.type = ControlType::host_infomation; + memcpy(&remote_action.i.host_name, host_name.data(), host_name.size()); + remote_action.i.host_name[host_name.size()] = '\0'; + remote_action.i.host_name_size = host_name.size(); + + std::string msg = remote_action.to_json(); + int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(), + runtime->control_data_label_.c_str()); + remote_action_codec::Free(remote_action); + } + + if (!runtime->need_to_create_stream_window_ && + !runtime->remote_sessions_.empty()) { + runtime->need_to_create_stream_window_ = true; + } + props->connection_established_ = true; + props->stream_render_rect_ = { + 0, (int)runtime->title_bar_height_, (int)runtime->stream_window_width_, + (int)(runtime->stream_window_height_ - runtime->title_bar_height_)}; + props->stream_render_rect_f_ = { + 0.0f, runtime->title_bar_height_, runtime->stream_window_width_, + runtime->stream_window_height_ - runtime->title_bar_height_}; + runtime->start_keyboard_capturer_ = true; + break; + } + case ConnectionStatus::Disconnected: + case ConnectionStatus::Failed: + case ConnectionStatus::Closed: { + runtime->keyboard_.ReleaseRemotePressedKeys(remote_id, + "connection_closed"); + props->connection_established_ = false; + props->enable_mouse_control_ = false; + runtime->ResetRemoteServiceStatus(*props); + + { + std::lock_guard lock(props->video_frame_mutex_); + props->front_frame_.reset(); + props->back_frame_.reset(); + props->video_width_ = 0; + props->video_height_ = 0; + props->video_size_ = 0; + props->render_rect_dirty_ = true; + props->stream_cleanup_pending_ = true; + } + + SDL_Event event; + event.type = runtime->STREAM_REFRESH_EVENT; + event.user.data1 = props.get(); + SDL_PushEvent(&event); + + runtime->focus_on_stream_window_ = false; + + break; + } + case ConnectionStatus::IncorrectPassword: { + runtime->password_validating_ = false; + runtime->password_validating_time_++; + if (runtime->connect_button_pressed_) { + runtime->connect_button_pressed_ = false; + props->connection_established_ = false; + runtime->connect_button_label_ = + localization::connect[runtime->localization_language_index_]; + } + break; + } + case ConnectionStatus::NoSuchTransmissionId: + case ConnectionStatus::RemoteUnavailable: { + if (runtime->connect_button_pressed_) { + props->connection_established_ = false; + runtime->connect_button_label_ = + localization::connect[runtime->localization_language_index_]; + } + break; + } + default: + break; + } + } else { + runtime->is_client_mode_ = false; + runtime->show_connection_status_window_ = true; + { + std::unique_lock lock(runtime->connection_status_mutex_); + runtime->connection_status_[remote_id] = status; + } + + switch (status) { + case ConnectionStatus::Connected: { +#if _WIN32 + runtime->last_windows_service_status_tick_ = 0; +#endif + { + RemoteAction remote_action; + remote_action.i.display_num = + runtime->devices_.display_info_list().size(); + remote_action.i.display_list = + (char **)malloc(remote_action.i.display_num * sizeof(char *)); + remote_action.i.left = + (int *)malloc(remote_action.i.display_num * sizeof(int)); + remote_action.i.top = + (int *)malloc(remote_action.i.display_num * sizeof(int)); + remote_action.i.right = + (int *)malloc(remote_action.i.display_num * sizeof(int)); + remote_action.i.bottom = + (int *)malloc(remote_action.i.display_num * sizeof(int)); + for (int i = 0; i < remote_action.i.display_num; i++) { + LOG_INFO("Local display [{}:{}]", i + 1, + runtime->devices_.display_info_list()[i].name); + remote_action.i.display_list[i] = (char *)malloc( + runtime->devices_.display_info_list()[i].name.length() + 1); + strncpy(remote_action.i.display_list[i], + runtime->devices_.display_info_list()[i].name.c_str(), + runtime->devices_.display_info_list()[i].name.length()); + remote_action.i.display_list + [i][runtime->devices_.display_info_list()[i].name.length()] = '\0'; + remote_action.i.left[i] = + runtime->devices_.display_info_list()[i].left; + remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top; + remote_action.i.right[i] = + runtime->devices_.display_info_list()[i].right; + remote_action.i.bottom[i] = + runtime->devices_.display_info_list()[i].bottom; + } + + std::string host_name = GetHostName(); + remote_action.type = ControlType::host_infomation; + memcpy(&remote_action.i.host_name, host_name.data(), host_name.size()); + remote_action.i.host_name[host_name.size()] = '\0'; + remote_action.i.host_name_size = host_name.size(); + + std::string msg = remote_action.to_json(); + int ret = SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(), + runtime->control_data_label_.c_str()); + remote_action_codec::Free(remote_action); + } + + runtime->need_to_create_server_window_ = true; + runtime->is_server_mode_ = true; + runtime->start_screen_capturer_ = true; + runtime->start_speaker_capturer_ = true; + runtime->remote_client_id_ = remote_id; + runtime->start_mouse_controller_ = true; + { + std::shared_lock lock(runtime->connection_status_mutex_); + if (std::all_of(runtime->connection_status_.begin(), + runtime->connection_status_.end(), [](const auto &kv) { + return kv.first.find("web") != std::string::npos; + })) { + runtime->show_cursor_ = true; + } + } + + break; + } + case ConnectionStatus::Disconnected: + case ConnectionStatus::Failed: + case ConnectionStatus::Closed: { + runtime->keyboard_.ReleaseRemotePressedKeys(remote_id, + "connection_closed"); + bool all_disconnected = false; + { + std::shared_lock lock(runtime->connection_status_mutex_); + all_disconnected = + std::all_of(runtime->connection_status_.begin(), + runtime->connection_status_.end(), [](const auto &kv) { + return kv.second == ConnectionStatus::Closed || + kv.second == ConnectionStatus::Failed || + kv.second == ConnectionStatus::Disconnected; + }); + } + if (all_disconnected) { + runtime->need_to_destroy_server_window_ = true; + runtime->is_server_mode_ = false; +#if defined(__linux__) && !defined(__APPLE__) + if (IsWaylandSession()) { + // Keep Wayland capture session warm to avoid black screen on + // subsequent reconnects. + runtime->start_screen_capturer_ = true; + LOG_INFO("Keeping Wayland screen capturer running after " + "disconnect to preserve reconnect stability"); + } else { + runtime->start_screen_capturer_ = false; + } +#else + runtime->start_screen_capturer_ = false; +#endif + runtime->start_speaker_capturer_ = false; + runtime->start_mouse_controller_ = false; + runtime->start_keyboard_capturer_ = false; + runtime->remote_client_id_ = ""; + if (props) + props->connection_established_ = false; + if (runtime->audio_capture_) { + runtime->devices_.StopSpeakerCapturer(); + runtime->audio_capture_ = false; + } + + { + std::unique_lock lock(runtime->connection_status_mutex_); + runtime->connection_status_.erase(remote_id); + runtime->connection_host_names_.erase(remote_id); + } + runtime->devices_.ResetToInitialDisplay(); + } + + { + std::shared_lock lock(runtime->connection_status_mutex_); + if (std::all_of(runtime->connection_status_.begin(), + runtime->connection_status_.end(), [](const auto &kv) { + return kv.first.find("web") == std::string::npos; + })) { + runtime->show_cursor_ = false; + } + } + + break; + } + default: + break; + } + } +} + +void PeerEventHandler::OnNetStatusReport( + const char *client_id, size_t client_id_size, TraversalMode mode, + const XNetTrafficStats *net_traffic_stats, const char *user_id, + const size_t user_id_size, void *user_data) { + auto *handler = static_cast(user_data); + GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; + if (!runtime) { + return; + } + + if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) { + std::string id, password; + const char *at_pos = strchr(client_id, '@'); + if (at_pos == nullptr) { + id = client_id; + password.clear(); + } else { + id.assign(client_id, at_pos - client_id); + password = at_pos + 1; + } + + bool is_self_hosted = runtime->config_center_->IsSelfHosted(); + + if (is_self_hosted) { + memset(&runtime->client_id_, 0, sizeof(runtime->client_id_)); + strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1); + runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0'; + + memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_)); + strncpy(runtime->password_saved_, password.c_str(), + sizeof(runtime->password_saved_) - 1); + runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0'; + + memset(&runtime->self_hosted_id_, 0, sizeof(runtime->self_hosted_id_)); + strncpy(runtime->self_hosted_id_, client_id, + sizeof(runtime->self_hosted_id_) - 1); + runtime->self_hosted_id_[sizeof(runtime->self_hosted_id_) - 1] = '\0'; + + LOG_INFO("Use self-hosted client id [{}] and save to cache file", id); + + runtime->settings_.PersistSelfHostedIdentity(client_id); + } else { + memset(&runtime->client_id_, 0, sizeof(runtime->client_id_)); + strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1); + runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0'; + + memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_)); + strncpy(runtime->password_saved_, password.c_str(), + sizeof(runtime->password_saved_) - 1); + runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0'; + + memset(&runtime->client_id_with_password_, 0, + sizeof(runtime->client_id_with_password_)); + strncpy(runtime->client_id_with_password_, client_id, + sizeof(runtime->client_id_with_password_) - 1); + runtime + ->client_id_with_password_[sizeof(runtime->client_id_with_password_) - + 1] = '\0'; + + LOG_INFO("Use client id [{}] and save id into cache file", id); + runtime->settings_.Save(); + } + } + + std::string remote_id(user_id, user_id_size); + // std::shared_lock lock(runtime->remote_sessions_mutex_); + if (runtime->remote_sessions_.find(remote_id) == + runtime->remote_sessions_.end()) { + return; + } + auto props = runtime->remote_sessions_.find(remote_id)->second; + if (props->traversal_mode_ != mode) { + props->traversal_mode_ = mode; + LOG_INFO("Net mode: [{}]", int(props->traversal_mode_)); + } + + if (!net_traffic_stats) { + return; + } + + // only display client side net status if connected to itself + if (!(runtime->peer_reserved_ && !strstr(client_id, "C-"))) { + props->net_traffic_stats_ = *net_traffic_stats; + } +} +} // namespace crossdesk diff --git a/src/gui/runtime/peer_event_handler.h b/src/gui/runtime/peer_event_handler.h new file mode 100644 index 0000000..1baece2 --- /dev/null +++ b/src/gui/runtime/peer_event_handler.h @@ -0,0 +1,47 @@ +#ifndef CROSSDESK_GUI_PEER_EVENT_HANDLER_H_ +#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_ + +#include + +#include "minirtc.h" + +namespace crossdesk { + +class GuiRuntime; + +// Adapts MiniRTC C callbacks to the GUI runtime. +class PeerEventHandler { +public: + explicit PeerEventHandler(GuiRuntime &owner); + + static void OnReceiveVideoBuffer(const XVideoFrame *video_frame, + const char *user_id, size_t user_id_size, + const char *src_id, size_t src_id_size, + void *user_data); + static void OnReceiveAudioBuffer(const char *data, size_t size, + const char *user_id, size_t user_id_size, + const char *src_id, size_t src_id_size, + void *user_data); + static void OnReceiveDataBuffer(const char *data, size_t size, + const char *user_id, size_t user_id_size, + const char *src_id, size_t src_id_size, + void *user_data); + static void OnSignalStatus(SignalStatus status, const char *user_id, + size_t user_id_size, void *user_data); + static void OnSignalMessage(const char *message, size_t size, + void *user_data); + static void OnConnectionStatus(ConnectionStatus status, const char *user_id, + size_t user_id_size, void *user_data); + static void OnNetStatusReport(const char *client_id, size_t client_id_size, + TraversalMode mode, + const XNetTrafficStats *net_traffic_stats, + const char *user_id, size_t user_id_size, + void *user_data); + +private: + GuiRuntime &owner_; +}; + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_PEER_EVENT_HANDLER_H_ diff --git a/src/gui/runtime/peer_media_callbacks.cpp b/src/gui/runtime/peer_media_callbacks.cpp new file mode 100644 index 0000000..71a7b49 --- /dev/null +++ b/src/gui/runtime/peer_media_callbacks.cpp @@ -0,0 +1,111 @@ +#include "runtime/peer_event_handler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "device_controller.h" +#include "file_transfer.h" +#include "localization.h" +#include "platform.h" +#include "rd_log.h" +#include "runtime/gui_runtime.h" +#include "runtime/remote_action_codec.h" + +#if _WIN32 +#include "interactive_state.h" +#include "service_host.h" +#endif + +namespace crossdesk { + +void PeerEventHandler::OnReceiveVideoBuffer( + const XVideoFrame *video_frame, const char *user_id, size_t user_id_size, + const char *src_id, size_t src_id_size, void *user_data) { + auto *handler = static_cast(user_data); + GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; + if (!runtime) { + return; + } + + std::string remote_id(user_id, user_id_size); + // std::shared_lock lock(runtime->remote_sessions_mutex_); + if (runtime->remote_sessions_.find(remote_id) == + runtime->remote_sessions_.end()) { + return; + } + GuiRuntime::RemoteSession *props = + runtime->remote_sessions_.find(remote_id)->second.get(); + + if (props->connection_established_) { + { + std::lock_guard lock(props->video_frame_mutex_); + + if (!props->back_frame_) { + props->back_frame_ = + std::make_shared>(video_frame->size); + } + if (props->back_frame_->size() != video_frame->size) { + props->back_frame_->resize(video_frame->size); + } + + std::memcpy(props->back_frame_->data(), video_frame->data, + video_frame->size); + + 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; + + props->front_frame_.swap(props->back_frame_); + } + + SDL_Event event; + event.type = runtime->STREAM_REFRESH_EVENT; + event.user.data1 = props; + SDL_PushEvent(&event); + props->streaming_ = true; + + if (props->net_traffic_stats_button_pressed_) { + props->frame_count_++; + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast( + now - props->last_time_) + .count(); + + if (elapsed >= 1000) { + props->fps_ = props->frame_count_ * 1000 / elapsed; + props->frame_count_ = 0; + props->last_time_ = now; + } + } + } +} + +void PeerEventHandler::OnReceiveAudioBuffer( + const char *data, size_t size, const char *user_id, size_t user_id_size, + const char *src_id, size_t src_id_size, void *user_data) { + auto *handler = static_cast(user_data); + GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; + if (!runtime) { + return; + } + + runtime->audio_buffer_fresh_ = true; + + runtime->devices_.PushAudio(data, size); +} + +} // namespace crossdesk diff --git a/src/gui/runtime/remote_action_codec.cpp b/src/gui/runtime/remote_action_codec.cpp new file mode 100644 index 0000000..1f70ef9 --- /dev/null +++ b/src/gui/runtime/remote_action_codec.cpp @@ -0,0 +1,117 @@ +#include "runtime/remote_action_codec.h" + +#include +#include + +namespace crossdesk::remote_action_codec { + +std::vector Serialize(const RemoteAction &action) { + std::vector buffer; + buffer.push_back(static_cast(action.type)); + + auto insert_bytes = [&](const void *ptr, size_t len) { + buffer.insert(buffer.end(), static_cast(ptr), + static_cast(ptr) + len); + }; + + if (action.type == ControlType::host_infomation) { + insert_bytes(&action.i.host_name_size, sizeof(size_t)); + insert_bytes(action.i.host_name, action.i.host_name_size); + + size_t num = action.i.display_num; + insert_bytes(&num, sizeof(size_t)); + + for (size_t i = 0; i < num; ++i) { + const size_t len = std::strlen(action.i.display_list[i]); + insert_bytes(&len, sizeof(size_t)); + insert_bytes(action.i.display_list[i], len); + } + + insert_bytes(action.i.left, sizeof(int) * num); + insert_bytes(action.i.top, sizeof(int) * num); + insert_bytes(action.i.right, sizeof(int) * num); + insert_bytes(action.i.bottom, sizeof(int) * num); + } + + return buffer; +} + +bool Deserialize(const char *data, size_t size, RemoteAction &out) { + size_t offset = 0; + auto read = [&](void *dst, size_t len) -> bool { + if (offset + len > size) { + return false; + } + std::memcpy(dst, data + offset, len); + offset += len; + return true; + }; + + if (size < 1) { + return false; + } + out.type = static_cast(data[offset++]); + + if (out.type == ControlType::host_infomation) { + size_t name_len; + if (!read(&name_len, sizeof(size_t)) || + name_len >= sizeof(out.i.host_name)) { + return false; + } + if (!read(out.i.host_name, name_len)) { + return false; + } + out.i.host_name[name_len] = '\0'; + out.i.host_name_size = name_len; + + size_t num; + if (!read(&num, sizeof(size_t))) { + return false; + } + out.i.display_num = num; + + out.i.display_list = + static_cast(std::malloc(num * sizeof(char *))); + for (size_t i = 0; i < num; ++i) { + size_t len; + if (!read(&len, sizeof(size_t)) || offset + len > size) { + return false; + } + out.i.display_list[i] = static_cast(std::malloc(len + 1)); + std::memcpy(out.i.display_list[i], data + offset, len); + out.i.display_list[i][len] = '\0'; + offset += len; + } + + auto alloc_int_array = [&](int *&array) { + array = static_cast(std::malloc(num * sizeof(int))); + return read(array, num * sizeof(int)); + }; + + return alloc_int_array(out.i.left) && alloc_int_array(out.i.top) && + alloc_int_array(out.i.right) && alloc_int_array(out.i.bottom); + } + + return true; +} + +void Free(RemoteAction &action) { + if (action.type != ControlType::host_infomation) { + return; + } + + for (size_t i = 0; i < action.i.display_num; ++i) { + std::free(action.i.display_list[i]); + } + std::free(action.i.display_list); + std::free(action.i.left); + std::free(action.i.top); + std::free(action.i.right); + std::free(action.i.bottom); + + action.i.display_list = nullptr; + action.i.left = action.i.top = action.i.right = action.i.bottom = nullptr; + action.i.display_num = 0; +} + +} // namespace crossdesk::remote_action_codec diff --git a/src/gui/runtime/remote_action_codec.h b/src/gui/runtime/remote_action_codec.h new file mode 100644 index 0000000..62a6094 --- /dev/null +++ b/src/gui/runtime/remote_action_codec.h @@ -0,0 +1,17 @@ +#ifndef CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_ +#define CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_ + +#include +#include + +#include "device_controller.h" + +namespace crossdesk::remote_action_codec { + +std::vector Serialize(const RemoteAction &action); +bool Deserialize(const char *data, size_t size, RemoteAction &out); +void Free(RemoteAction &action); + +} // namespace crossdesk::remote_action_codec + +#endif // CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_ diff --git a/src/gui/runtime/remote_session.h b/src/gui/runtime/remote_session.h new file mode 100644 index 0000000..296217d --- /dev/null +++ b/src/gui/runtime/remote_session.h @@ -0,0 +1,175 @@ +#ifndef CROSSDESK_GUI_REMOTE_SESSION_H_ +#define CROSSDESK_GUI_REMOTE_SESSION_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "display_info.h" +#include "imgui.h" +#include "minirtc.h" + +namespace crossdesk::gui_detail { + +struct FileTransferState { + std::atomic file_sending_ = false; + std::atomic file_sent_bytes_ = 0; + std::atomic file_total_bytes_ = 0; + std::atomic file_send_rate_bps_ = 0; + std::mutex file_transfer_mutex_; + std::chrono::steady_clock::time_point file_send_start_time_; + std::chrono::steady_clock::time_point file_send_last_update_time_; + uint64_t file_send_last_bytes_ = 0; + bool file_transfer_window_visible_ = false; + bool file_transfer_window_hovered_ = false; + std::atomic current_file_id_{0}; + + struct QueuedFile { + std::filesystem::path file_path; + std::string file_label; + std::string remote_id; + }; + std::queue file_send_queue_; + std::mutex file_queue_mutex_; + + enum class FileTransferStatus { Queued, Sending, Completed, Failed }; + + struct FileTransferInfo { + std::string file_name; + std::filesystem::path file_path; + uint64_t file_size = 0; + FileTransferStatus status = FileTransferStatus::Queued; + uint64_t sent_bytes = 0; + uint32_t file_id = 0; + uint32_t rate_bps = 0; + }; + std::vector file_transfer_list_; + std::mutex file_transfer_list_mutex_; +}; + +// Runtime state for one connected or connecting remote endpoint. It groups the +// connection, media, input, window and transfer data that share one lifetime. +struct RemoteSession { + Params params_; + PeerPtr *peer_ = nullptr; + std::string audio_label_ = "control_audio"; + std::string data_label_ = "data"; + std::string mouse_label_ = "mouse"; + std::string keyboard_label_ = "keyboard"; + std::string file_label_ = "file"; + std::string control_data_label_ = "control_data"; + std::string file_feedback_label_ = "file_feedback"; + std::string clipboard_label_ = "clipboard"; + std::string local_id_; + std::string remote_id_; + bool exit_ = false; + bool signal_connected_ = false; + SignalStatus signal_status_ = SignalStatus::SignalClosed; + bool connection_established_ = false; + bool rejoin_ = false; + std::atomic connection_attempt_active_ = false; + std::chrono::steady_clock::time_point connection_attempt_started_at_; + bool net_traffic_stats_button_pressed_ = false; + bool enable_mouse_control_ = true; + bool mouse_controller_is_started_ = false; + bool audio_capture_button_pressed_ = true; + bool control_mouse_ = true; + bool streaming_ = false; + bool is_control_bar_in_left_ = true; + bool control_bar_hovered_ = false; + bool display_selectable_hovered_ = false; + bool shortcut_selectable_hovered_ = false; + bool control_bar_expand_ = true; + bool reset_control_bar_pos_ = false; + bool control_window_width_is_changing_ = false; + bool control_window_height_is_changing_ = false; + bool p2p_mode_ = true; + bool remember_password_ = false; + char remote_password_[7] = ""; + float sub_stream_window_width_ = 1280; + float sub_stream_window_height_ = 720; + float control_window_min_width_ = 20; + float control_window_max_width_ = 300; + float control_window_min_height_ = 38; + float control_window_max_height_ = 180; + float control_window_width_ = 300; + float control_window_height_ = 38; + float control_bar_pos_x_ = 0; + float control_bar_pos_y_ = 30; + float mouse_diff_control_bar_pos_x_ = 0; + float mouse_diff_control_bar_pos_y_ = 0; + double control_bar_button_pressed_time_ = 0; + double net_traffic_stats_button_pressed_time_ = 0; + + // Written by the decode callback thread and consumed by the SDL thread. + std::mutex video_frame_mutex_; + std::shared_ptr> front_frame_; + std::shared_ptr> back_frame_; + bool render_rect_dirty_ = false; + bool stream_cleanup_pending_ = false; + float mouse_pos_x_ = 0; + float mouse_pos_y_ = 0; + float mouse_pos_x_last_ = 0; + float mouse_pos_y_last_ = 0; + int texture_width_ = 1280; + int texture_height_ = 720; + int video_width_ = 0; + int video_height_ = 0; + int video_width_last_ = 0; + int video_height_last_ = 0; + int selected_display_ = 0; + size_t video_size_ = 0; + bool tab_selected_ = false; + bool tab_opened_ = true; + std::optional pos_x_before_docked_; + std::optional pos_y_before_docked_; + float render_window_x_ = 0; + float render_window_y_ = 0; + float render_window_width_ = 0; + float render_window_height_ = 0; + std::string fullscreen_button_label_ = "Fullscreen"; + std::string net_traffic_stats_button_label_ = "Show Net Traffic Stats"; + std::string mouse_control_button_label_ = "Mouse Control"; + std::string audio_capture_button_label_ = "Audio Capture"; + std::string remote_host_name_; + bool remote_service_status_received_ = false; + bool remote_service_available_ = false; + std::string remote_interactive_stage_; + std::vector display_info_list_; + SDL_Texture *stream_texture_ = nullptr; + uint8_t *argb_buffer_ = nullptr; + int argb_buffer_size_ = 0; + SDL_FRect stream_render_rect_f_ = {0.0f, 0.0f, 0.0f, 0.0f}; + SDL_Rect stream_render_rect_{}; + SDL_Rect stream_render_rect_last_{}; + ImVec2 control_window_pos_{}; + + // Shared by minirtc callbacks, SDL rendering and SDL audio callbacks. + std::atomic connection_status_ = ConnectionStatus::Closed; + TraversalMode traversal_mode_ = TraversalMode::UnknownMode; + int fps_ = 0; + int frame_count_ = 0; + std::chrono::steady_clock::time_point last_time_; + XNetTrafficStats net_traffic_stats_{}; + + using QueuedFile = FileTransferState::QueuedFile; + using FileTransferStatus = FileTransferState::FileTransferStatus; + using FileTransferInfo = FileTransferState::FileTransferInfo; + FileTransferState file_transfer_; +}; + +using RemoteSessionPtr = std::shared_ptr; + +} // namespace crossdesk::gui_detail + +#endif // CROSSDESK_GUI_REMOTE_SESSION_H_ diff --git a/src/gui/runtime/runtime_state.h b/src/gui/runtime/runtime_state.h new file mode 100644 index 0000000..dc93103 --- /dev/null +++ b/src/gui/runtime/runtime_state.h @@ -0,0 +1,186 @@ +#ifndef CROSSDESK_GUI_RUNTIME_STATE_H_ +#define CROSSDESK_GUI_RUNTIME_STATE_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config_center.h" +#include "path_manager.h" +#include "runtime/device_presence_cache.h" +#include "runtime/remote_session.h" +#include "thumbnail.h" + +namespace crossdesk::gui_detail { + +struct InfrastructureState { + std::unique_ptr config_center_; + ConfigCenter::LANGUAGE localization_language_ = + ConfigCenter::LANGUAGE::CHINESE; + std::unique_ptr path_manager_; + std::string exec_log_path_; + std::string dll_log_path_; + std::string cache_path_; + int localization_language_index_ = -1; + int localization_language_index_last_ = -1; + bool modules_inited_ = false; + unsigned char aes128_key_[16]{}; + unsigned char aes128_iv_[16]{}; +}; + +struct RecentConnectionsState { + std::shared_ptr thumbnail_; + std::vector> + recent_connections_; + std::vector recent_connection_ids_; + int recent_connection_image_width_ = 160; + int recent_connection_image_height_ = 90; + uint32_t recent_connection_image_save_time_ = 0; + DevicePresenceCache device_presence_cache_; + bool need_to_send_recent_connections_ = true; +}; + +struct PeerState { + SignalStatus signal_status_ = SignalStatus::SignalClosed; + std::string signal_status_str_; + bool signal_connected_ = false; + PeerPtr *peer_ = nullptr; + PeerPtr *peer_reserved_ = nullptr; + std::string video_primary_label_ = "primary_display"; + std::string video_secondary_label_ = "secondary_display"; + std::string audio_label_ = "audio"; + std::string data_label_ = "data"; + std::string mouse_label_ = "mouse"; + std::string keyboard_label_ = "keyboard"; + std::string info_label_ = "info"; + std::string control_data_label_ = "control_data"; + std::string file_label_ = "file"; + std::string file_feedback_label_ = "file_feedback"; + std::string clipboard_label_ = "clipboard"; + Params params_; +}; + +// OS-specific service and permission state is kept separate from transport +// state so platform code does not appear to be part of the peer protocol. +struct PlatformIntegrationState { +#if _WIN32 + std::atomic pending_windows_service_sas_{false}; + bool local_service_status_received_ = false; + bool local_service_available_ = false; + std::string local_interactive_stage_; + uint32_t last_local_secure_input_block_log_tick_ = 0; + uint32_t last_windows_service_status_tick_ = 0; + uint32_t optimistic_windows_secure_desktop_until_tick_ = 0; +#if CROSSDESK_PORTABLE + enum class PortableServiceInstallState { + idle, + installing, + succeeded, + failed, + }; + bool portable_service_prompt_checked_ = false; + bool show_portable_service_install_window_ = false; + bool show_portable_service_prompt_suppressed_window_ = false; + bool portable_service_do_not_remind_ = false; + bool portable_service_prompt_suppressed_ = false; + std::atomic portable_service_install_state_{ + PortableServiceInstallState::idle}; + std::thread portable_service_install_thread_; +#endif +#endif + +#ifdef __APPLE__ + bool show_request_permission_window_ = true; + bool mac_permission_status_initialized_ = false; + uint32_t mac_permission_last_check_tick_ = 0; + bool mac_screen_recording_permission_granted_ = false; + bool mac_accessibility_permission_granted_ = false; + bool mac_screen_recording_permission_requested_ = false; + bool mac_accessibility_permission_requested_ = false; +#endif +}; + +struct UserSettingsState { + char client_id_[10] = ""; + char client_id_display_[12] = ""; + char client_id_with_password_[17] = ""; + char password_saved_[7] = ""; + char self_hosted_id_[17] = ""; + char self_hosted_user_id_[17] = ""; + int language_button_value_ = 0; + int video_quality_button_value_ = 2; + int video_frame_rate_button_value_ = 1; + int video_encode_format_button_value_ = 0; + bool enable_hardware_video_codec_ = true; + bool enable_turn_ = true; + bool enable_srtp_ = false; + char signal_server_ip_[256] = "api.crossdesk.cn"; + char signal_server_port_[6] = "9099"; + char coturn_server_port_[6] = "3478"; + bool enable_self_hosted_ = false; + int language_button_value_last_ = 0; + int video_quality_button_value_last_ = 0; + int video_frame_rate_button_value_last_ = 0; + int video_encode_format_button_value_last_ = 0; + bool enable_hardware_video_codec_last_ = false; + bool enable_turn_last_ = true; + bool enable_srtp_last_ = false; + bool enable_self_hosted_last_ = false; + bool enable_autostart_ = false; + bool enable_autostart_last_ = false; + bool enable_daemon_ = false; + bool enable_daemon_last_ = false; + bool enable_minimize_to_tray_ = false; + bool enable_minimize_to_tray_last_ = false; + char file_transfer_save_path_buf_[512] = ""; + std::string file_transfer_save_path_last_; + char signal_server_ip_self_[256] = ""; + char signal_server_port_self_[6] = ""; + char coturn_server_port_self_[6] = ""; + bool settings_window_pos_reset_ = true; + bool self_hosted_server_config_window_pos_reset_ = true; + std::string selected_current_file_path_; + bool show_file_browser_ = true; +}; + +struct ConnectionState { + using RemoteSessionMap = + std::unordered_map; + RemoteSessionMap remote_sessions_; + std::shared_mutex remote_sessions_mutex_; + std::vector thumbnail_save_threads_; + std::mutex thumbnail_save_threads_mutex_; + + std::shared_mutex connection_status_mutex_; + std::unordered_map connection_status_; + std::unordered_map connection_host_names_; + std::string selected_server_remote_id_; + std::string selected_server_remote_hostname_; + std::mutex pending_presence_probe_mutex_; + bool pending_presence_probe_ = false; + bool pending_presence_result_ready_ = false; + bool pending_presence_online_ = false; + std::chrono::steady_clock::time_point pending_presence_probe_started_at_; + std::string pending_presence_remote_id_; + std::string pending_presence_password_; + bool pending_presence_remember_password_ = false; +}; + +struct RuntimeState : InfrastructureState, + RecentConnectionsState, + PeerState, + PlatformIntegrationState, + UserSettingsState, + ConnectionState {}; + +} // namespace crossdesk::gui_detail + +#endif // CROSSDESK_GUI_RUNTIME_STATE_H_ diff --git a/src/gui/runtime/windows_service_runtime.cpp b/src/gui/runtime/windows_service_runtime.cpp new file mode 100644 index 0000000..84a7384 --- /dev/null +++ b/src/gui/runtime/windows_service_runtime.cpp @@ -0,0 +1,262 @@ +#include "runtime/gui_runtime.h" + +#include +#include +#include +#include + +#include "rd_log.h" + +#if _WIN32 +#include "interactive_state.h" +#include "service_host.h" +#endif + +namespace crossdesk { +namespace { +#if _WIN32 +struct WindowsServiceInteractiveStatus { + bool available = false; + bool sas_secure_desktop_grace_active = false; + unsigned int error_code = 0; + std::string interactive_stage; + std::string error; +}; + +constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000; +constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000; +constexpr DWORD kWindowsServiceQueryTimeoutMs = 500; +constexpr DWORD kWindowsServiceSasTimeoutMs = 500; + +bool IsTransientWindowsServiceStatusError(const std::string &error) { + return error == "pipe_unavailable" || error == "pipe_connect_failed" || + error == "pipe_read_failed"; +} + +RemoteAction +BuildWindowsServiceStatusAction(const WindowsServiceInteractiveStatus &status) { + RemoteAction action{}; + action.type = ControlType::service_status; + action.ss.available = status.available; + std::strncpy(action.ss.interactive_stage, status.interactive_stage.c_str(), + sizeof(action.ss.interactive_stage) - 1); + action.ss.interactive_stage[sizeof(action.ss.interactive_stage) - 1] = '\0'; + return action; +} + +bool QueryWindowsServiceInteractiveStatus( + WindowsServiceInteractiveStatus *status) { + if (status == nullptr) { + return false; + } + + *status = WindowsServiceInteractiveStatus{}; + const std::string response = + QueryCrossDeskService("status", kWindowsServiceQueryTimeoutMs); + auto json = nlohmann::json::parse(response, nullptr, false); + if (json.is_discarded() || !json.is_object()) { + status->error = "invalid_service_status_json"; + return false; + } + + status->available = json.value("ok", false); + if (!status->available) { + status->error = json.value("error", std::string("service_unavailable")); + status->error_code = json.value("code", 0u); + return true; + } + + status->interactive_stage = json.value("interactive_stage", std::string()); + status->sas_secure_desktop_grace_active = + json.value("sas_secure_desktop_grace_active", false); + + if (ShouldNormalizeUnlockToUserDesktop( + json.value("interactive_lock_screen_visible", false), + status->interactive_stage, json.value("session_locked", false), + json.value("interactive_logon_ui_visible", false), + json.value("interactive_secure_desktop_active", + json.value("secure_desktop_active", false)), + json.value("credential_ui_visible", false), + json.value("password_box_visible", false), + json.value("unlock_ui_visible", false), + json.value("last_session_event", std::string()))) { + status->interactive_stage = "user-desktop"; + } + return true; +} +#endif +} // namespace + +void GuiRuntime::ResetRemoteServiceStatus(RemoteSession &props) { + props.remote_service_status_received_ = false; + props.remote_service_available_ = false; + props.remote_interactive_stage_.clear(); +} + +void GuiRuntime::ApplyRemoteServiceStatus(RemoteSession &props, + const ServiceStatus &status) { + props.remote_service_status_received_ = true; + props.remote_service_available_ = status.available; + props.remote_interactive_stage_ = status.interactive_stage; +} + +GuiRuntime::RemoteUnlockState +GuiRuntime::GetRemoteUnlockState(const RemoteSession &props) const { + if (!props.remote_service_status_received_) { + return RemoteUnlockState::none; + } + if (!props.remote_service_available_) { + return RemoteUnlockState::service_unavailable; + } + if (props.remote_interactive_stage_ == "credential-ui") { + return RemoteUnlockState::credential_ui; + } + if (props.remote_interactive_stage_ == "lock-screen") { + return RemoteUnlockState::lock_screen; + } + if (props.remote_interactive_stage_ == "secure-desktop") { + return RemoteUnlockState::secure_desktop; + } + return RemoteUnlockState::none; +} + +void GuiRuntime::HandleWindowsServiceIntegration() { +#if _WIN32 + static bool last_logged_service_available = true; + static unsigned int last_logged_service_error_code = 0; + static std::string last_logged_service_error; + + if (!is_server_mode_ || peer_ == nullptr) { + ResetLocalWindowsServiceState(true); + return; + } + + const bool has_connected_remote = [&] { + std::shared_lock lock(connection_status_mutex_); + return std::any_of(connection_status_.begin(), connection_status_.end(), + [](const auto &entry) { + return entry.second == ConnectionStatus::Connected; + }); + }(); + if (!has_connected_remote) { + ResetLocalWindowsServiceState(false); + return; + } + + bool force_broadcast = false; + if (pending_windows_service_sas_.exchange(false, std::memory_order_relaxed)) { + const std::string response = + QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs); + auto json = nlohmann::json::parse(response, nullptr, false); + if (json.is_discarded() || !json.value("ok", false)) { + LOG_WARN("Remote SAS request failed: {}", response); + } else { + LOG_INFO("Remote SAS request forwarded to local Windows service"); + optimistic_windows_secure_desktop_until_tick_ = + static_cast(SDL_GetTicks()) + + kWindowsServiceSasSecureDesktopGraceMs; + local_service_status_received_ = true; + local_service_available_ = true; + local_interactive_stage_ = "secure-desktop"; + } + last_windows_service_status_tick_ = 0; + force_broadcast = true; + } + + const uint32_t now = static_cast(SDL_GetTicks()); + if (!force_broadcast && last_windows_service_status_tick_ != 0 && + now - last_windows_service_status_tick_ < + kWindowsServiceStatusIntervalMs) { + return; + } + last_windows_service_status_tick_ = now; + + WindowsServiceInteractiveStatus status; + const bool status_ok = QueryWindowsServiceInteractiveStatus(&status); + WindowsServiceInteractiveStatus broadcast_status = status; + const bool previous_secure_desktop_interaction = + IsSecureDesktopInteractionRequired(local_interactive_stage_); + const bool optimistic_secure_desktop_active = + optimistic_windows_secure_desktop_until_tick_ != 0 && + static_cast(optimistic_windows_secure_desktop_until_tick_ - + now) > 0; + const bool keep_optimistic_secure_desktop = + status_ok && status.available && optimistic_secure_desktop_active && + status.sas_secure_desktop_grace_active && + status.interactive_stage == "user-desktop"; + local_service_status_received_ = + status_ok || previous_secure_desktop_interaction; + local_service_available_ = status.available; + if (status.available) { + if (keep_optimistic_secure_desktop) { + local_interactive_stage_ = "secure-desktop"; + broadcast_status.interactive_stage = local_interactive_stage_; + } else { + local_interactive_stage_ = status.interactive_stage; + optimistic_windows_secure_desktop_until_tick_ = 0; + } + } else if (!previous_secure_desktop_interaction) { + local_interactive_stage_.clear(); + optimistic_windows_secure_desktop_until_tick_ = 0; + } + + if (status_ok) { + const bool availability_changed = + status.available != last_logged_service_available; + const bool error_changed = + !status.available && + (status.error != last_logged_service_error || + status.error_code != last_logged_service_error_code); + if (availability_changed || error_changed) { + if (status.available) { + LOG_INFO( + "Local Windows service available for secure desktop integration"); + } else if (IsTransientWindowsServiceStatusError(status.error)) { + LOG_INFO("Local Windows service temporarily unavailable, keeping last " + "secure desktop state: error={}, code={}", + status.error, status.error_code); + } else { + LOG_WARN( + "Local Windows service unavailable, secure desktop integration " + "disabled: error={}, code={}", + status.error, status.error_code); + } + last_logged_service_available = status.available; + last_logged_service_error = status.error; + last_logged_service_error_code = status.error_code; + } + } else if (last_logged_service_available || + last_logged_service_error != "invalid_service_status_json") { + LOG_WARN( + "Local Windows service status query failed, secure desktop integration " + "disabled"); + last_logged_service_available = false; + last_logged_service_error = "invalid_service_status_json"; + last_logged_service_error_code = 0; + } + + RemoteAction remote_action = + BuildWindowsServiceStatusAction(broadcast_status); + std::string msg = remote_action.to_json(); + int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(), + control_data_label_.c_str()); + if (ret != 0) { + LOG_WARN("Broadcast Windows service status failed, ret={}", ret); + } +#endif +} + +#if _WIN32 +void GuiRuntime::ResetLocalWindowsServiceState(bool clear_pending_sas) { + last_windows_service_status_tick_ = 0; + if (clear_pending_sas) { + pending_windows_service_sas_.store(false, std::memory_order_relaxed); + } + local_service_status_received_ = false; + local_service_available_ = false; + local_interactive_stage_.clear(); + optimistic_windows_secure_desktop_until_tick_ = 0; +} +#endif + +} // namespace crossdesk diff --git a/src/gui/panels/local_peer_panel.cpp b/src/gui/views/panels/local_peer_panel.cpp similarity index 98% rename from src/gui/panels/local_peer_panel.cpp rename to src/gui/views/panels/local_peer_panel.cpp index a4e2d53..a3485ce 100644 --- a/src/gui/panels/local_peer_panel.cpp +++ b/src/gui/views/panels/local_peer_panel.cpp @@ -1,14 +1,14 @@ #include +#include "application/gui_application.h" #include "layout_relative.h" #include "localization.h" #include "rd_log.h" -#include "render.h" namespace crossdesk { -int Render::LocalWindow() { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::LocalWindow() { + ImGuiIO &io = ImGui::GetIO(); float local_window_width = io.DisplaySize.x * 0.5f; float local_window_height = io.DisplaySize.y * (1 - TITLE_BAR_HEIGHT - STATUS_BAR_HEIGHT); @@ -239,7 +239,7 @@ int Render::LocalWindow() { if (config_center_->IsSelfHosted()) { std::string self_hosted_id_str; if (strlen(self_hosted_id_) > 0) { - const char* at_pos = strchr(self_hosted_id_, '@'); + const char *at_pos = strchr(self_hosted_id_, '@'); if (at_pos != nullptr) { self_hosted_id_str = std::string(self_hosted_id_, at_pos - self_hosted_id_); @@ -266,7 +266,7 @@ int Render::LocalWindow() { '\0'; } - SaveSettingsIntoCacheFile(); + settings_.Save(); memset(new_password_, 0, sizeof(new_password_)); @@ -300,4 +300,4 @@ int Render::LocalWindow() { return 0; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/panels/recent_connections_panel.cpp b/src/gui/views/panels/recent_connections_panel.cpp similarity index 93% rename from src/gui/panels/recent_connections_panel.cpp rename to src/gui/views/panels/recent_connections_panel.cpp index 9565b3d..fec795b 100644 --- a/src/gui/panels/recent_connections_panel.cpp +++ b/src/gui/views/panels/recent_connections_panel.cpp @@ -1,15 +1,15 @@ #include #include +#include "application/gui_application.h" #include "layout_relative.h" #include "localization.h" #include "rd_log.h" -#include "render.h" namespace crossdesk { namespace { -std::string TrimConnectionAlias(const char* value) { +std::string TrimConnectionAlias(const char *value) { std::string alias = value ? value : ""; auto not_space = [](unsigned char ch) { return !std::isspace(ch); }; @@ -21,7 +21,7 @@ std::string TrimConnectionAlias(const char* value) { return alias; } -void SetDarkTextTooltip(const char* text) { +void SetDarkTextTooltip(const char *text) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.05f, 0.05f, 0.05f, 1.0f)); ImGui::BeginTooltip(); ImGui::SetWindowFontScale(0.5f); @@ -31,10 +31,10 @@ void SetDarkTextTooltip(const char* text) { ImGui::PopStyleColor(); } -} // namespace +} // namespace -int Render::RecentConnectionsWindow() { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::RecentConnectionsWindow() { + ImGuiIO &io = ImGui::GetIO(); float recent_connection_window_width = io.DisplaySize.x; float recent_connection_window_height = io.DisplaySize.y * (0.455f - STATUS_BAR_HEIGHT); @@ -66,8 +66,8 @@ int Render::RecentConnectionsWindow() { return 0; } -int Render::ShowRecentConnections() { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::ShowRecentConnections() { + ImGuiIO &io = ImGui::GetIO(); float recent_connection_panel_width = io.DisplaySize.x * 0.912f; float recent_connection_panel_height = io.DisplaySize.y * 0.29f; float recent_connection_image_height = recent_connection_panel_height * 0.6f; @@ -114,7 +114,7 @@ int Render::ShowRecentConnections() { ImGui::PopStyleColor(); size_t recent_connections_count = recent_connections_.size(); int count = 0; - for (auto& it : recent_connections_) { + for (auto &it : recent_connections_) { sub_containers_pos[it.first] = ImGui::GetCursorPos(); std::string recent_connection_sub_window_name = "RecentConnectionsSubContainer" + it.first; @@ -173,8 +173,8 @@ int Render::ShowRecentConnections() { it.second.remember_password = false; } - std::string display_name = GetRecentConnectionDisplayName(it.second); - bool online = device_presence_.IsOnline(it.second.remote_id); + std::string display_name = settings_.RecentConnectionDisplayName(it.second); + bool online = device_presence_cache_.IsOnline(it.second.remote_id); ImVec2 image_pos = ImVec2(ImGui::GetCursorPosX() + recent_connection_image_width * 0.05f, @@ -264,7 +264,7 @@ int Render::ShowRecentConnections() { ImGui::EndTooltip(); } - ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImDrawList *draw_list = ImGui::GetWindowDrawList(); // connection name footer { @@ -314,11 +314,11 @@ int Render::ShowRecentConnections() { ImVec2 text_min = ImVec2(status_block_end_x + status_gap, footer_screen_pos.y); - ImVec2 text_max = ImVec2( - card_hovered - ? toolbar_screen_pos.x - status_gap - : footer_screen_end.x - recent_connection_name_width * 0.05f, - footer_screen_end.y); + ImVec2 text_max = + ImVec2(card_hovered ? toolbar_screen_pos.x - status_gap + : footer_screen_end.x - + recent_connection_name_width * 0.05f, + footer_screen_end.y); ImGui::SetWindowFontScale(0.52f); @@ -367,7 +367,7 @@ int Render::ShowRecentConnections() { if (ImGui::Button(recent_connection_edit_button_name.c_str(), ImVec2(recent_connection_button_width, recent_connection_button_height))) { - BeginEditRecentConnectionAlias(it.second); + settings_.BeginEditRecentConnectionAlias(it.second); } if (ImGui::IsItemHovered()) { @@ -433,8 +433,8 @@ int Render::ShowRecentConnections() { if (delete_connection_ && delete_connection_name_ == it.first) { if (!thumbnail_->DeleteThumbnail(it.first)) { - recent_connection_aliases_.erase(it.second.remote_id); - SaveRecentConnectionAliases(); + settings_.EraseRecentConnectionAlias(it.second.remote_id); + settings_.SaveRecentConnectionAliases(); reload_recent_connections_ = true; delete_connection_ = false; } @@ -475,8 +475,8 @@ int Render::ShowRecentConnections() { return 0; } -int Render::ConfirmDeleteConnection() { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::ConfirmDeleteConnection() { + ImGuiIO &io = ImGui::GetIO(); ImGui::SetNextWindowPos( ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f)); ImGui::SetNextWindowSize( @@ -529,8 +529,8 @@ int Render::ConfirmDeleteConnection() { return 0; } -int Render::EditRecentConnectionAliasWindow() { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::EditRecentConnectionAliasWindow() { + ImGuiIO &io = ImGui::GetIO(); ImGui::SetNextWindowPos( ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f)); ImGui::SetNextWindowSize( @@ -582,12 +582,13 @@ int Render::EditRecentConnectionAliasWindow() { enter_pressed) { std::string alias = TrimConnectionAlias(edit_connection_alias_); if (alias.empty()) { - recent_connection_aliases_.erase(edit_connection_alias_remote_id_); + settings_.EraseRecentConnectionAlias(edit_connection_alias_remote_id_); } else { - recent_connection_aliases_[edit_connection_alias_remote_id_] = alias; + settings_.SetRecentConnectionAlias(edit_connection_alias_remote_id_, + alias); } - SaveRecentConnectionAliases(); + settings_.SaveRecentConnectionAliases(); show_edit_connection_alias_window_ = false; focus_on_input_widget_ = true; memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_)); @@ -612,8 +613,8 @@ int Render::EditRecentConnectionAliasWindow() { return 0; } -int Render::OfflineWarningWindow() { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::OfflineWarningWindow() { + ImGuiIO &io = ImGui::GetIO(); ImGui::SetNextWindowPos( ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f)); ImGui::SetNextWindowSize( @@ -652,4 +653,4 @@ int Render::OfflineWarningWindow() { ImGui::PopStyleVar(); return 0; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/panels/remote_peer_panel.cpp b/src/gui/views/panels/remote_peer_panel.cpp similarity index 50% rename from src/gui/panels/remote_peer_panel.cpp rename to src/gui/views/panels/remote_peer_panel.cpp index bb4f68a..bb75971 100644 --- a/src/gui/panels/remote_peer_panel.cpp +++ b/src/gui/views/panels/remote_peer_panel.cpp @@ -1,14 +1,14 @@ +#include "application/gui_application.h" #include "layout_relative.h" #include "localization.h" #include "rd_log.h" -#include "render.h" namespace crossdesk { -static int InputTextCallback(ImGuiInputTextCallbackData* data); +static int InputTextCallback(ImGuiInputTextCallbackData *data); -int Render::RemoteWindow() { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::RemoteWindow() { + ImGuiIO &io = ImGui::GetIO(); float remote_window_width = io.DisplaySize.x * 0.5f; float remote_window_height = io.DisplaySize.y * (1 - TITLE_BAR_HEIGHT - STATUS_BAR_HEIGHT); @@ -92,16 +92,16 @@ int Render::RemoteWindow() { bool should_connect = false; bool already_connected = false; - for (auto& [id, props] : recent_connections_) { + for (auto &[id, props] : recent_connections_) { if (id.find(remote_id) != std::string::npos) { found = true; target_remote_id = props.remote_id; target_password = props.password; { - // std::shared_lock lock(client_properties_mutex_); - if (client_properties_.find(remote_id) != - client_properties_.end()) { - if (!client_properties_[remote_id]->connection_established_) { + // std::shared_lock lock(remote_sessions_mutex_); + if (remote_sessions_.find(remote_id) != + remote_sessions_.end()) { + if (!remote_sessions_[remote_id]->connection_established_) { should_connect = true; } else { already_connected = true; @@ -134,8 +134,8 @@ int Render::RemoteWindow() { if (elapsed >= 1000) { last_rejoin_check_time_ = now; need_to_rejoin_ = false; - // std::shared_lock lock(client_properties_mutex_); - for (const auto& [_, props] : client_properties_) { + // std::shared_lock lock(remote_sessions_mutex_); + for (const auto &[_, props] : remote_sessions_) { if (props->rejoin_) { ConnectTo(props->remote_id_, props->remote_password_, props->remember_password_); @@ -152,7 +152,7 @@ int Render::RemoteWindow() { return 0; } -static int InputTextCallback(ImGuiInputTextCallbackData* data) { +static int InputTextCallback(ImGuiInputTextCallbackData *data) { if (data->BufTextLen > 3 && data->Buf[3] != ' ') { data->InsertChars(3, " "); } @@ -164,117 +164,4 @@ static int InputTextCallback(ImGuiInputTextCallbackData* data) { return 0; } -int Render::ConnectTo(const std::string& remote_id, const char* password, - bool remember_password, bool bypass_presence_check) { - if (!bypass_presence_check && !device_presence_.IsOnline(remote_id)) { - int ret = - RequestSingleDevicePresence(remote_id, password, remember_password); - if (ret != 0) { - offline_warning_text_ = - localization::device_offline[localization_language_index_]; - show_offline_warning_window_ = true; - LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret); - } else { - LOG_INFO("Presence probe requested for [{}] before connect", remote_id); - } - return -1; - } - - LOG_INFO("Connect to [{}]", remote_id); - focused_remote_id_ = remote_id; - - // std::shared_lock shared_lock(client_properties_mutex_); - bool exists = - (client_properties_.find(remote_id) != client_properties_.end()); - // shared_lock.unlock(); - - if (!exists) { - PeerPtr* peer_to_init = nullptr; - std::string local_id; - - { - // std::unique_lock unique_lock(client_properties_mutex_); - if (client_properties_.find(remote_id) == client_properties_.end()) { - client_properties_[remote_id] = - std::make_shared(); - auto props = client_properties_[remote_id]; - props->local_id_ = "C-" + std::string(client_id_); - props->remote_id_ = remote_id; - memcpy(&props->params_, ¶ms_, sizeof(Params)); - props->params_.user_id = props->local_id_.c_str(); - props->peer_ = CreatePeer(&props->params_); - - props->control_window_width_ = title_bar_height_ * 10.0f; - props->control_window_height_ = title_bar_height_ * 1.3f; - props->control_window_min_width_ = title_bar_height_ * 0.65f; - props->control_window_min_height_ = title_bar_height_ * 1.3f; - props->control_window_max_width_ = title_bar_height_ * 10.0f; - props->control_window_max_height_ = title_bar_height_ * 7.0f; - - props->connection_status_.store(ConnectionStatus::Connecting); - show_connection_status_window_ = true; - - if (!props->peer_) { - LOG_INFO("Create peer [{}] instance failed", props->local_id_); - return -1; - } - - for (auto& display_info : display_info_list_) { - AddVideoStream(props->peer_, display_info.name.c_str()); - } - AddAudioStream(props->peer_, props->audio_label_.c_str()); - AddDataStream(props->peer_, props->data_label_.c_str(), false); - AddDataStream(props->peer_, props->mouse_label_.c_str(), false); - AddDataStream(props->peer_, props->keyboard_label_.c_str(), true); - AddDataStream(props->peer_, props->control_data_label_.c_str(), true); - AddDataStream(props->peer_, props->file_label_.c_str(), true); - AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true); - AddDataStream(props->peer_, props->clipboard_label_.c_str(), true); - - props->connection_status_.store(ConnectionStatus::Connecting); - - peer_to_init = props->peer_; - local_id = props->local_id_; - } - } - - if (peer_to_init) { - LOG_INFO("[{}] Create peer instance successful", local_id); - Init(peer_to_init); - LOG_INFO("[{}] Peer init finish", local_id); - } - } - - int ret = -1; - // std::shared_lock read_lock(client_properties_mutex_); - auto props = client_properties_[remote_id]; - if (!props->connection_established_) { - props->connection_status_.store(ConnectionStatus::Connecting); - props->connection_attempt_active_.store(true); - props->connection_attempt_started_at_ = std::chrono::steady_clock::now(); - show_connection_status_window_ = true; - - props->remember_password_ = remember_password; - if (strcmp(password, "") != 0 && - strcmp(password, props->remote_password_) != 0) { - strncpy(props->remote_password_, password, - sizeof(props->remote_password_) - 1); - props->remote_password_[sizeof(props->remote_password_) - 1] = '\0'; - } - - std::string remote_id_with_pwd = remote_id + "@" + password; - if (props->peer_) { - ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str()); - if (0 == ret) { - props->rejoin_ = false; - } else { - props->rejoin_ = true; - need_to_rejoin_ = true; - } - } - } - // read_lock.unlock(); - - return 0; -} -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/toolbars/control_bar.cpp b/src/gui/views/toolbars/control_bar.cpp similarity index 82% rename from src/gui/toolbars/control_bar.cpp rename to src/gui/views/toolbars/control_bar.cpp index 5de751d..26c33b3 100644 --- a/src/gui/toolbars/control_bar.cpp +++ b/src/gui/views/toolbars/control_bar.cpp @@ -6,18 +6,18 @@ #include #include +#include "application/gui_application.h" #include "file_transfer.h" #include "layout.h" #include "localization.h" #include "rd_log.h" -#include "render.h" #include "tinyfiledialogs.h" namespace crossdesk { namespace { -void ShowControlBarTooltip(const std::string& text) { +void ShowControlBarTooltip(const std::string &text) { if (!ImGui::IsItemHovered() || text.empty()) { return; } @@ -29,10 +29,11 @@ void ShowControlBarTooltip(const std::string& text) { ImGui::EndTooltip(); } -} // namespace +} // namespace int CountDigits(int number) { - if (number == 0) return 1; + if (number == 0) + return 1; return (int)std::floor(std::log10(std::abs(number))) + 1; } @@ -57,98 +58,20 @@ int LossRateDisplay(float loss_rate) { return 0; } -std::string Render::OpenFileDialog(std::string title) { - const char* path = tinyfd_openFileDialog(title.c_str(), - "", // default path - 0, // number of filters - nullptr, // filters - nullptr, // filter description - 0 // no multiple selection +std::string GuiApplication::OpenFileDialog(std::string title) { + const char *path = tinyfd_openFileDialog(title.c_str(), + "", // default path + 0, // number of filters + nullptr, // filters + nullptr, // filter description + 0 // no multiple selection ); return path ? path : ""; } -void Render::ProcessSelectedFile( - const std::string& path, - const std::shared_ptr& props, - const std::string& file_label, const std::string& remote_id) { - if (path.empty()) { - return; - } - - FileTransferState* file_transfer_state = - props ? &props->file_transfer_ : &file_transfer_; - - LOG_INFO("Selected file: {}", path.c_str()); - - std::filesystem::path file_path = std::filesystem::u8path(path); - - // Get file size - std::error_code ec; - uint64_t file_size = std::filesystem::file_size(file_path, ec); - if (ec) { - LOG_ERROR("Failed to get file size: {}", ec.message().c_str()); - file_size = 0; - } - - // Add file to transfer list - { - std::lock_guard lock( - file_transfer_state->file_transfer_list_mutex_); - FileTransferState::FileTransferInfo info; - info.file_name = file_path.filename().u8string(); - info.file_path = file_path; // Store full path for precise matching - info.file_size = file_size; - info.status = FileTransferState::FileTransferStatus::Queued; - info.sent_bytes = 0; - info.file_id = 0; - info.rate_bps = 0; - file_transfer_state->file_transfer_list_.push_back(info); - } - file_transfer_state->file_transfer_window_visible_ = true; - - if (file_transfer_state->file_sending_.load()) { - // Add to queue - size_t queue_size = 0; - { - std::lock_guard lock(file_transfer_state->file_queue_mutex_); - FileTransferState::QueuedFile queued_file; - queued_file.file_path = file_path; - queued_file.file_label = file_label; - queued_file.remote_id = remote_id; - file_transfer_state->file_send_queue_.push(queued_file); - queue_size = file_transfer_state->file_send_queue_.size(); - } - LOG_INFO("File added to queue: {} ({} files in queue)", - file_path.filename().string().c_str(), queue_size); - } else { - StartFileTransfer(props, file_path, file_label, remote_id); - - if (file_transfer_state->file_sending_.load()) { - } else { - // Failed to start (race condition: another file started between - // check and call) Add to queue - size_t queue_size = 0; - { - std::lock_guard lock( - file_transfer_state->file_queue_mutex_); - FileTransferState::QueuedFile queued_file; - queued_file.file_path = file_path; - queued_file.file_label = file_label; - queued_file.remote_id = remote_id; - file_transfer_state->file_send_queue_.push(queued_file); - queue_size = file_transfer_state->file_send_queue_.size(); - } - LOG_INFO( - "File added to queue after race condition: {} ({} files in " - "queue)", - file_path.filename().string().c_str(), queue_size); - } - } -} - -int Render::ControlBar(std::shared_ptr& props) { +int GuiApplication::ControlBar( + std::shared_ptr &props) { float button_width = title_bar_height_ * 0.8f; float button_height = title_bar_height_ * 0.8f; float line_padding = title_bar_height_ * 0.12f; @@ -160,7 +83,7 @@ int Render::ControlBar(std::shared_ptr& props) { ? props->control_window_width_ * 0.03f : props->control_window_width_ * 0.17f); - ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImDrawList *draw_list = ImGui::GetWindowDrawList(); if (!props->is_control_bar_in_left_) { draw_list->AddLine( ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f, @@ -214,7 +137,7 @@ int Render::ControlBar(std::shared_ptr& props) { std::to_string(props->selected_display_ + 1).c_str()); auto send_service_command = [&](ServiceCommandFlag flag, - const char* log_action) { + const char *log_action) { if (props->connection_status_.load() == ConnectionStatus::Connected && props->peer_) { RemoteAction remote_action; @@ -290,9 +213,9 @@ int Render::ControlBar(std::shared_ptr& props) { draw_list->AddLine( ImVec2(disable_mouse_x - line_thickness * 0.7f, disable_mouse_y + line_thickness * 0.7f), - ImVec2( - mouse_x + button_width - line_padding - line_thickness * 0.7f, - mouse_y + button_height - line_padding + line_thickness * 0.7f), + ImVec2(mouse_x + button_width - line_padding - line_thickness * 0.7f, + mouse_y + button_height - line_padding + + line_thickness * 0.7f), mouse_button_hovered ? IM_COL32(66, 150, 250, 255) : IM_COL32(179, 213, 253, 255), line_thickness); @@ -341,9 +264,9 @@ int Render::ControlBar(std::shared_ptr& props) { draw_list->AddLine( ImVec2(disable_audio_x - line_thickness * 0.7f, disable_audio_y + line_thickness * 0.7f), - ImVec2( - audio_x + button_width - line_padding - line_thickness * 0.7f, - audio_y + button_height - line_padding + line_thickness * 0.7f), + ImVec2(audio_x + button_width - line_padding - line_thickness * 0.7f, + audio_y + button_height - line_padding + + line_thickness * 0.7f), audio_button_hovered ? IM_COL32(66, 150, 250, 255) : IM_COL32(179, 213, 253, 255), line_thickness); @@ -356,7 +279,7 @@ int Render::ControlBar(std::shared_ptr& props) { std::string title = localization::select_file[localization_language_index_]; std::string path = OpenFileDialog(title); - ProcessSelectedFile(path, props, file_label_); + transfers_.ProcessSelectedFile(path, props, file_label_); } ShowControlBarTooltip( localization::select_file[localization_language_index_]); @@ -428,7 +351,7 @@ int Render::ControlBar(std::shared_ptr& props) { ImGui::SetWindowFontScale(0.5f); if (ImGui::Button(close_button.c_str(), ImVec2(button_width, button_height))) { - CleanupPeer(props); + CloseRemoteSession(props); } ShowControlBarTooltip( localization::disconnect[localization_language_index_]); @@ -487,7 +410,8 @@ int Render::ControlBar(std::shared_ptr& props) { return 0; } -int Render::NetTrafficStats(std::shared_ptr& props) { +int GuiApplication::NetTrafficStats( + std::shared_ptr &props) { ImGui::SetCursorPos(ImVec2(props->control_window_width_ * 0.048f, props->control_window_min_height_)); ImGui::SetWindowFontScale(0.5f); @@ -582,4 +506,4 @@ int Render::NetTrafficStats(std::shared_ptr& props) { return 0; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/toolbars/status_bar.cpp b/src/gui/views/toolbars/status_bar.cpp similarity index 64% rename from src/gui/toolbars/status_bar.cpp rename to src/gui/views/toolbars/status_bar.cpp index 280ebe6..7ef7cfe 100644 --- a/src/gui/toolbars/status_bar.cpp +++ b/src/gui/views/toolbars/status_bar.cpp @@ -1,11 +1,11 @@ +#include "application/gui_application.h" #include "layout_relative.h" #include "localization.h" -#include "render.h" namespace crossdesk { -int Render::StatusBar() { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::StatusBar() { + ImGuiIO &io = ImGui::GetIO(); float status_bar_width = io.DisplaySize.x; float status_bar_height = io.DisplaySize.y * STATUS_BAR_HEIGHT; @@ -23,21 +23,19 @@ int Render::StatusBar() { ImVec2 dot_pos = ImVec2(status_bar_width * 0.025f, io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.5f)); - ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImDrawList *draw_list = ImGui::GetWindowDrawList(); draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.25f, ImColor(1.0f, 1.0f, 1.0f), 100); - bool tls_cert_error = - signal_status_ == SignalStatus::SignalTlsCertError; - draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.2f, - tls_cert_error - ? ImColor(1.0f, 0.65f, 0.0f) - : ImColor(signal_connected_ ? 0.0f : 1.0f, - signal_connected_ ? 1.0f : 0.0f, - 0.0f), - 100); + bool tls_cert_error = signal_status_ == SignalStatus::SignalTlsCertError; + draw_list->AddCircleFilled( + dot_pos, status_bar_height * 0.2f, + tls_cert_error ? ImColor(1.0f, 0.65f, 0.0f) + : ImColor(signal_connected_ ? 0.0f : 1.0f, + signal_connected_ ? 1.0f : 0.0f, 0.0f), + 100); ImGui::SetWindowFontScale(0.6f); - const char* signal_status_text = + const char *signal_status_text = tls_cert_error ? localization::signal_tls_cert_error[localization_language_index_] .c_str() @@ -47,13 +45,12 @@ int Render::StatusBar() { : localization::signal_disconnected [localization_language_index_] .c_str()); - draw_list->AddText( - ImVec2(status_bar_width * 0.045f, - io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.9f)), - ImColor(0.0f, 0.0f, 0.0f), signal_status_text); + draw_list->AddText(ImVec2(status_bar_width * 0.045f, + io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.9f)), + ImColor(0.0f, 0.0f, 0.0f), signal_status_text); ImGui::SetWindowFontScale(1.0f); ImGui::EndChild(); return 0; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/toolbars/title_bar.cpp b/src/gui/views/toolbars/title_bar.cpp similarity index 95% rename from src/gui/toolbars/title_bar.cpp rename to src/gui/views/toolbars/title_bar.cpp index c986345..8a12295 100644 --- a/src/gui/toolbars/title_bar.cpp +++ b/src/gui/views/toolbars/title_bar.cpp @@ -1,7 +1,7 @@ +#include "application/gui_application.h" #include "layout_relative.h" #include "localization.h" #include "rd_log.h" -#include "render.h" #include "rounded_corner_button.h" constexpr double kNewVersionIconBlinkIntervalSec = 2.0; @@ -9,8 +9,8 @@ constexpr double kNewVersionIconBlinkOnTimeSec = 1.0; namespace crossdesk { -int Render::TitleBar(bool main_window) { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::TitleBar(bool main_window) { + ImGuiIO &io = ImGui::GetIO(); float title_bar_width = title_bar_width_; float title_bar_height = title_bar_height_; float title_bar_height_padding = title_bar_height_; @@ -58,7 +58,7 @@ int Render::TitleBar(bool main_window) { ImGui::PopStyleColor(); // get draw list - ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImDrawList *draw_list = ImGui::GetWindowDrawList(); ImGui::SetCursorPos( ImVec2(title_bar_width - title_bar_button_width * 3, 0.0f)); @@ -83,7 +83,7 @@ int Render::TitleBar(bool main_window) { bar_pos_y + title_bar_button_height * 0.15f), IM_COL32(0, 0, 0, 255)); - std::string title_bar_menu_button = "##title_bar_menu"; // ICON_FA_BARS; + std::string title_bar_menu_button = "##title_bar_menu"; // ICON_FA_BARS; std::string title_bar_menu = "##title_bar_menu"; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); @@ -195,7 +195,7 @@ int Render::TitleBar(bool main_window) { title_bar_button_width * (main_window ? 2 : 3) + title_bar_button_width * 0.33f; float minimize_pos_y = title_bar_button_height * 0.5f; - std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS; + std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS; if (ImGui::Button(window_minimize_button.c_str(), ImVec2(title_bar_button_width, title_bar_button_height))) { if (main_window) { @@ -225,7 +225,7 @@ int Render::TitleBar(bool main_window) { float pos_x_bottom = title_bar_width - title_bar_button_width * 1.6f; float pos_y_bottom = title_bar_button_height * 0.28f; std::string window_restore_button = - "##restore"; // ICON_FA_WINDOW_RESTORE; + "##restore"; // ICON_FA_WINDOW_RESTORE; if (ImGui::Button( window_restore_button.c_str(), ImVec2(title_bar_button_width, title_bar_button_height))) { @@ -259,8 +259,7 @@ int Render::TitleBar(bool main_window) { float maximize_pos_x = title_bar_width - title_bar_button_width * 1.5f - title_bar_button_height * 0.165f; float maximize_pos_y = title_bar_button_height * 0.33f; - std::string window_maximize_button = - "##maximize"; // ICON_FA_SQUARE_FULL; + std::string window_maximize_button = "##maximize"; // ICON_FA_SQUARE_FULL; if (ImGui::Button( window_maximize_button.c_str(), ImVec2(title_bar_button_width, title_bar_button_height))) { @@ -285,7 +284,7 @@ int Render::TitleBar(bool main_window) { float xmark_pos_x = xmark_button_pos_x + title_bar_button_width * 0.5f; float xmark_pos_y = title_bar_button_height * 0.5f; float xmark_size = title_bar_button_width * 0.33f; - std::string close_button = "##xmark"; // ICON_FA_XMARK; + std::string close_button = "##xmark"; // ICON_FA_XMARK; bool close_button_clicked = false; if (main_window) { close_button_clicked = RoundedCornerButton( @@ -324,4 +323,4 @@ int Render::TitleBar(bool main_window) { ImGui::EndChild(); return 0; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/windows/about_window.cpp b/src/gui/views/windows/about_window.cpp similarity index 92% rename from src/gui/windows/about_window.cpp rename to src/gui/views/windows/about_window.cpp index 62d14a7..a153d06 100644 --- a/src/gui/windows/about_window.cpp +++ b/src/gui/views/windows/about_window.cpp @@ -5,14 +5,14 @@ #include #endif +#include "application/gui_application.h" #include "layout.h" #include "localization.h" #include "rd_log.h" -#include "render.h" namespace crossdesk { -bool Render::OpenUrl(const std::string& url) { +bool GuiApplication::OpenUrl(const std::string &url) { #if defined(_WIN32) int wide_len = MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0); if (wide_len <= 0) { @@ -46,8 +46,8 @@ bool Render::OpenUrl(const std::string& url) { #endif } -void Render::Hyperlink(const std::string& label, const std::string& url, - const float window_width) { +void GuiApplication::Hyperlink(const std::string &label, const std::string &url, + const float window_width) { ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255)); ImGui::TextUnformatted(label.c_str()); ImGui::PopStyleColor(); @@ -65,14 +65,14 @@ void Render::Hyperlink(const std::string& label, const std::string& url, } } -int Render::AboutWindow() { +int GuiApplication::AboutWindow() { if (show_about_window_) { float about_window_width = title_bar_button_width_ * 7.5f; float about_window_height = latest_version_.empty() ? title_bar_button_width_ * 4.0f : title_bar_button_width_ * 4.9f; - const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const ImGuiViewport *viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(ImVec2( (viewport->WorkSize.x - viewport->WorkPos.x - about_window_width) / 2, (viewport->WorkSize.y - viewport->WorkPos.y - about_window_height) / @@ -145,4 +145,4 @@ int Render::AboutWindow() { } return 0; } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk \ No newline at end of file diff --git a/src/gui/windows/connection_status_window.cpp b/src/gui/views/windows/connection_status_window.cpp similarity index 97% rename from src/gui/windows/connection_status_window.cpp rename to src/gui/views/windows/connection_status_window.cpp index a55d5ab..29ad26f 100644 --- a/src/gui/windows/connection_status_window.cpp +++ b/src/gui/views/windows/connection_status_window.cpp @@ -1,13 +1,13 @@ +#include "application/gui_application.h" #include "layout.h" #include "localization.h" #include "rd_log.h" -#include "render.h" namespace crossdesk { -bool Render::ConnectionStatusWindow( - std::shared_ptr& props) { - ImGuiIO& io = ImGui::GetIO(); +bool GuiApplication::ConnectionStatusWindow( + std::shared_ptr &props) { + ImGuiIO &io = ImGui::GetIO(); bool ret_flag = false; ImGui::SetNextWindowPos( @@ -206,4 +206,4 @@ bool Render::ConnectionStatusWindow( return ret_flag; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/windows/control_window.cpp b/src/gui/views/windows/control_window.cpp similarity index 97% rename from src/gui/windows/control_window.cpp rename to src/gui/views/windows/control_window.cpp index bd55e4a..1a9253d 100644 --- a/src/gui/windows/control_window.cpp +++ b/src/gui/views/windows/control_window.cpp @@ -1,9 +1,10 @@ +#include "application/gui_application.h" #include "rd_log.h" -#include "render.h" namespace crossdesk { -int Render::ControlWindow(std::shared_ptr& props) { +int GuiApplication::ControlWindow( + std::shared_ptr &props) { double time_duration = ImGui::GetTime() - props->control_bar_button_pressed_time_; if (props->control_window_width_is_changing_) { @@ -77,8 +78,10 @@ int Render::ControlWindow(std::shared_ptr& props) { ImVec2 delta = ImGui::GetIO().MouseDelta; pos_x = current_x_rel + delta.x; pos_y = current_y_rel + delta.y; - if (pos_x < 0.0f) pos_x = 0.0f; - if (pos_y < 0.0f) pos_y = 0.0f; + if (pos_x < 0.0f) + pos_x = 0.0f; + if (pos_y < 0.0f) + pos_y = 0.0f; if (pos_x + props->control_window_width_ > container_w) pos_x = container_w - props->control_window_width_; if (pos_y + props->control_window_height_ > container_h) @@ -185,8 +188,10 @@ int Render::ControlWindow(std::shared_ptr& props) { float current_y_rel = props->control_window_pos_.y - container_pos.y; pos_x = current_x_rel; pos_y = current_y_rel; - if (pos_x < 0.0f) pos_x = 0.0f; - if (pos_y < 0.0f) pos_y = 0.0f; + if (pos_x < 0.0f) + pos_x = 0.0f; + if (pos_y < 0.0f) + pos_y = 0.0f; if (pos_x + props->control_window_width_ > container_w) pos_x = container_w - props->control_window_width_; if (pos_y + props->control_window_height_ > container_h) @@ -256,4 +261,4 @@ int Render::ControlWindow(std::shared_ptr& props) { return 0; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/windows/file_transfer_window.cpp b/src/gui/views/windows/file_transfer_window.cpp similarity index 71% rename from src/gui/windows/file_transfer_window.cpp rename to src/gui/views/windows/file_transfer_window.cpp index 7872b37..03fd560 100644 --- a/src/gui/windows/file_transfer_window.cpp +++ b/src/gui/views/windows/file_transfer_window.cpp @@ -2,16 +2,17 @@ #include #include "IconsFontAwesome6.h" +#include "application/gui_application.h" #include "layout.h" #include "localization.h" #include "rd_log.h" -#include "render.h" namespace crossdesk { namespace { int CountDigits(int number) { - if (number == 0) return 1; + if (number == 0) + return 1; return (int)std::floor(std::log10(std::abs(number))) + 1; } @@ -26,16 +27,16 @@ int BitrateDisplay(int bitrate) { } return 0; } -} // namespace +} // namespace -int Render::FileTransferWindow( - std::shared_ptr& props) { - FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_; +int GuiApplication::FileTransferWindow( + std::shared_ptr &props) { + FileTransferState *state = &transfers_.state_for(props); state->file_transfer_window_hovered_ = false; // Only show window if there are files in transfer list or currently // transferring - std::vector file_list; + std::vector file_list; { std::lock_guard lock(state->file_transfer_list_mutex_); file_list = state->file_transfer_list_; @@ -44,20 +45,20 @@ int Render::FileTransferWindow( // Sort file list: Sending first, then Completed, then Queued, then Failed std::sort( file_list.begin(), file_list.end(), - [](const SubStreamWindowProperties::FileTransferInfo& a, - const SubStreamWindowProperties::FileTransferInfo& b) { + [](const RemoteSession::FileTransferInfo &a, + const RemoteSession::FileTransferInfo &b) { // Priority: Sending > Completed > Queued > Failed auto get_priority = - [](SubStreamWindowProperties::FileTransferStatus status) { + [](RemoteSession::FileTransferStatus status) { switch (status) { - case SubStreamWindowProperties::FileTransferStatus::Sending: - return 0; - case SubStreamWindowProperties::FileTransferStatus::Completed: - return 1; - case SubStreamWindowProperties::FileTransferStatus::Queued: - return 2; - case SubStreamWindowProperties::FileTransferStatus::Failed: - return 3; + case RemoteSession::FileTransferStatus::Sending: + return 0; + case RemoteSession::FileTransferStatus::Completed: + return 1; + case RemoteSession::FileTransferStatus::Queued: + return 2; + case RemoteSession::FileTransferStatus::Failed: + return 3; } return 3; }; @@ -67,8 +68,8 @@ int Render::FileTransferWindow( // Only show window if file_transfer_window_visible_ is true // Window can be closed by user even during transfer // It will be reopened automatically when: - // 1. A file transfer completes (in render_callback.cpp) - // 2. A new file starts sending from queue (in render.cpp) + // 1. A file transfer completes (via FileTransferManager) + // 2. FileTransferManager starts the next queued file. if (!state->file_transfer_window_visible_) { return 0; } @@ -77,7 +78,7 @@ int Render::FileTransferWindow( // Adjust window size based on number of files float file_transfer_window_width = main_window_width_ * 0.6f; float file_transfer_window_height = - main_window_height_ * 0.3f; // Dynamic height + main_window_height_ * 0.3f; // Dynamic height float pos_x = file_transfer_window_width * 0.05f; float pos_y = stream_window_height_ - file_transfer_window_height - file_transfer_window_width * 0.05; @@ -147,40 +148,40 @@ int Render::FileTransferWindow( ImGui::SetWindowFontScale(0.5f); for (size_t i = 0; i < file_list.size(); ++i) { - const auto& info = file_list[i]; + const auto &info = file_list[i]; ImGui::PushID(static_cast(i)); // Status icon and file name - const char* status_icon = ""; + const char *status_icon = ""; ImVec4 status_color(0.5f, 0.5f, 0.5f, 1.0f); - const char* status_text = ""; + const char *status_text = ""; switch (info.status) { - case SubStreamWindowProperties::FileTransferStatus::Queued: - status_icon = ICON_FA_CLOCK; - status_color = - ImVec4(0.5f, 0.6f, 0.7f, 1.0f); // Common blue-gray for queued - status_text = - localization::queued[localization_language_index_].c_str(); - break; - case SubStreamWindowProperties::FileTransferStatus::Sending: - status_icon = ICON_FA_ARROW_UP; - status_color = ImVec4(0.2f, 0.6f, 1.0f, 1.0f); - status_text = - localization::sending[localization_language_index_].c_str(); - break; - case SubStreamWindowProperties::FileTransferStatus::Completed: - status_icon = ICON_FA_CHECK; - status_color = ImVec4(0.0f, 0.8f, 0.0f, 1.0f); - status_text = - localization::completed[localization_language_index_].c_str(); - break; - case SubStreamWindowProperties::FileTransferStatus::Failed: - status_icon = ICON_FA_XMARK; - status_color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); - status_text = - localization::failed[localization_language_index_].c_str(); - break; + case RemoteSession::FileTransferStatus::Queued: + status_icon = ICON_FA_CLOCK; + status_color = + ImVec4(0.5f, 0.6f, 0.7f, 1.0f); // Common blue-gray for queued + status_text = + localization::queued[localization_language_index_].c_str(); + break; + case RemoteSession::FileTransferStatus::Sending: + status_icon = ICON_FA_ARROW_UP; + status_color = ImVec4(0.2f, 0.6f, 1.0f, 1.0f); + status_text = + localization::sending[localization_language_index_].c_str(); + break; + case RemoteSession::FileTransferStatus::Completed: + status_icon = ICON_FA_CHECK; + status_color = ImVec4(0.0f, 0.8f, 0.0f, 1.0f); + status_text = + localization::completed[localization_language_index_].c_str(); + break; + case RemoteSession::FileTransferStatus::Failed: + status_icon = ICON_FA_XMARK; + status_color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); + status_text = + localization::failed[localization_language_index_].c_str(); + break; } ImGui::TextColored(status_color, "%s", status_icon); @@ -191,7 +192,7 @@ int Render::FileTransferWindow( // Progress bar for sending files if (info.status == - SubStreamWindowProperties::FileTransferStatus::Sending && + RemoteSession::FileTransferStatus::Sending && info.file_size > 0) { float progress = static_cast(info.sent_bytes) / static_cast(info.file_size); @@ -210,7 +211,7 @@ int Render::FileTransferWindow( ImGui::SetCursorPosX(speed_x_pos); BitrateDisplay(static_cast(info.rate_bps)); } else if (info.status == - SubStreamWindowProperties::FileTransferStatus::Completed) { + RemoteSession::FileTransferStatus::Completed) { // Show completed size char size_str[64]; if (info.file_size < 1024) { @@ -249,4 +250,4 @@ int Render::FileTransferWindow( return 0; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/windows/main_settings_window.cpp b/src/gui/views/windows/main_settings_window.cpp similarity index 97% rename from src/gui/windows/main_settings_window.cpp rename to src/gui/views/windows/main_settings_window.cpp index 58b1e15..5647fd8 100644 --- a/src/gui/windows/main_settings_window.cpp +++ b/src/gui/views/windows/main_settings_window.cpp @@ -1,7 +1,7 @@ +#include "application/gui_application.h" #include "layout.h" #include "localization.h" #include "rd_log.h" -#include "render.h" #include "tinyfiledialogs.h" #if _WIN32 && CROSSDESK_PORTABLE @@ -10,8 +10,8 @@ namespace crossdesk { -int Render::SettingWindow() { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::SettingWindow() { + ImGuiIO &io = ImGui::GetIO(); float portable_y_padding = 0.0f; #if _WIN32 && CROSSDESK_PORTABLE portable_y_padding = 0.05f; @@ -19,10 +19,10 @@ int Render::SettingWindow() { if (show_settings_window_) { if (settings_window_pos_reset_) { - const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const ImGuiViewport *viewport = ImGui::GetMainViewport(); if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { -#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ - !defined(__arm__) && USE_CUDA) || \ +#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ + !defined(__arm__) && USE_CUDA) || \ defined(__APPLE__)) ImGui::SetNextWindowPos( ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f)); @@ -37,8 +37,8 @@ int Render::SettingWindow() { io.DisplaySize.y * (0.85f + portable_y_padding))); #endif } else { -#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ - !defined(__arm__) && USE_CUDA) || \ +#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ + !defined(__arm__) && USE_CUDA) || \ defined(__APPLE__)) ImGui::SetNextWindowPos( ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f)); @@ -73,7 +73,7 @@ int Render::SettingWindow() { ImGui::SetWindowFontScale(0.5f); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); { - const auto& supported_languages = localization::GetSupportedLanguages(); + const auto &supported_languages = localization::GetSupportedLanguages(); language_button_value_ = localization::detail::ClampLanguageIndex(language_button_value_); @@ -118,7 +118,7 @@ int Render::SettingWindow() { } { - const char* video_quality_items[] = { + const char *video_quality_items[] = { localization::video_quality_low[localization_language_index_] .c_str(), localization::video_quality_medium[localization_language_index_] @@ -157,7 +157,7 @@ int Render::SettingWindow() { ImGui::Separator(); { - const char* video_frame_rate_items[] = {"30 fps", "60 fps"}; + const char *video_frame_rate_items[] = {"30 fps", "60 fps"}; settings_items_offset += settings_items_padding; ImGui::SetCursorPosY(settings_items_offset); @@ -190,7 +190,7 @@ int Render::SettingWindow() { ImGui::Separator(); { - const char* video_encode_format_items[] = { + const char *video_encode_format_items[] = { localization::h264[localization_language_index_].c_str(), localization::av1[localization_language_index_].c_str()}; @@ -223,8 +223,8 @@ int Render::SettingWindow() { } } -#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ - !defined(__arm__) && USE_CUDA) || \ +#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ + !defined(__arm__) && USE_CUDA) || \ defined(__APPLE__)) ImGui::Separator(); @@ -402,7 +402,7 @@ int Render::SettingWindow() { ImGui::PushFont(main_windows_system_chinese_font_); if (ImGui::Button(display_path.c_str(), ImVec2(title_bar_button_width_ * 2.0f, 0))) { - const char* folder = + const char *folder = tinyfd_selectFolderDialog(localization::file_transfer_save_path [localization_language_index_] .c_str(), @@ -632,12 +632,12 @@ int Render::SettingWindow() { settings_window_pos_reset_ = true; // Recreate peer instance - LoadSettingsFromCacheFile(); + settings_.Load(); // Recreate peer instance if (!stream_window_inited_) { LOG_INFO("Recreate peer instance"); - CleanupPeers(); + CloseAllRemoteSessions(); CreateConnectionPeer(); } } @@ -694,4 +694,4 @@ int Render::SettingWindow() { return 0; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/windows/main_window.cpp b/src/gui/views/windows/main_window.cpp similarity index 78% rename from src/gui/windows/main_window.cpp rename to src/gui/views/windows/main_window.cpp index 0578176..265d8c7 100644 --- a/src/gui/windows/main_window.cpp +++ b/src/gui/views/windows/main_window.cpp @@ -1,12 +1,12 @@ +#include "application/gui_application.h" #include "layout_relative.h" #include "localization.h" #include "rd_log.h" -#include "render.h" namespace crossdesk { -int Render::MainWindow() { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::MainWindow() { + ImGuiIO &io = ImGui::GetIO(); float local_remote_window_width = io.DisplaySize.x; float local_remote_window_height = io.DisplaySize.y * (0.56f - TITLE_BAR_HEIGHT); @@ -27,7 +27,7 @@ int Render::MainWindow() { LocalWindow(); - ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImDrawList *draw_list = ImGui::GetWindowDrawList(); draw_list->AddLine(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.1f), ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.53f), IM_COL32(0, 0, 0, 122), 1.0f); @@ -39,13 +39,13 @@ int Render::MainWindow() { StatusBar(); if (show_connection_status_window_) { - // std::unique_lock lock(client_properties_mutex_); - for (auto it = client_properties_.begin(); - it != client_properties_.end();) { - auto& props = it->second; + // std::unique_lock lock(remote_sessions_mutex_); + for (auto it = remote_sessions_.begin(); + it != remote_sessions_.end();) { + auto &props = it->second; if (focused_remote_id_ == props->remote_id_) { if (ConnectionStatusWindow(props)) { - it = client_properties_.erase(it); + it = remote_sessions_.erase(it); } else { ++it; } @@ -57,4 +57,4 @@ int Render::MainWindow() { return 0; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/windows/portable_service_install_window.cpp b/src/gui/views/windows/portable_service_install_window.cpp similarity index 91% rename from src/gui/windows/portable_service_install_window.cpp rename to src/gui/views/windows/portable_service_install_window.cpp index 84669e7..28886ee 100644 --- a/src/gui/windows/portable_service_install_window.cpp +++ b/src/gui/views/windows/portable_service_install_window.cpp @@ -1,4 +1,4 @@ -#include "render.h" +#include "application/gui_application.h" #if _WIN32 && CROSSDESK_PORTABLE @@ -45,11 +45,10 @@ bool InstallServiceWithElevation() { executable_path.parent_path() / L"crossdesk_session_helper.exe"; if (!std::filesystem::exists(service_path) || !std::filesystem::exists(helper_path)) { - LOG_ERROR( - "Portable service install failed: service binaries missing, " - "service={}, " - "helper={}", - service_path.string(), helper_path.string()); + LOG_ERROR("Portable service install failed: service binaries missing, " + "service={}, " + "helper={}", + service_path.string(), helper_path.string()); return false; } @@ -95,9 +94,9 @@ bool InstallServiceWithElevation() { return IsCrossDeskServiceInstalled() && started; } -} // namespace +} // namespace -void Render::CheckPortableWindowsService() { +void GuiApplication::CheckPortableWindowsService() { if (portable_service_prompt_checked_) { return; } @@ -116,7 +115,7 @@ void Render::CheckPortableWindowsService() { show_portable_service_install_window_ = true; } -void Render::StartPortableWindowsServiceInstall() { +void GuiApplication::StartPortableWindowsServiceInstall() { portable_service_do_not_remind_ = false; PortableServiceInstallState expected = PortableServiceInstallState::idle; if (!portable_service_install_state_.compare_exchange_strong( @@ -139,19 +138,19 @@ void Render::StartPortableWindowsServiceInstall() { }); } -void Render::JoinPortableWindowsServiceInstallThread() { +void GuiApplication::JoinPortableWindowsServiceInstallThread() { if (portable_service_install_thread_.joinable()) { portable_service_install_thread_.join(); } } -int Render::PortableServiceInstallWindow() { +int GuiApplication::PortableServiceInstallWindow() { if (!show_portable_service_install_window_ && !show_portable_service_prompt_suppressed_window_) { return 0; } - const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const ImGuiViewport *viewport = ImGui::GetMainViewport(); const float window_width = (std::min)(viewport->WorkSize.x * 0.6f, title_bar_button_width_ * 18.0f); const float window_height = @@ -162,10 +161,10 @@ int Render::PortableServiceInstallWindow() { const float notice_height = (std::min)(viewport->WorkSize.y * 0.35f, title_bar_button_width_ * 4.6f); ImGui::SetNextWindowPos( - ImVec2( - viewport->WorkPos.x + (viewport->WorkSize.x - notice_width) / 2.0f, - viewport->WorkPos.y + - (viewport->WorkSize.y - notice_height) / 2.0f), + ImVec2(viewport->WorkPos.x + + (viewport->WorkSize.x - notice_width) / 2.0f, + viewport->WorkPos.y + + (viewport->WorkSize.y - notice_height) / 2.0f), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(notice_width, notice_height), ImGuiCond_Always); @@ -199,7 +198,7 @@ int Render::PortableServiceInstallWindow() { ImGui::PopTextWrapPos(); const std::string ok_label = localization::ok[localization_language_index_]; - const ImGuiStyle& style = ImGui::GetStyle(); + const ImGuiStyle &style = ImGui::GetStyle(); const float ok_width = ImGui::CalcTextSize(ok_label.c_str()).x + style.FramePadding.x * 2.0f; ImGui::SetCursorPosX((notice_width - ok_width) * 0.5f); @@ -219,9 +218,9 @@ int Render::PortableServiceInstallWindow() { } ImGui::SetNextWindowPos( - ImVec2( - viewport->WorkPos.x + (viewport->WorkSize.x - window_width) / 2.0f, - viewport->WorkPos.y + (viewport->WorkSize.y - window_height) / 2.0f), + ImVec2(viewport->WorkPos.x + (viewport->WorkSize.x - window_width) / 2.0f, + viewport->WorkPos.y + + (viewport->WorkSize.y - window_height) / 2.0f), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGuiCond_Always); @@ -249,7 +248,7 @@ int Render::PortableServiceInstallWindow() { const PortableServiceInstallState state = portable_service_install_state_.load(std::memory_order_acquire); - const char* status_text = nullptr; + const char *status_text = nullptr; if (state == PortableServiceInstallState::installing || state == PortableServiceInstallState::succeeded || state == PortableServiceInstallState::failed) { @@ -297,8 +296,8 @@ int Render::PortableServiceInstallWindow() { ImGui::PopStyleVar(); const float button_y = window_height * 0.84f; - const ImGuiStyle& style = ImGui::GetStyle(); - const auto default_button_width = [&style](const std::string& label) { + const ImGuiStyle &style = ImGui::GetStyle(); + const auto default_button_width = [&style](const std::string &label) { return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f; }; const std::string install_label = @@ -355,6 +354,6 @@ int Render::PortableServiceInstallWindow() { return 0; } -} // namespace crossdesk +} // namespace crossdesk #endif diff --git a/src/gui/views/windows/request_permission_window.mm b/src/gui/views/windows/request_permission_window.mm new file mode 100644 index 0000000..05766d7 --- /dev/null +++ b/src/gui/views/windows/request_permission_window.mm @@ -0,0 +1,187 @@ +#include "application/gui_application.h" +#include "layout.h" +#include "localization.h" + +namespace crossdesk { + +bool GuiApplication::DrawToggleSwitch(const char *id, bool active, + bool enabled) { + const float TRACK_HEIGHT = ImGui::GetFrameHeight(); + const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f; + const float TRACK_RADIUS = TRACK_HEIGHT * 0.5f; + const float KNOB_PADDING = 2.0f; + const float KNOB_HEIGHT = TRACK_HEIGHT - 4.0f; + const float KNOB_WIDTH = KNOB_HEIGHT * 1.2f; + const float KNOB_RADIUS = KNOB_HEIGHT * 0.5f; + const float DISABLED_ALPHA = 0.6f; + const float KNOB_ALPHA_DISABLED = 0.9f; + + const ImVec4 COLOR_ACTIVE = ImVec4(0.0f, 0.0f, 1.0f, 1.0f); + const ImVec4 COLOR_ACTIVE_HOVER = ImVec4(0.26f, 0.59f, 0.98f, 1.0f); + const ImVec4 COLOR_INACTIVE = ImVec4(0.60f, 0.60f, 0.60f, 1.0f); + const ImVec4 COLOR_INACTIVE_HOVER = ImVec4(0.70f, 0.70f, 0.70f, 1.0f); + const ImVec4 COLOR_KNOB = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + + ImDrawList *draw_list = ImGui::GetWindowDrawList(); + ImVec2 track_pos = ImGui::GetCursorScreenPos(); + + ImGui::InvisibleButton(id, ImVec2(TRACK_WIDTH, TRACK_HEIGHT)); + bool hovered = ImGui::IsItemHovered(); + bool clicked = ImGui::IsItemClicked() && enabled; + + ImVec4 track_color = + active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE) + : (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE); + + if (!enabled) { + track_color.w *= DISABLED_ALPHA; + } + + ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f); + ImVec2 track_max = + ImVec2(track_pos.x + TRACK_WIDTH, track_pos.y + TRACK_HEIGHT - 0.5f); + draw_list->AddRectFilled(track_min, track_max, + ImGui::GetColorU32(track_color), TRACK_RADIUS); + + float knob_position = active ? 1.0f : 0.0f; + float knob_min_x = track_pos.x + KNOB_PADDING; + float knob_max_x = track_pos.x + TRACK_WIDTH - KNOB_WIDTH - KNOB_PADDING; + float knob_x = knob_min_x + knob_position * (knob_max_x - knob_min_x); + float knob_y = track_pos.y + (TRACK_HEIGHT - KNOB_HEIGHT) * 0.5f; + + ImVec4 knob_color = COLOR_KNOB; + if (!enabled) { + knob_color.w = KNOB_ALPHA_DISABLED; + } + + ImVec2 knob_min = ImVec2(knob_x, knob_y); + ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT); + draw_list->AddRectFilled(knob_min, knob_max, ImGui::GetColorU32(knob_color), + KNOB_RADIUS); + + return clicked; +} + +int GuiApplication::RequestPermissionWindow() { + RefreshMacPermissionStatus(false); + + const bool screen_recording_granted = + mac_screen_recording_permission_granted_; + const bool accessibility_granted = mac_accessibility_permission_granted_; + + show_request_permission_window_ = + !screen_recording_granted || !accessibility_granted; + + if (!show_request_permission_window_) { + return 0; + } + + const ImGuiViewport *viewport = ImGui::GetMainViewport(); + float window_width = localization_language_index_ == 0 + ? REQUEST_PERMISSION_WINDOW_WIDTH_CN + : REQUEST_PERMISSION_WINDOW_WIDTH_EN; + float window_height = localization_language_index_ == 0 + ? REQUEST_PERMISSION_WINDOW_HEIGHT_CN + : REQUEST_PERMISSION_WINDOW_HEIGHT_EN; + + float checkbox_padding = localization_language_index_ == 0 + ? REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN + : REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN; + + ImVec2 center_pos = ImVec2( + (viewport->WorkSize.x - window_width) * 0.5f + viewport->WorkPos.x, + (viewport->WorkSize.y - window_height) * 0.5f + viewport->WorkPos.y); + ImGui::SetNextWindowPos(center_pos, ImGuiCond_Once); + + ImGui::SetNextWindowSize(ImVec2(window_width, window_height), + ImGuiCond_Always); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + + ImGui::Begin( + localization::request_permissions[localization_language_index_].c_str(), + nullptr, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoSavedSettings); + + ImGui::SetWindowFontScale(0.3f); + + // use system font + if (main_windows_system_chinese_font_ != nullptr) { + ImGui::PushFont(main_windows_system_chinese_font_); + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetTextLineHeight() + + 5.0f); + ImGui::SetCursorPosX(10.0f); + ImGui::TextWrapped( + "%s", + localization::permission_required_message[localization_language_index_] + .c_str()); + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Spacing(); + + // accessibility permission + ImGui::SetCursorPosX(10.0f); + ImGui::AlignTextToFramePadding(); + ImGui::Text( + "1. %s:", + localization::accessibility_permission[localization_language_index_] + .c_str()); + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + ImGui::SetCursorPosX(checkbox_padding); + if (accessibility_granted) { + DrawToggleSwitch("accessibility_toggle_on", true, false); + } else { + if (DrawToggleSwitch("accessibility_toggle", false, true)) { + OpenAccessibilityPreferences(); + mac_accessibility_permission_requested_ = true; + RefreshMacPermissionStatus(true); + } + } + + ImGui::Spacing(); + + // screen recording permission + ImGui::SetCursorPosX(10.0f); + ImGui::AlignTextToFramePadding(); + ImGui::Text( + "2. %s:", + localization::screen_recording_permission[localization_language_index_] + .c_str()); + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + ImGui::SetCursorPosX(checkbox_padding); + if (screen_recording_granted) { + DrawToggleSwitch("screen_recording_toggle_on", true, false); + } else { + if (DrawToggleSwitch("screen_recording_toggle", false, true)) { + OpenScreenRecordingPreferences(); + mac_screen_recording_permission_requested_ = true; + RefreshMacPermissionStatus(true); + } + } + + ImGui::SetWindowFontScale(1.0f); + ImGui::SetWindowFontScale(0.45f); + + // pop system font + if (main_windows_system_chinese_font_ != nullptr) { + ImGui::PopFont(); + } + + ImGui::End(); + ImGui::SetWindowFontScale(1.0f); + ImGui::PopStyleVar(4); + ImGui::PopStyleColor(); + + return 0; +} +} // namespace crossdesk diff --git a/src/gui/windows/server_settings_window.cpp b/src/gui/views/windows/server_settings_window.cpp similarity index 97% rename from src/gui/windows/server_settings_window.cpp rename to src/gui/views/windows/server_settings_window.cpp index ba94ab8..2cb6732 100644 --- a/src/gui/windows/server_settings_window.cpp +++ b/src/gui/views/windows/server_settings_window.cpp @@ -5,10 +5,10 @@ #include #endif +#include "application/gui_application.h" #include "layout.h" #include "localization.h" #include "rd_log.h" -#include "render.h" namespace crossdesk { @@ -28,8 +28,8 @@ std::vector GetRootEntries() { return roots; } -int Render::SelfHostedServerWindow() { - ImGuiIO& io = ImGui::GetIO(); +int GuiApplication::SelfHostedServerWindow() { + ImGuiIO &io = ImGui::GetIO(); if (show_self_hosted_server_config_window_) { if (self_hosted_server_config_window_pos_reset_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { @@ -194,4 +194,4 @@ int Render::SelfHostedServerWindow() { return 0; } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk \ No newline at end of file diff --git a/src/gui/windows/server_window.cpp b/src/gui/views/windows/server_window.cpp similarity index 84% rename from src/gui/windows/server_window.cpp rename to src/gui/views/windows/server_window.cpp index db274e8..7d864f2 100644 --- a/src/gui/windows/server_window.cpp +++ b/src/gui/views/windows/server_window.cpp @@ -6,17 +6,18 @@ #include #include +#include "application/gui_application.h" #include "layout_relative.h" #include "localization.h" #include "rd_log.h" -#include "render.h" #include "rounded_corner_button.h" namespace crossdesk { namespace { int CountDigits(int number) { - if (number == 0) return 1; + if (number == 0) + return 1; return (int)std::floor(std::log10(std::abs(number))) + 1; } @@ -45,9 +46,9 @@ std::string FormatBytes(uint64_t bytes) { } return std::string(buf); } -} // namespace +} // namespace -int Render::ServerWindow() { +int GuiApplication::ServerWindow() { ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_), ImGuiCond_Always); ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); @@ -85,7 +86,7 @@ int Render::ServerWindow() { ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::SetWindowFontScale(0.5f); - const char* icon = + const char *icon = server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP; std::string toggle_label = std::string(icon) + "##server_toggle"; @@ -132,7 +133,7 @@ int Render::ServerWindow() { return 0; } -int Render::RemoteClientInfoWindow() { +int GuiApplication::RemoteClientInfoWindow() { float remote_client_info_window_width = server_window_width_ * 0.8f; float remote_client_info_window_height = (server_window_height_ - server_window_title_bar_height_) * 0.9f; @@ -154,7 +155,7 @@ int Render::RemoteClientInfoWindow() { { std::shared_lock lock(connection_status_mutex_); remote_entries.reserve(connection_status_.size()); - for (const auto& kv : connection_status_) { + for (const auto &kv : connection_status_) { const auto host_it = connection_host_names_.find(kv.first); const std::string display_name = (host_it != connection_host_names_.end() && !host_it->second.empty()) @@ -165,8 +166,8 @@ int Render::RemoteClientInfoWindow() { } auto find_display_name_by_remote_id = - [&remote_entries](const std::string& remote_id) -> std::string { - for (const auto& entry : remote_entries) { + [&remote_entries](const std::string &remote_id) -> std::string { + for (const auto &entry : remote_entries) { if (entry.first == remote_id) { return entry.second; } @@ -193,7 +194,7 @@ int Render::RemoteClientInfoWindow() { localization::controller[localization_language_index_].c_str()); ImGui::SameLine(); - const char* selected_preview = "-"; + const char *selected_preview = "-"; if (!selected_server_remote_hostname_.empty()) { selected_preview = selected_server_remote_hostname_.c_str(); } else if (!remote_client_id_.empty()) { @@ -240,36 +241,33 @@ int Render::RemoteClientInfoWindow() { ImGui::SameLine(); switch (status) { - case ConnectionStatus::Connected: - ImGui::Text( - "%s", - localization::p2p_connected[localization_language_index_].c_str()); - break; - case ConnectionStatus::Connecting: - ImGui::Text( - "%s", - localization::p2p_connecting[localization_language_index_].c_str()); - break; - case ConnectionStatus::Disconnected: - ImGui::Text("%s", - localization::p2p_disconnected[localization_language_index_] - .c_str()); - break; - case ConnectionStatus::Failed: - ImGui::Text( - "%s", - localization::p2p_failed[localization_language_index_].c_str()); - break; - case ConnectionStatus::Closed: - ImGui::Text( - "%s", - localization::p2p_closed[localization_language_index_].c_str()); - break; - default: - ImGui::Text( - "%s", - localization::p2p_failed[localization_language_index_].c_str()); - break; + case ConnectionStatus::Connected: + ImGui::Text( + "%s", + localization::p2p_connected[localization_language_index_].c_str()); + break; + case ConnectionStatus::Connecting: + ImGui::Text( + "%s", + localization::p2p_connecting[localization_language_index_].c_str()); + break; + case ConnectionStatus::Disconnected: + ImGui::Text( + "%s", + localization::p2p_disconnected[localization_language_index_].c_str()); + break; + case ConnectionStatus::Failed: + ImGui::Text( + "%s", localization::p2p_failed[localization_language_index_].c_str()); + break; + case ConnectionStatus::Closed: + ImGui::Text( + "%s", localization::p2p_closed[localization_language_index_].c_str()); + break; + default: + ImGui::Text( + "%s", localization::p2p_failed[localization_language_index_].c_str()); + break; } } @@ -287,16 +285,18 @@ int Render::RemoteClientInfoWindow() { std::string path = OpenFileDialog(title); LOG_INFO("Selected file path: {}", path.c_str()); - ProcessSelectedFile(path, nullptr, file_label_, selected_server_remote_id_); + transfers_.ProcessSelectedFile(path, nullptr, file_label_, + selected_server_remote_id_); } - if (file_transfer_.file_transfer_window_visible_) { + auto &file_transfer = transfers_.global_state(); + if (file_transfer.file_transfer_window_visible_) { ImGui::SameLine(); - const bool is_sending = file_transfer_.file_sending_.load(); + const bool is_sending = file_transfer.file_sending_.load(); if (is_sending) { // Simple animation: cycle icon every 0.5s while sending. - static const char* kFileTransferIcons[] = {ICON_FA_ANGLE_UP, + static const char *kFileTransferIcons[] = {ICON_FA_ANGLE_UP, ICON_FA_ANGLES_UP}; const int icon_index = static_cast(ImGui::GetTime() / 0.5) % (static_cast(sizeof(kFileTransferIcons) / @@ -308,9 +308,9 @@ int Render::RemoteClientInfoWindow() { } if (ImGui::IsItemHovered()) { - const uint64_t sent_bytes = file_transfer_.file_sent_bytes_.load(); - const uint64_t total_bytes = file_transfer_.file_total_bytes_.load(); - const uint32_t rate_bps = file_transfer_.file_send_rate_bps_.load(); + const uint64_t sent_bytes = file_transfer.file_sent_bytes_.load(); + const uint64_t total_bytes = file_transfer.file_total_bytes_.load(); + const uint32_t rate_bps = file_transfer.file_send_rate_bps_.load(); float progress = 0.0f; if (total_bytes > 0) { @@ -320,11 +320,11 @@ int Render::RemoteClientInfoWindow() { } std::string current_file_name; - const uint32_t current_file_id = file_transfer_.current_file_id_.load(); + const uint32_t current_file_id = file_transfer.current_file_id_.load(); if (current_file_id != 0) { std::lock_guard lock( - file_transfer_.file_transfer_list_mutex_); - for (const auto& info : file_transfer_.file_transfer_list_) { + file_transfer.file_transfer_list_mutex_); + for (const auto &info : file_transfer.file_transfer_list_) { if (info.file_id == current_file_id) { current_file_name = info.file_name; break; @@ -385,4 +385,4 @@ int Render::RemoteClientInfoWindow() { return 0; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/windows/stream_window.cpp b/src/gui/views/windows/stream_window.cpp similarity index 75% rename from src/gui/windows/stream_window.cpp rename to src/gui/views/windows/stream_window.cpp index 543b82f..fa6450b 100644 --- a/src/gui/windows/stream_window.cpp +++ b/src/gui/views/windows/stream_window.cpp @@ -1,24 +1,24 @@ +#include "application/gui_application.h" #include "localization.h" #include "rd_log.h" -#include "render.h" namespace crossdesk { -void Render::DrawConnectionStatusText( - std::shared_ptr& props) { +void GuiApplication::DrawConnectionStatusText( + std::shared_ptr &props) { std::string text; switch (props->connection_status_.load()) { - case ConnectionStatus::Disconnected: - text = localization::p2p_disconnected[localization_language_index_]; - break; - case ConnectionStatus::Failed: - text = localization::p2p_failed[localization_language_index_]; - break; - case ConnectionStatus::Closed: - text = localization::p2p_closed[localization_language_index_]; - break; - default: - break; + case ConnectionStatus::Disconnected: + text = localization::p2p_disconnected[localization_language_index_]; + break; + case ConnectionStatus::Failed: + text = localization::p2p_failed[localization_language_index_]; + break; + case ConnectionStatus::Closed: + text = localization::p2p_closed[localization_language_index_]; + break; + default: + break; } if (!text.empty()) { @@ -31,8 +31,8 @@ void Render::DrawConnectionStatusText( } } -void Render::DrawReceivingScreenText( - std::shared_ptr& props) { +void GuiApplication::DrawReceivingScreenText( + std::shared_ptr &props) { if (!props->connection_established_ || props->connection_status_.load() != ConnectionStatus::Connected) { return; @@ -50,7 +50,7 @@ void Render::DrawReceivingScreenText( return; } - const std::string& text = + const std::string &text = localization::receiving_screen[localization_language_index_]; ImVec2 size = ImGui::GetWindowSize(); ImVec2 text_size = ImGui::CalcTextSize(text.c_str()); @@ -59,12 +59,12 @@ void Render::DrawReceivingScreenText( ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.92f), "%s", text.c_str()); } -void Render::CloseTab(decltype(client_properties_)::iterator& it) { - // std::unique_lock lock(client_properties_mutex_); - if (it != client_properties_.end()) { - CleanupPeer(it->second); - it = client_properties_.erase(it); - if (client_properties_.empty()) { +void GuiApplication::CloseTab(decltype(remote_sessions_)::iterator &it) { + // std::unique_lock lock(remote_sessions_mutex_); + if (it != remote_sessions_.end()) { + CloseRemoteSession(it->second); + it = remote_sessions_.erase(it); + if (remote_sessions_.empty()) { SDL_Event event; event.type = SDL_EVENT_QUIT; SDL_PushEvent(&event); @@ -72,7 +72,7 @@ void Render::CloseTab(decltype(client_properties_)::iterator& it) { } } -int Render::StreamWindow() { +int GuiApplication::StreamWindow() { ImGui::SetNextWindowPos( ImVec2(0, fullscreen_button_pressed_ ? 0 : title_bar_height_), ImGuiCond_Always); @@ -117,22 +117,22 @@ int Render::StreamWindow() { ImGuiTabBarFlags_AutoSelectNewTabs)) { is_tab_bar_hovered_ = ImGui::IsWindowHovered(); - // std::shared_lock lock(client_properties_mutex_); - for (auto it = client_properties_.begin(); - it != client_properties_.end();) { - auto& props = it->second; + // std::shared_lock lock(remote_sessions_mutex_); + for (auto it = remote_sessions_.begin(); + it != remote_sessions_.end();) { + auto &props = it->second; if (!props->tab_opened_) { std::string remote_id_to_close = props->remote_id_; // lock.unlock(); { - // std::unique_lock unique_lock(client_properties_mutex_); - auto close_it = client_properties_.find(remote_id_to_close); - if (close_it != client_properties_.end()) { + // std::unique_lock unique_lock(remote_sessions_mutex_); + auto close_it = remote_sessions_.find(remote_id_to_close); + if (close_it != remote_sessions_.end()) { CloseTab(close_it); } } // lock.lock(); - it = client_properties_.begin(); + it = remote_sessions_.begin(); continue; } @@ -180,13 +180,13 @@ int Render::StreamWindow() { std::string remote_id_to_erase = props->remote_id_; // lock.unlock(); { - // std::unique_lock unique_lock(client_properties_mutex_); - auto erase_it = client_properties_.find(remote_id_to_erase); - if (erase_it != client_properties_.end()) { + // std::unique_lock unique_lock(remote_sessions_mutex_); + auto erase_it = remote_sessions_.find(remote_id_to_erase); + if (erase_it != remote_sessions_.end()) { // Ensure we flush pending STREAM_REFRESH_EVENT events and // clean up peer resources before erasing the entry, otherwise // SDL events may still hold raw pointers to freed - // SubStreamWindowProperties (including video_frame_mutex_), + // RemoteSession (including video_frame_mutex_), // leading to std::system_error when locking. CloseTab(erase_it); } @@ -194,7 +194,7 @@ int Render::StreamWindow() { // lock.lock(); ImGui::End(); ImGui::EndTabItem(); - it = client_properties_.begin(); + it = remote_sessions_.begin(); continue; } else { DrawConnectionStatusText(props); @@ -209,14 +209,14 @@ int Render::StreamWindow() { std::string remote_id_to_close = props->remote_id_; // lock.unlock(); { - // std::unique_lock unique_lock(client_properties_mutex_); - auto close_it = client_properties_.find(remote_id_to_close); - if (close_it != client_properties_.end()) { + // std::unique_lock unique_lock(remote_sessions_mutex_); + auto close_it = remote_sessions_.find(remote_id_to_close); + if (close_it != remote_sessions_.end()) { CloseTab(close_it); } } // lock.lock(); - it = client_properties_.begin(); + it = remote_sessions_.begin(); continue; } ++it; @@ -226,24 +226,24 @@ int Render::StreamWindow() { ImGui::EndTabBar(); } - ImGui::End(); // End TabBar + ImGui::End(); // End TabBar } else { - // std::shared_lock lock(client_properties_mutex_); - for (auto it = client_properties_.begin(); - it != client_properties_.end();) { - auto& props = it->second; + // std::shared_lock lock(remote_sessions_mutex_); + for (auto it = remote_sessions_.begin(); + it != remote_sessions_.end();) { + auto &props = it->second; if (!props->tab_opened_) { std::string remote_id_to_close = props->remote_id_; // lock.unlock(); { - // std::unique_lock unique_lock(client_properties_mutex_); - auto close_it = client_properties_.find(remote_id_to_close); - if (close_it != client_properties_.end()) { + // std::unique_lock unique_lock(remote_sessions_mutex_); + auto close_it = remote_sessions_.find(remote_id_to_close); + if (close_it != remote_sessions_.end()) { CloseTab(close_it); } } // lock.lock(); - it = client_properties_.begin(); + it = remote_sessions_.begin(); continue; } @@ -284,14 +284,14 @@ int Render::StreamWindow() { std::string remote_id_to_erase = props->remote_id_; // lock.unlock(); { - // std::unique_lock unique_lock(client_properties_mutex_); - auto erase_it = client_properties_.find(remote_id_to_erase); - if (erase_it != client_properties_.end()) { + // std::unique_lock unique_lock(remote_sessions_mutex_); + auto erase_it = remote_sessions_.find(remote_id_to_erase); + if (erase_it != remote_sessions_.end()) { CloseTab(erase_it); } } // lock.lock(); - it = client_properties_.begin(); + it = remote_sessions_.begin(); continue; } else { DrawConnectionStatusText(props); @@ -304,8 +304,8 @@ int Render::StreamWindow() { } // UpdateRenderRect(); - ImGui::End(); // End VideoBg + ImGui::End(); // End VideoBg return 0; } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk \ No newline at end of file diff --git a/src/gui/windows/update_notification_window.cpp b/src/gui/views/windows/update_notification_window.cpp similarity index 95% rename from src/gui/windows/update_notification_window.cpp rename to src/gui/views/windows/update_notification_window.cpp index 8d84757..17c841d 100644 --- a/src/gui/windows/update_notification_window.cpp +++ b/src/gui/views/windows/update_notification_window.cpp @@ -1,14 +1,14 @@ #include #include +#include "application/gui_application.h" #include "layout.h" #include "localization.h" #include "rd_log.h" -#include "render.h" namespace crossdesk { -std::string CleanMarkdown(const std::string& markdown) { +std::string CleanMarkdown(const std::string &markdown) { std::string result = markdown; // remove # title mark @@ -44,15 +44,15 @@ std::string CleanMarkdown(const std::string& markdown) { pos = 0; while ((pos = result.find('.', pos)) != std::string::npos) { result.replace(pos, 1, "、"); - pos += 1; // Move to next position after the replacement + pos += 1; // Move to next position after the replacement } return result; } -int Render::UpdateNotificationWindow() { +int GuiApplication::UpdateNotificationWindow() { if (show_update_notification_window_ && update_available_) { - const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const ImGuiViewport *viewport = ImGui::GetMainViewport(); float update_notification_window_width = title_bar_button_width_ * 10.0f; float update_notification_window_height = title_bar_button_width_ * 8.0f; @@ -204,4 +204,4 @@ int Render::UpdateNotificationWindow() { return 0; } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/windows/request_permission_window.mm b/src/gui/windows/request_permission_window.mm deleted file mode 100644 index 6792c46..0000000 --- a/src/gui/windows/request_permission_window.mm +++ /dev/null @@ -1,280 +0,0 @@ -#include "layout.h" -#include "localization.h" -#include "rd_log.h" -#include "render.h" - -#include -#include -#import -#include -#include - -namespace crossdesk { - -namespace { -constexpr uint32_t kPermissionRefreshIntervalVisibleMs = 500; - -void OpenPrivacyPreferences(const char* pane) { - if (pane == nullptr || pane[0] == '\0') { - return; - } - - std::string command = - "open \"x-apple.systempreferences:com.apple.preference.security?"; - command += pane; - command += "\""; - system(command.c_str()); -} -} // namespace - -bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) { - const float TRACK_HEIGHT = ImGui::GetFrameHeight(); - const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f; - const float TRACK_RADIUS = TRACK_HEIGHT * 0.5f; - const float KNOB_PADDING = 2.0f; - const float KNOB_HEIGHT = TRACK_HEIGHT - 4.0f; - const float KNOB_WIDTH = KNOB_HEIGHT * 1.2f; - const float KNOB_RADIUS = KNOB_HEIGHT * 0.5f; - const float DISABLED_ALPHA = 0.6f; - const float KNOB_ALPHA_DISABLED = 0.9f; - - const ImVec4 COLOR_ACTIVE = ImVec4(0.0f, 0.0f, 1.0f, 1.0f); - const ImVec4 COLOR_ACTIVE_HOVER = ImVec4(0.26f, 0.59f, 0.98f, 1.0f); - const ImVec4 COLOR_INACTIVE = ImVec4(0.60f, 0.60f, 0.60f, 1.0f); - const ImVec4 COLOR_INACTIVE_HOVER = ImVec4(0.70f, 0.70f, 0.70f, 1.0f); - const ImVec4 COLOR_KNOB = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - ImVec2 track_pos = ImGui::GetCursorScreenPos(); - - ImGui::InvisibleButton(id, ImVec2(TRACK_WIDTH, TRACK_HEIGHT)); - bool hovered = ImGui::IsItemHovered(); - bool clicked = ImGui::IsItemClicked() && enabled; - - ImVec4 track_color = - active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE) - : (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE); - - if (!enabled) { - track_color.w *= DISABLED_ALPHA; - } - - ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f); - ImVec2 track_max = ImVec2(track_pos.x + TRACK_WIDTH, - track_pos.y + TRACK_HEIGHT - 0.5f); - draw_list->AddRectFilled(track_min, track_max, - ImGui::GetColorU32(track_color), TRACK_RADIUS); - - float knob_position = active ? 1.0f : 0.0f; - float knob_min_x = track_pos.x + KNOB_PADDING; - float knob_max_x = track_pos.x + TRACK_WIDTH - KNOB_WIDTH - KNOB_PADDING; - float knob_x = knob_min_x + knob_position * (knob_max_x - knob_min_x); - float knob_y = track_pos.y + (TRACK_HEIGHT - KNOB_HEIGHT) * 0.5f; - - ImVec4 knob_color = COLOR_KNOB; - if (!enabled) { - knob_color.w = KNOB_ALPHA_DISABLED; - } - - ImVec2 knob_min = ImVec2(knob_x, knob_y); - ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT); - draw_list->AddRectFilled(knob_min, knob_max, - ImGui::GetColorU32(knob_color), KNOB_RADIUS); - - return clicked; -} - -bool Render::CheckScreenRecordingPermission() { - // CGPreflightScreenCaptureAccess is available on macOS 10.15+ - if (@available(macOS 10.15, *)) { - bool granted = CGPreflightScreenCaptureAccess(); - return granted; - } - // for older macOS versions, assume permission is granted - return true; -} - -bool Render::CheckAccessibilityPermission() { - NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @NO}; - bool trusted = AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); - return trusted; -} - -void Render::OpenAccessibilityPreferences() { - if (!mac_accessibility_permission_requested_) { - NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES}; - AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); - } else { - OpenPrivacyPreferences("Privacy_Accessibility"); - } -} - -void Render::OpenScreenRecordingPreferences() { - if (@available(macOS 10.15, *)) { - if (!mac_screen_recording_permission_requested_) { - CGRequestScreenCaptureAccess(); - } else { - OpenPrivacyPreferences("Privacy_ScreenCapture"); - } - } else { - OpenPrivacyPreferences("Privacy_ScreenCapture"); - } -} - -void Render::RefreshMacPermissionStatus(bool force) { - const uint32_t now = static_cast(SDL_GetTicks()); - if (!force && mac_permission_status_initialized_ && - now - mac_permission_last_check_tick_ < - kPermissionRefreshIntervalVisibleMs) { - return; - } - - const bool old_screen_recording_granted = - mac_screen_recording_permission_granted_; - const bool old_accessibility_granted = mac_accessibility_permission_granted_; - - mac_screen_recording_permission_granted_ = - CheckScreenRecordingPermission(); - mac_accessibility_permission_granted_ = CheckAccessibilityPermission(); - mac_permission_last_check_tick_ = now; - mac_permission_status_initialized_ = true; - - if (old_screen_recording_granted != - mac_screen_recording_permission_granted_ || - old_accessibility_granted != mac_accessibility_permission_granted_) { - LOG_INFO("macOS permission status: screen_recording={}, accessibility={}", - mac_screen_recording_permission_granted_, - mac_accessibility_permission_granted_); - } -} - -bool Render::EnsureMacScreenRecordingPermission() { - RefreshMacPermissionStatus(false); - if (mac_screen_recording_permission_granted_) { - return true; - } - - show_request_permission_window_ = true; - return false; -} - -bool Render::EnsureMacAccessibilityPermission() { - RefreshMacPermissionStatus(false); - if (mac_accessibility_permission_granted_) { - return true; - } - - show_request_permission_window_ = true; - return false; -} - -int Render::RequestPermissionWindow() { - RefreshMacPermissionStatus(false); - - const bool screen_recording_granted = - mac_screen_recording_permission_granted_; - const bool accessibility_granted = mac_accessibility_permission_granted_; - - show_request_permission_window_ = - !screen_recording_granted || !accessibility_granted; - - if (!show_request_permission_window_) { - return 0; - } - - const ImGuiViewport* viewport = ImGui::GetMainViewport(); - float window_width = localization_language_index_ == 0 ? REQUEST_PERMISSION_WINDOW_WIDTH_CN - : REQUEST_PERMISSION_WINDOW_WIDTH_EN; - float window_height = localization_language_index_ == 0 ? REQUEST_PERMISSION_WINDOW_HEIGHT_CN - : REQUEST_PERMISSION_WINDOW_HEIGHT_EN; - - float checkbox_padding = localization_language_index_ == 0 - ? REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN - : REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN; - - ImVec2 center_pos = ImVec2((viewport->WorkSize.x - window_width) * 0.5f + viewport->WorkPos.x, - (viewport->WorkSize.y - window_height) * 0.5f + viewport->WorkPos.y); - ImGui::SetNextWindowPos(center_pos, ImGuiCond_Once); - - ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGuiCond_Always); - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - - ImGui::Begin( - localization::request_permissions[localization_language_index_].c_str(), nullptr, - ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings); - - ImGui::SetWindowFontScale(0.3f); - - // use system font - if (main_windows_system_chinese_font_ != nullptr) { - ImGui::PushFont(main_windows_system_chinese_font_); - } - - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetTextLineHeight() + 5.0f); - ImGui::SetCursorPosX(10.0f); - ImGui::TextWrapped( - "%s", localization::permission_required_message[localization_language_index_].c_str()); - - ImGui::Spacing(); - ImGui::Spacing(); - ImGui::Spacing(); - - // accessibility permission - ImGui::SetCursorPosX(10.0f); - ImGui::AlignTextToFramePadding(); - ImGui::Text("1. %s:", - localization::accessibility_permission[localization_language_index_].c_str()); - ImGui::SameLine(); - ImGui::AlignTextToFramePadding(); - ImGui::SetCursorPosX(checkbox_padding); - if (accessibility_granted) { - DrawToggleSwitch("accessibility_toggle_on", true, false); - } else { - if (DrawToggleSwitch("accessibility_toggle", false, true)) { - OpenAccessibilityPreferences(); - mac_accessibility_permission_requested_ = true; - RefreshMacPermissionStatus(true); - } - } - - ImGui::Spacing(); - - // screen recording permission - ImGui::SetCursorPosX(10.0f); - ImGui::AlignTextToFramePadding(); - ImGui::Text("2. %s:", - localization::screen_recording_permission[localization_language_index_].c_str()); - ImGui::SameLine(); - ImGui::AlignTextToFramePadding(); - ImGui::SetCursorPosX(checkbox_padding); - if (screen_recording_granted) { - DrawToggleSwitch("screen_recording_toggle_on", true, false); - } else { - if (DrawToggleSwitch("screen_recording_toggle", false, true)) { - OpenScreenRecordingPreferences(); - mac_screen_recording_permission_requested_ = true; - RefreshMacPermissionStatus(true); - } - } - - ImGui::SetWindowFontScale(1.0f); - ImGui::SetWindowFontScale(0.45f); - - // pop system font - if (main_windows_system_chinese_font_ != nullptr) { - ImGui::PopFont(); - } - - ImGui::End(); - ImGui::SetWindowFontScale(1.0f); - ImGui::PopStyleVar(4); - ImGui::PopStyleColor(); - - return 0; -} -} // namespace crossdesk diff --git a/tests/connection_status_protocol_test.cpp b/tests/connection_status_protocol_test.cpp index ebff554..90b001f 100644 --- a/tests/connection_status_protocol_test.cpp +++ b/tests/connection_status_protocol_test.cpp @@ -10,7 +10,8 @@ std::filesystem::path FindRepoRoot() { std::filesystem::path current = std::filesystem::current_path(); while (!current.empty()) { if (std::filesystem::exists(current / "xmake.lua") && - std::filesystem::exists(current / "submodules/minirtc/src/api/minirtc.h")) { + std::filesystem::exists(current / + "submodules/minirtc/src/api/minirtc.h")) { return current; } current = current.parent_path(); @@ -18,7 +19,7 @@ std::filesystem::path FindRepoRoot() { return {}; } -std::string ReadFile(const std::filesystem::path& path) { +std::string ReadFile(const std::filesystem::path &path) { std::ifstream file(path, std::ios::binary); if (!file) { return {}; @@ -29,8 +30,8 @@ std::string ReadFile(const std::filesystem::path& path) { return stream.str(); } -bool ExpectContains(const char* name, const std::string& value, - const std::string& expected) { +bool ExpectContains(const char *name, const std::string &value, + const std::string &expected) { if (value.find(expected) != std::string::npos) { return true; } @@ -39,8 +40,8 @@ bool ExpectContains(const char* name, const std::string& value, return false; } -bool ExpectNotContains(const char* name, const std::string& value, - const std::string& unexpected) { +bool ExpectNotContains(const char *name, const std::string &value, + const std::string &unexpected) { if (value.find(unexpected) == std::string::npos) { return true; } @@ -62,12 +63,14 @@ int main() { ReadFile(repo_root / "submodules/minirtc/src/api/minirtc.h"); const std::string peer_connection = ReadFile(repo_root / "submodules/minirtc/src/pc/peer_connection.cpp"); - const std::string connection_status_window = - ReadFile(repo_root / "src/gui/windows/connection_status_window.cpp"); - const std::string render_h = ReadFile(repo_root / "src/gui/render.h"); - const std::string render_cpp = ReadFile(repo_root / "src/gui/render.cpp"); - const std::string remote_peer_panel = - ReadFile(repo_root / "src/gui/panels/remote_peer_panel.cpp"); + const std::string connection_status_window = ReadFile( + repo_root / "src/gui/views/windows/connection_status_window.cpp"); + const std::string runtime_state_h = + ReadFile(repo_root / "src/gui/runtime/runtime_state.h"); + const std::string remote_session_h = + ReadFile(repo_root / "src/gui/runtime/remote_session.h"); + const std::string connection_runtime_cpp = + ReadFile(repo_root / "src/gui/runtime/connection_runtime.cpp"); bool ok = true; ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable"); @@ -80,20 +83,19 @@ int main() { "\"Device offline\""); ok &= ExpectNotContains("peer_connection.cpp", peer_connection, "ConnectionStatus::DeviceOffline"); - ok &= ExpectContains("connection_status_window.cpp", - connection_status_window, + ok &= ExpectContains("connection_status_window.cpp", connection_status_window, "localization::device_offline"); - ok &= ExpectContains("render.h", render_h, + ok &= ExpectContains("runtime_state.h", runtime_state_h, "pending_presence_probe_started_at_"); - ok &= ExpectContains("render.h", render_h, + ok &= ExpectContains("remote_session.h", remote_session_h, "connection_attempt_started_at_"); - ok &= ExpectContains("render.cpp", render_cpp, + ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, "HandleConnectionTimeouts"); - ok &= ExpectContains("render.cpp", render_cpp, + ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, "kPresenceProbeTimeout"); - ok &= ExpectContains("render.cpp", render_cpp, + ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, "kConnectionAttemptTimeout"); - ok &= ExpectContains("remote_peer_panel.cpp", remote_peer_panel, + ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, "connection_attempt_started_at_"); return ok ? 0 : 1; diff --git a/tests/display_popup_hover_state_test.cpp b/tests/display_popup_hover_state_test.cpp index e14b424..57d828c 100644 --- a/tests/display_popup_hover_state_test.cpp +++ b/tests/display_popup_hover_state_test.cpp @@ -10,7 +10,8 @@ std::filesystem::path FindRepoRoot() { std::filesystem::path current = std::filesystem::current_path(); while (!current.empty()) { if (std::filesystem::exists(current / "xmake.lua") && - std::filesystem::exists(current / "src/gui/toolbars/control_bar.cpp")) { + std::filesystem::exists(current / + "src/gui/views/toolbars/control_bar.cpp")) { return current; } current = current.parent_path(); @@ -63,8 +64,7 @@ bool ExpectContainsAtLeast(const char* name, const std::string& value, } std::cerr << name << " expected at least " << min_count - << " occurrences of: " << expected << ", found " << count - << "\n"; + << " occurrences of: " << expected << ", found " << count << "\n"; return false; } @@ -110,7 +110,7 @@ int main() { } const std::string control_bar = - ReadFile(repo_root / "src/gui/toolbars/control_bar.cpp"); + ReadFile(repo_root / "src/gui/views/toolbars/control_bar.cpp"); bool ok = true; ok &= ExpectContains("control_bar.cpp", control_bar, diff --git a/tests/keyboard_state_protocol_test.cpp b/tests/keyboard_state_protocol_test.cpp index 8c7efd5..8c5e780 100644 --- a/tests/keyboard_state_protocol_test.cpp +++ b/tests/keyboard_state_protocol_test.cpp @@ -1,8 +1,8 @@ -#include "device_controller.h" - #include #include +#include "device_controller.h" + namespace { bool ExpectEqual(const char* name, size_t actual, size_t expected) { @@ -60,10 +60,8 @@ int main() { ok &= ExpectEqual("parsed pressed_count", parsed.ks.pressed_count, 2); ok &= ExpectEqual("parsed key 0", parsed.ks.pressed_keys[0].key_value, 65); ok &= ExpectEqual("parsed scan 0", parsed.ks.pressed_keys[0].scan_code, 30); - ok &= ExpectTrue("parsed extended 0", - !parsed.ks.pressed_keys[0].extended); - ok &= ExpectEqual("parsed key 1", parsed.ks.pressed_keys[1].key_value, - 0xA3); + ok &= ExpectTrue("parsed extended 0", !parsed.ks.pressed_keys[0].extended); + ok &= ExpectEqual("parsed key 1", parsed.ks.pressed_keys[1].key_value, 0xA3); ok &= ExpectEqual("parsed scan 1", parsed.ks.pressed_keys[1].scan_code, 29); ok &= ExpectTrue("parsed extended 1", parsed.ks.pressed_keys[1].extended); diff --git a/tests/macos_keyboard_modifier_state_test.cpp b/tests/macos_keyboard_modifier_state_test.cpp index 357438f..cd6d7e6 100644 --- a/tests/macos_keyboard_modifier_state_test.cpp +++ b/tests/macos_keyboard_modifier_state_test.cpp @@ -25,18 +25,17 @@ int main() { ok &= ExpectEqual("initial flags", state.flags(), 0); ok &= ExpectEqual("left shift down", state.Update(0xA0, true), crossdesk::kMacInjectedModifierShift); - ok &= ExpectEqual("shifted semicolon keeps shift", - state.Update(0xBA, true), + ok &= ExpectEqual("shifted semicolon keeps shift", state.Update(0xBA, true), crossdesk::kMacInjectedModifierShift); ok &= ExpectEqual("semicolon up keeps shift", state.Update(0xBA, false), crossdesk::kMacInjectedModifierShift); - ok &= ExpectEqual("right shift down while left held", - state.Update(0xA1, true), - crossdesk::kMacInjectedModifierShift); + ok &= + ExpectEqual("right shift down while left held", state.Update(0xA1, true), + crossdesk::kMacInjectedModifierShift); ok &= ExpectEqual("left shift up while right held", state.Update(0xA0, false), crossdesk::kMacInjectedModifierShift); - ok &= ExpectEqual("right shift up clears shift", state.Update(0xA1, false), - 0); + ok &= + ExpectEqual("right shift up clears shift", state.Update(0xA1, false), 0); ok &= ExpectEqual("left control down", state.Update(0xA2, true), crossdesk::kMacInjectedModifierControl); @@ -53,8 +52,7 @@ int main() { crossdesk::kMacInjectedModifierCommand); ok &= ExpectEqual("right alt up leaves command", state.Update(0xA5, false), crossdesk::kMacInjectedModifierCommand); - ok &= ExpectEqual("left command up clears all", state.Update(0x5B, false), - 0); + ok &= ExpectEqual("left command up clears all", state.Update(0x5B, false), 0); return ok ? 0 : 1; } diff --git a/tests/path_manager_portable_test.cpp b/tests/path_manager_portable_test.cpp index 6689a01..cf5286f 100644 --- a/tests/path_manager_portable_test.cpp +++ b/tests/path_manager_portable_test.cpp @@ -1,15 +1,15 @@ -#include "path_manager.h" - #include #include #include #include +#include "path_manager.h" + #ifdef _WIN32 #include #elif defined(__APPLE__) -#include #include +#include #else #include #include @@ -43,8 +43,7 @@ std::filesystem::path GetExecutableDirectory() { #endif } -bool ExpectEqual(const char* name, - const std::filesystem::path& actual, +bool ExpectEqual(const char* name, const std::filesystem::path& actual, const std::filesystem::path& expected) { if (actual.lexically_normal() == expected.lexically_normal()) { return true; diff --git a/tests/windows_manifest_resource_test.cpp b/tests/windows_manifest_resource_test.cpp index 3caeea8..d914db9 100644 --- a/tests/windows_manifest_resource_test.cpp +++ b/tests/windows_manifest_resource_test.cpp @@ -130,11 +130,12 @@ int main() { ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest, "processorArchitecture=\"*\""); #ifdef _WIN32 - ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest"); - ok &= ExpectActivationContext( - repo_root / "scripts/windows/crossdesk_debug.manifest"); - ok &= ExpectActivationContext( - repo_root / "scripts/windows/crossdesk_portable.manifest"); + ok &= + ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest"); + ok &= ExpectActivationContext(repo_root / + "scripts/windows/crossdesk_debug.manifest"); + ok &= ExpectActivationContext(repo_root / + "scripts/windows/crossdesk_portable.manifest"); #endif return ok ? 0 : 1; } diff --git a/tests/windows_mouse_controller_safety_test.cpp b/tests/windows_mouse_controller_safety_test.cpp index 6a9e9f5..b98d6e5 100644 --- a/tests/windows_mouse_controller_safety_test.cpp +++ b/tests/windows_mouse_controller_safety_test.cpp @@ -11,7 +11,8 @@ std::filesystem::path FindRepoRoot() { while (!current.empty()) { if (std::filesystem::exists(current / "xmake.lua") && std::filesystem::exists( - current / "src/device_controller/mouse/windows/mouse_controller.cpp")) { + current / + "src/device_controller/mouse/windows/mouse_controller.cpp")) { return current; } current = current.parent_path(); diff --git a/tests/windows_sas_guard_test.cpp b/tests/windows_sas_guard_test.cpp index 3223d16..e63456f 100644 --- a/tests/windows_sas_guard_test.cpp +++ b/tests/windows_sas_guard_test.cpp @@ -12,8 +12,8 @@ std::filesystem::path FindRepoRoot() { std::filesystem::path current = std::filesystem::current_path(); while (!current.empty()) { if (std::filesystem::exists(current / "xmake.lua") && - std::filesystem::exists( - current / "src/service/windows/service_host.cpp")) { + std::filesystem::exists(current / + "src/service/windows/service_host.cpp")) { return current; } current = current.parent_path(); @@ -21,7 +21,7 @@ std::filesystem::path FindRepoRoot() { return {}; } -std::string ReadFile(const std::filesystem::path& path) { +std::string ReadFile(const std::filesystem::path &path) { std::ifstream file(path, std::ios::binary); if (!file) { return {}; @@ -32,8 +32,8 @@ std::string ReadFile(const std::filesystem::path& path) { return stream.str(); } -bool ExpectContains(const char* name, const std::string& value, - const std::string& expected) { +bool ExpectContains(const char *name, const std::string &value, + const std::string &expected) { if (value.find(expected) != std::string::npos) { return true; } @@ -42,8 +42,8 @@ bool ExpectContains(const char* name, const std::string& value, return false; } -bool ExpectNotContains(const char* name, const std::string& value, - const std::string& unexpected) { +bool ExpectNotContains(const char *name, const std::string &value, + const std::string &unexpected) { if (value.find(unexpected) == std::string::npos) { return true; } @@ -52,7 +52,7 @@ bool ExpectNotContains(const char* name, const std::string& value, return false; } -bool ExpectTrue(const char* name, bool value) { +bool ExpectTrue(const char *name, bool value) { if (value) { return true; } @@ -71,9 +71,11 @@ int main() { } const std::string control_bar = - ReadFile(repo_root / "src/gui/toolbars/control_bar.cpp"); - const std::string render = ReadFile(repo_root / "src/gui/render.cpp"); - const std::string render_h = ReadFile(repo_root / "src/gui/render.h"); + ReadFile(repo_root / "src/gui/views/toolbars/control_bar.cpp"); + const std::string windows_service_runtime = + ReadFile(repo_root / "src/gui/runtime/windows_service_runtime.cpp"); + const std::string runtime_state_h = + ReadFile(repo_root / "src/gui/runtime/runtime_state.h"); const std::string service_host = ReadFile(repo_root / "src/service/windows/service_host.cpp"); const std::string service_host_h = @@ -82,9 +84,9 @@ int main() { ReadFile(repo_root / "src/service/windows/session_helper_main.cpp"); bool ok = true; - ok &= ExpectTrue("secure desktop input routing", - crossdesk::IsSecureDesktopInteractionRequired( - "secure-desktop")); + ok &= ExpectTrue( + "secure desktop input routing", + crossdesk::IsSecureDesktopInteractionRequired("secure-desktop")); ok &= ExpectNotContains("control_bar.cpp", control_bar, "CanSendSecureAttentionSequence(" "props->remote_interactive_stage_)"); @@ -92,18 +94,20 @@ int main() { "ImGui::BeginDisabled();\n" " }\n" " if (ImGui::Selectable(sas_label.c_str()))"); - ok &= ExpectNotContains("render.cpp", render, "sas_requires_lock_screen"); - ok &= ExpectContains("render.h", render_h, + ok &= ExpectNotContains("windows_service_runtime.cpp", + windows_service_runtime, "sas_requires_lock_screen"); + ok &= ExpectContains("runtime_state.h", runtime_state_h, "optimistic_windows_secure_desktop_until_tick_"); - ok &= ExpectContains("render.cpp", render, + ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime, "kWindowsServiceSasSecureDesktopGraceMs"); - ok &= ExpectContains("render.cpp", render, + ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime, "status->sas_secure_desktop_grace_active"); - ok &= ExpectContains("render.cpp", render, - "json.value(\"sas_secure_desktop_grace_active\", false)"); - ok &= ExpectContains("render.cpp", render, + ok &= + ExpectContains("windows_service_runtime.cpp", windows_service_runtime, + "json.value(\"sas_secure_desktop_grace_active\", false)"); + ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime, "status.sas_secure_desktop_grace_active"); - ok &= ExpectContains("render.cpp", render, + ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime, "local_interactive_stage_ = \"secure-desktop\""); ok &= ExpectContains("service_host.h", service_host_h, "sas_secure_desktop_until_tick_"); @@ -126,8 +130,9 @@ int main() { "now + kSasSecureDesktopGraceMs"); ok &= ExpectContains("service_host.cpp", service_host, "\\\"sas_secure_desktop_grace_active\\\""); - ok &= ExpectContains("service_host.cpp", service_host, - "raw_interactive_stage = ResolveInteractiveStageLocked()"); + ok &= + ExpectContains("service_host.cpp", service_host, + "raw_interactive_stage = ResolveInteractiveStageLocked()"); ok &= ExpectContains("session_helper_main.cpp", session_helper, "kSessionHelperStatePollMs = 1000"); ok &= ExpectContains("session_helper_main.cpp", session_helper, diff --git a/tests/windows_service_mouse_ipc_test.cpp b/tests/windows_service_mouse_ipc_test.cpp index b846986..3269051 100644 --- a/tests/windows_service_mouse_ipc_test.cpp +++ b/tests/windows_service_mouse_ipc_test.cpp @@ -10,7 +10,8 @@ std::filesystem::path FindRepoRoot() { std::filesystem::path current = std::filesystem::current_path(); while (!current.empty()) { if (std::filesystem::exists(current / "xmake.lua") && - std::filesystem::exists(current / "src/service/windows/service_host.cpp")) { + std::filesystem::exists(current / + "src/service/windows/service_host.cpp")) { return current; } current = current.parent_path(); @@ -18,7 +19,7 @@ std::filesystem::path FindRepoRoot() { return {}; } -std::string ReadFile(const std::filesystem::path& path) { +std::string ReadFile(const std::filesystem::path &path) { std::ifstream file(path, std::ios::binary); if (!file) { return {}; @@ -29,8 +30,8 @@ std::string ReadFile(const std::filesystem::path& path) { return stream.str(); } -bool ExpectContains(const char* name, const std::string& value, - const std::string& expected) { +bool ExpectContains(const char *name, const std::string &value, + const std::string &expected) { if (value.find(expected) != std::string::npos) { return true; } @@ -39,8 +40,8 @@ bool ExpectContains(const char* name, const std::string& value, return false; } -bool ExpectNotContains(const char* name, const std::string& value, - const std::string& unexpected) { +bool ExpectNotContains(const char *name, const std::string &value, + const std::string &unexpected) { if (value.find(unexpected) == std::string::npos) { return true; } @@ -64,17 +65,18 @@ int main() { ReadFile(repo_root / "src/service/windows/service_host.h"); const std::string session_helper = ReadFile(repo_root / "src/service/windows/session_helper_main.cpp"); - const std::string targets = - ReadFile(repo_root / "xmake/targets.lua"); + const std::string targets = ReadFile(repo_root / "xmake/targets.lua"); const std::string interactive_state = ReadFile(repo_root / "src/service/windows/interactive_state.h"); - const std::string render_callback = - ReadFile(repo_root / "src/gui/render_callback.cpp"); - const std::string render = ReadFile(repo_root / "src/gui/render.cpp"); + const std::string gui_input_sources = + ReadFile(repo_root / "src/gui/runtime/peer_data_callbacks.cpp") + "\n" + + ReadFile(repo_root / "src/gui/features/input/keyboard_controller.cpp"); + const std::string windows_service_runtime = + ReadFile(repo_root / "src/gui/runtime/windows_service_runtime.cpp"); const std::string screen_capturer_h = ReadFile(repo_root / "src/screen_capturer/windows/screen_capturer_win.h"); - const std::string screen_capturer_cpp = - ReadFile(repo_root / "src/screen_capturer/windows/screen_capturer_win.cpp"); + const std::string screen_capturer_cpp = ReadFile( + repo_root / "src/screen_capturer/windows/screen_capturer_win.cpp"); bool ok = true; ok &= ExpectContains("service_host.cpp", service_host, @@ -93,8 +95,9 @@ int main() { ok &= ExpectContains("session_helper_main.cpp", session_helper, "EnablePerMonitorDpiAwareness();\n\n" " InitializeHelperLogger();"); - ok &= ExpectContains("service_host.cpp", service_host, - "const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms"); + ok &= ExpectContains( + "service_host.cpp", service_host, + "const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms"); ok &= ExpectContains("service_host.cpp", service_host, "while (GetTickCount64() <= deadline_tick)"); ok &= ExpectNotContains("service_host.cpp", service_host, @@ -103,18 +106,18 @@ int main() { "BuildSecureInputHelperKeyboardCommand("); ok &= ExpectContains("service_host.cpp", service_host, "const std::string& interactive_stage"); - ok &= ExpectContains("service_host.h", service_host_h, - "bool LaunchSecureInputHelper(DWORD session_id,\n" - " const std::string& interactive_stage,\n" - " const std::string& interactive_desktop)"); + ok &= ExpectContains( + "service_host.h", service_host_h, + "bool LaunchSecureInputHelper(DWORD session_id,\n" + " const std::string& interactive_stage,\n" + " const std::string& interactive_desktop)"); ok &= ExpectContains("service_host.h", service_host_h, "std::string secure_input_helper_interactive_stage_"); ok &= ExpectContains("service_host.cpp", service_host, "SecureInputHelperDesktopForStage"); ok &= ExpectContains("service_host.cpp", service_host, "IsConsentUiRunningInSession"); - ok &= ExpectContains("service_host.cpp", service_host, - "L\"Consent.exe\""); + ok &= ExpectContains("service_host.cpp", service_host, "L\"Consent.exe\""); ok &= ExpectContains("session_helper_main.cpp", session_helper, "IsConsentUiRunningInCurrentSession"); ok &= ExpectContains("session_helper_main.cpp", session_helper, @@ -136,41 +139,47 @@ int main() { "return L\"winsta0\\\\Winlogon\""); ok &= ExpectContains("service_host.cpp", service_host, "return L\"winsta0\\\\default\""); - ok &= ExpectContains("service_host.cpp", service_host, - "secure_input_helper_interactive_stage_ == interactive_stage"); - ok &= ExpectContains("service_host.cpp", service_host, - "secure_input_helper_interactive_stage_ = interactive_stage"); + ok &= ExpectContains( + "service_host.cpp", service_host, + "secure_input_helper_interactive_stage_ == interactive_stage"); + ok &= ExpectContains( + "service_host.cpp", service_host, + "secure_input_helper_interactive_stage_ = interactive_stage"); ok &= ExpectContains("service_host.cpp", service_host, "secure_input_helper_interactive_stage_.clear()"); - ok &= ExpectContains("service_host.cpp", service_host, - "LaunchSecureInputHelper(target_session_id, interactive_stage,\n" - " interactive_desktop)"); + ok &= ExpectContains( + "service_host.cpp", service_host, + "LaunchSecureInputHelper(target_session_id, interactive_stage,\n" + " interactive_desktop)"); ok &= ExpectContains("service_host.cpp", service_host, "\\\"secure_input_helper_stage\\\":\\\""); ok &= ExpectContains("service_host.cpp", service_host, "session_helper_report_interactive_stage_"); ok &= ExpectContains("service_host.cpp", service_host, "return SendSecureDesktopMouseInput"); - ok &= ExpectContains("render.cpp", render, + ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime, "constexpr DWORD kWindowsServiceQueryTimeoutMs = 500"); - ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp, - "constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500"); - ok &= ExpectContains("render.cpp", render, + ok &= + ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp, + "constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500"); + ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime, "IsTransientWindowsServiceStatusError(status.error)"); ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp, "IsTransientWindowsServiceStatusError(status.error)"); - ok &= ExpectContains("render.cpp", render, + ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime, "Local Windows service temporarily unavailable"); - ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp, - "Windows capturer secure desktop service temporarily unavailable"); - ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp, - "Windows capturer secure desktop transient frame query failed"); + ok &= ExpectContains( + "screen_capturer_win.cpp", screen_capturer_cpp, + "Windows capturer secure desktop service temporarily unavailable"); + ok &= ExpectContains( + "screen_capturer_win.cpp", screen_capturer_cpp, + "Windows capturer secure desktop transient frame query failed"); ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp, "if (transient_error) {\n" " LOG_INFO("); - ok &= ExpectContains("render_callback.cpp", render_callback, + ok &= ExpectContains("gui_input_sources.cpp", gui_input_sources, "IsTransientSecureDesktopInputFailure"); - ok &= ExpectContains("render_callback.cpp", render_callback, + ok &= ExpectContains("gui_input_sources.cpp", gui_input_sources, "Secure desktop keyboard injection transient failure"); ok &= ExpectContains("session_helper_main.cpp", session_helper, "MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE"); @@ -210,31 +219,36 @@ int main() { "json[\"current_desktop\"]"); ok &= ExpectContains("session_helper_main.cpp", session_helper, "json[\"stage\"]"); - ok &= ExpectContains("session_helper_main.cpp", session_helper, - "ParseSecureInputKeyboardCommand(command, &key_code, &is_down, &scan_code,\n" - " &extended, &interactive_stage,\n" - " &interactive_desktop)"); - ok &= ExpectContains("session_helper_main.cpp", session_helper, - "InjectKeyboardInput(key_code, is_down, scan_code, extended,\n" - " interactive_stage, interactive_desktop)"); + ok &= ExpectContains( + "session_helper_main.cpp", session_helper, + "ParseSecureInputKeyboardCommand(command, &key_code, &is_down, " + "&scan_code,\n" + " &extended, &interactive_stage,\n" + " &interactive_desktop)"); + ok &= ExpectContains( + "session_helper_main.cpp", session_helper, + "InjectKeyboardInput(key_code, is_down, scan_code, extended,\n" + " interactive_stage, interactive_desktop)"); ok &= ExpectContains("session_helper_main.cpp", session_helper, "InjectMouseInput(mouse_request)"); - ok &= ExpectNotContains("session_helper_main.cpp", session_helper, - "EnsureThreadDesktop(L\"Winlogon\", &secure_desktop)"); - ok &= ExpectContains("service_host.cpp", service_host, - "winsta0\\\\default"); - ok &= ExpectNotContains("service_host.cpp", service_host, - "startup_info.lpDesktop = const_cast(L\"winsta0\\\\Winlogon\")"); + ok &= + ExpectNotContains("session_helper_main.cpp", session_helper, + "EnsureThreadDesktop(L\"Winlogon\", &secure_desktop)"); + ok &= ExpectContains("service_host.cpp", service_host, "winsta0\\\\default"); + ok &= ExpectNotContains( + "service_host.cpp", service_host, + "startup_info.lpDesktop = const_cast(L\"winsta0\\\\Winlogon\")"); ok &= ExpectContains("interactive_state.h", interactive_state, "interactive_stage == \"lock-screen\""); - ok &= ExpectContains("render_callback.cpp", render_callback, + ok &= ExpectContains("gui_input_sources.cpp", gui_input_sources, "RemoteAction remote_action{};"); - ok &= ExpectContains("render.cpp", render, + ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime, "previous_secure_desktop_interaction"); ok &= ExpectNotContains( - "render_callback.cpp", render_callback, - "render->local_service_available_ &&\n" - " IsSecureDesktopInteractionRequired(render->local_interactive_stage_)"); + "gui_input_sources.cpp", gui_input_sources, + "runtime->local_service_available_ &&\n" + " " + "IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_)"); ok &= ExpectContains("screen_capturer_win.h", screen_capturer_h, "std::string secure_shared_stage_;"); ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp, diff --git a/xmake/targets.lua b/xmake/targets.lua index bec8a4f..37bf92e 100644 --- a/xmake/targets.lua +++ b/xmake/targets.lua @@ -214,20 +214,21 @@ function setup_targets() add_deps("rd_log", "common", "assets", "config_center", "minirtc", "path_manager", "screen_capturer", "speaker_capturer", "device_controller", "thumbnail", "version_checker", "tools") - add_files("src/gui/*.cpp", "src/gui/panels/*.cpp", "src/gui/toolbars/*.cpp", - "src/gui/windows/*.cpp") - add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars", - "src/gui/windows", {public = true}) + add_files("src/gui/render.cpp", "src/gui/application/*.cpp", + "src/gui/runtime/*.cpp", + "src/gui/features/devices/*.cpp", "src/gui/features/input/*.cpp", + "src/gui/features/clipboard/*.cpp", "src/gui/features/file_transfer/*.cpp", + "src/gui/features/settings/*.cpp", "src/gui/views/panels/*.cpp", + "src/gui/views/toolbars/*.cpp", "src/gui/views/windows/*.cpp") + add_includedirs("src/gui", {public = true}) if is_os("windows") then - add_files("src/gui/tray/win_tray.cpp") - add_includedirs("src/gui/tray", "src/service/windows", - {public = true}) + add_files("src/gui/platform/tray/win_tray.cpp") + add_includedirs("src/service/windows", {public = true}) elseif is_os("macosx") then - add_files("src/gui/windows/*.mm", "src/gui/tray/*.mm") - add_includedirs("src/gui/tray", {public = true}) + add_files("src/gui/runtime/*.mm", "src/gui/views/windows/*.mm", + "src/gui/platform/tray/*.mm") elseif is_os("linux") then - add_files("src/gui/tray/linux_tray.cpp") - add_includedirs("src/gui/tray", {public = true}) + add_files("src/gui/platform/tray/linux_tray.cpp") end if is_os("windows") then