From 619e54dc0eb5318a4414e112af9182677a0146c2 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 20 Jan 2026 21:22:20 +0800 Subject: [PATCH] [feat] add controller info and file transfer in server window --- src/common/window_util_mac.h | 25 ++ src/common/window_util_mac.mm | 64 ++++ src/gui/assets/localization/localization.h | 4 + src/gui/render.cpp | 332 +++++++++++++++++---- src/gui/render.h | 17 +- src/gui/render_callback.cpp | 2 + src/gui/toolbars/control_bar.cpp | 24 +- src/gui/windows/server_window.cpp | 268 +++++++---------- xmake.lua | 3 + 9 files changed, 511 insertions(+), 228 deletions(-) create mode 100644 src/common/window_util_mac.h create mode 100644 src/common/window_util_mac.mm diff --git a/src/common/window_util_mac.h b/src/common/window_util_mac.h new file mode 100644 index 0000000..a57fa03 --- /dev/null +++ b/src/common/window_util_mac.h @@ -0,0 +1,25 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-01-20 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _WINDOW_UTIL_MAC_H_ +#define _WINDOW_UTIL_MAC_H_ + +struct SDL_Window; + +namespace crossdesk { + +// Best-effort: keep an SDL window above normal windows on macOS. +// No-op on non-macOS builds. +void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top); + +// Best-effort: exclude an SDL window from the Window menu and window cycling. +// Note: Cmd-Tab switches apps (not individual windows), so this primarily +// affects the Window menu and Cmd-` window cycling. +void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded); + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/src/common/window_util_mac.mm b/src/common/window_util_mac.mm new file mode 100644 index 0000000..7d1b61a --- /dev/null +++ b/src/common/window_util_mac.mm @@ -0,0 +1,64 @@ +#include "window_util_mac.h" + +#if defined(__APPLE__) + +#include + +#import + +namespace crossdesk { + +static NSWindow* GetNSWindowFromSDL(::SDL_Window* window) { + if (!window) { + return nil; + } + +#if !defined(SDL_PROP_WINDOW_COCOA_WINDOW_POINTER) + return nil; +#else + SDL_PropertiesID props = SDL_GetWindowProperties(window); + void* cocoa_window_ptr = + SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL); + if (!cocoa_window_ptr) { + return nil; + } + return (__bridge NSWindow*)cocoa_window_ptr; +#endif +} + +void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top) { + NSWindow* ns_window = GetNSWindowFromSDL(window); + if (!ns_window) { + (void)always_on_top; + return; + } + + // Keep above normal windows. + const NSInteger level = always_on_top ? NSFloatingWindowLevel : NSNormalWindowLevel; + [ns_window setLevel:level]; + + // Optional: keep visible across Spaces/fullscreen. Safe as best-effort. + NSWindowCollectionBehavior behavior = [ns_window collectionBehavior]; + behavior |= NSWindowCollectionBehaviorCanJoinAllSpaces; + behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary; + [ns_window setCollectionBehavior:behavior]; +} + +void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded) { + NSWindow* ns_window = GetNSWindowFromSDL(window); + if (!ns_window) { + (void)excluded; + return; + } + + [ns_window setExcludedFromWindowsMenu:excluded]; + + NSWindowCollectionBehavior behavior = [ns_window collectionBehavior]; + behavior |= NSWindowCollectionBehaviorIgnoresCycle; + behavior |= NSWindowCollectionBehaviorTransient; + [ns_window setCollectionBehavior:behavior]; +} + +} // namespace crossdesk + +#endif // __APPLE__ diff --git a/src/gui/assets/localization/localization.h b/src/gui/assets/localization/localization.h index 0f17ab9..bbb8a25 100644 --- a/src/gui/assets/localization/localization.h +++ b/src/gui/assets/localization/localization.h @@ -197,6 +197,10 @@ static std::vector completed = { reinterpret_cast(u8"已完成"), "Completed"}; static std::vector failed = { reinterpret_cast(u8"失败"), "Failed"}; +static std::vector controller = { + reinterpret_cast(u8"控制端:"), "Controller:"}; +static std::vector file_transfer = { + reinterpret_cast(u8"文件传输:"), "File Transfer:"}; #if _WIN32 static std::vector minimize_to_tray = { diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 02eb2ca..dada7be 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -2,6 +2,12 @@ #include +#if defined(__linux__) && !defined(__APPLE__) +#include +#include +#endif + +#include #include #include #include @@ -21,10 +27,123 @@ #include "screen_capturer_factory.h" #include "version_checker.h" +#if defined(__APPLE__) +#include "window_util_mac.h" +#endif + #define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2 namespace crossdesk { +namespace { +#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)); @@ -131,6 +250,20 @@ SDL_HitTestResult Render::HitTestCallback(SDL_Window* window, 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); @@ -963,6 +1096,51 @@ int Render::CreateServerWindow() { return -1; } +#if _WIN32 + // Hide server window from the taskbar by making it an owned 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); + + HWND owner_hwnd = nullptr; + if (main_window_) { + SDL_PropertiesID main_props = SDL_GetWindowProperties(main_window_); + owner_hwnd = (HWND)SDL_GetPointerProperty( + main_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); + + if (owner_hwnd) { + SetWindowLongPtr(server_hwnd, GWLP_HWNDPARENT, (LONG_PTR)owner_hwnd); + } + + // 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_), @@ -2028,10 +2206,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) { } break; case SDL_EVENT_DROP_FILE: - if (stream_window_ && - SDL_GetWindowID(stream_window_) == event.window.windowID) { - ProcessFileDropEvent(event); - } + ProcessFileDropEvent(event); break; case SDL_EVENT_MOUSE_MOTION: @@ -2115,61 +2290,110 @@ void Render::ProcessSdlEvent(const SDL_Event& event) { } 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 (!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; + 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 \ No newline at end of file diff --git a/src/gui/render.h b/src/gui/render.h index ee363ae..abf8ad9 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -204,6 +204,7 @@ class Render { int UpdateNotificationWindow(); int StreamWindow(); int ServerWindow(); + int RemoteClientInfoWindow(); int LocalWindow(); int RemoteWindow(); int RecentConnectionsWindow(); @@ -220,6 +221,7 @@ class Render { 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, @@ -468,6 +470,7 @@ class Render { std::string controlled_remote_id_ = ""; std::string focused_remote_id_ = ""; bool need_to_send_host_info_ = false; + std::string remote_client_id_ = ""; SDL_Event last_mouse_event; SDL_AudioStream* output_stream_; uint32_t STREAM_REFRESH_EVENT = 0; @@ -504,14 +507,14 @@ class Render { bool need_to_destroy_server_window_ = false; bool server_window_created_ = false; bool server_window_inited_ = false; - int server_window_width_default_ = 300; - int server_window_height_default_ = 450; - float server_window_width_ = 300; - float server_window_height_ = 450; - float server_window_title_bar_height_ = 50.0f; + 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_ = 300; - int server_window_normal_height_ = 450; + 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; diff --git a/src/gui/render_callback.cpp b/src/gui/render_callback.cpp index 679cda9..4a5434d 100644 --- a/src/gui/render_callback.cpp +++ b/src/gui/render_callback.cpp @@ -628,6 +628,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id, render->is_server_mode_ = true; render->start_screen_capturer_ = true; render->start_speaker_capturer_ = true; + render->remote_client_id_ = remote_id; #ifdef CROSSDESK_DEBUG render->start_mouse_controller_ = false; render->start_keyboard_capturer_ = false; @@ -657,6 +658,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id, render->start_mouse_controller_ = false; render->start_keyboard_capturer_ = false; render->need_to_send_host_info_ = false; + render->remote_client_id_ = ""; if (props) props->connection_established_ = false; if (render->audio_capture_) { render->StopSpeakerCapturer(); diff --git a/src/gui/toolbars/control_bar.cpp b/src/gui/toolbars/control_bar.cpp index f125e41..355dcf2 100644 --- a/src/gui/toolbars/control_bar.cpp +++ b/src/gui/toolbars/control_bar.cpp @@ -15,18 +15,6 @@ namespace crossdesk { -std::string 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 : ""; -} - int CountDigits(int number) { if (number == 0) return 1; return (int)std::floor(std::log10(std::abs(number))) + 1; @@ -53,6 +41,18 @@ 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 + ); + + return path ? path : ""; +} + void Render::ProcessSelectedFile( const std::string& path, std::shared_ptr& props, const std::string& file_label) { diff --git a/src/gui/windows/server_window.cpp b/src/gui/windows/server_window.cpp index 7009a39..4fd1586 100644 --- a/src/gui/windows/server_window.cpp +++ b/src/gui/windows/server_window.cpp @@ -1,50 +1,11 @@ +#include "layout_relative.h" +#include "localization.h" #include "rd_log.h" #include "render.h" namespace crossdesk { -namespace { -constexpr float kDragThresholdPx = 3.0f; - -// Handles dragging for the *last submitted ImGui item*. -// `reset_on_deactivate` should be false when the caller needs to know whether a -// deactivation was a click (no drag) vs a drag-release (dragging==true). -inline void HandleWindowDragForLastItem(SDL_Window* window, bool& dragging, - float& start_mouse_x, - float& start_mouse_y, int& start_win_x, - int& start_win_y, - bool reset_on_deactivate = true) { - if (!window) { - return; - } - - if (ImGui::IsItemActivated()) { - SDL_GetGlobalMouseState(&start_mouse_x, &start_mouse_y); - SDL_GetWindowPosition(window, &start_win_x, &start_win_y); - dragging = false; - } - - if (ImGui::IsItemActive()) { - if (!dragging && - ImGui::IsMouseDragging(ImGuiMouseButton_Left, kDragThresholdPx)) { - dragging = true; - } - - if (dragging) { - float mouse_x = 0.0f; - float mouse_y = 0.0f; - SDL_GetGlobalMouseState(&mouse_x, &mouse_y); - const int dx = (int)(mouse_x - start_mouse_x); - const int dy = (int)(mouse_y - start_mouse_y); - SDL_SetWindowPosition(window, start_win_x + dx, start_win_y + dy); - } - } - - if (reset_on_deactivate && ImGui::IsItemDeactivated()) { - dragging = false; - } -} -} // namespace +namespace {} // namespace int Render::ServerWindow() { ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_), @@ -57,44 +18,6 @@ int Render::ServerWindow() { ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); - // Collapsed mode: no buttons; drag to move; click to restore. - if (server_window_collapsed_) { - ImGui::SetCursorPos(ImVec2(0.0f, 0.0f)); - ImGui::InvisibleButton("##server_collapsed_area", - ImVec2(server_window_width_, server_window_height_)); - - HandleWindowDragForLastItem(server_window_, - server_window_collapsed_dragging_, - server_window_collapsed_drag_start_mouse_x_, - server_window_collapsed_drag_start_mouse_y_, - server_window_collapsed_drag_start_win_x_, - server_window_collapsed_drag_start_win_y_, - /*reset_on_deactivate=*/false); - - const bool request_restore = - ImGui::IsItemDeactivated() && !server_window_collapsed_dragging_; - if (ImGui::IsItemDeactivated()) { - server_window_collapsed_dragging_ = false; - } - - if (request_restore && server_window_) { - int w = 0; - int x = 0; - int y = 0; - int h = 0; - SDL_GetWindowSize(server_window_, &w, &h); - SDL_GetWindowPosition(server_window_, &x, &y); - - const int normal_h = server_window_normal_height_; - SDL_SetWindowSize(server_window_, w, normal_h); - SDL_SetWindowPosition(server_window_, x, y); - server_window_collapsed_ = false; - } - - ImGui::End(); - return 0; - } - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); @@ -105,97 +28,132 @@ int Render::ServerWindow() { ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus); - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - float server_title_bar_button_width = server_window_title_bar_height_; float server_title_bar_button_height = server_window_title_bar_height_; - // Drag area: the title bar excluding the right-side buttons. + // Collapse/expand toggle button (FontAwesome icon). { - const float drag_w = - server_window_width_ - server_title_bar_button_width * 2; - const float drag_h = server_title_bar_button_height; ImGui::SetCursorPos(ImVec2(0.0f, 0.0f)); - ImGui::InvisibleButton("##server_title_drag_area", ImVec2(drag_w, drag_h)); - HandleWindowDragForLastItem( - server_window_, server_window_dragging_, - server_window_drag_start_mouse_x_, server_window_drag_start_mouse_y_, - server_window_drag_start_win_x_, server_window_drag_start_win_y_); - } + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, + ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - float minimize_button_pos_x = - server_window_width_ - server_title_bar_button_width * 2; - ImGui::SetCursorPos(ImVec2(minimize_button_pos_x, 0.0f)); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::SetWindowFontScale(0.5f); + const char* icon = + server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP; + std::string toggle_label = std::string(icon) + "##server_toggle"; + if (ImGui::Button(toggle_label.c_str(), + ImVec2(server_title_bar_button_width, + server_title_bar_button_height))) { + if (server_window_) { + int w = 0; + int h = 0; + int x = 0; + int y = 0; + SDL_GetWindowSize(server_window_, &w, &h); + SDL_GetWindowPosition(server_window_, &x, &y); - float minimize_pos_x = - minimize_button_pos_x + server_title_bar_button_width * 0.33f; - float minimize_pos_y = server_title_bar_button_height * 0.5f; - std::string server_minimize_button = "##minimize"; // ICON_FA_MINUS; - if (ImGui::Button(server_minimize_button.c_str(), - ImVec2(server_title_bar_button_width, - server_title_bar_button_height))) { - if (server_window_) { - int w = 0; - int h = 0; - int x = 0; - int y = 0; - SDL_GetWindowSize(server_window_, &w, &h); - SDL_GetWindowPosition(server_window_, &x, &y); - - const int collapsed_h = (int)server_window_title_bar_height_; - // Collapse upward: keep top edge stable. - SDL_SetWindowSize(server_window_, w, collapsed_h); - SDL_SetWindowPosition(server_window_, x, y); - server_window_collapsed_ = true; + if (server_window_collapsed_) { + const int normal_h = server_window_normal_height_; + SDL_SetWindowSize(server_window_, w, normal_h); + SDL_SetWindowPosition(server_window_, x, y); + server_window_collapsed_ = false; + } else { + const int collapsed_h = (int)server_window_title_bar_height_; + // Collapse upward: keep top edge stable. + SDL_SetWindowSize(server_window_, w, collapsed_h); + SDL_SetWindowPosition(server_window_, x, y); + server_window_collapsed_ = true; + } + } } + ImGui::SetWindowFontScale(1.0f); + + ImGui::PopStyleColor(3); } - draw_list->AddLine( - ImVec2(minimize_pos_x, minimize_pos_y), - ImVec2(minimize_pos_x + server_title_bar_button_width * 0.33f, - minimize_pos_y), - IM_COL32(0, 0, 0, 255)); - ImGui::PopStyleColor(3); - - float xmark_button_pos_x = - server_window_width_ - server_title_bar_button_width; - ImGui::SetCursorPos(ImVec2(xmark_button_pos_x, 0.0f)); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0, 0, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0, 0, 0.5f)); - - float xmark_pos_x = xmark_button_pos_x + server_title_bar_button_width * 0.5f; - float xmark_pos_y = server_title_bar_button_height * 0.5f; - float xmark_size = server_title_bar_button_width * 0.33f; - std::string server_close_button = "##xmark"; // ICON_FA_XMARK; - if (ImGui::Button(server_close_button.c_str(), - ImVec2(server_title_bar_button_width, - server_title_bar_button_height))) { - LOG_ERROR("Close button clicked"); - LeaveConnection(peer_, self_hosted_id_); - } - - draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f, - xmark_pos_y - xmark_size / 2 + 0.75f), - ImVec2(xmark_pos_x + xmark_size / 2 - 1.5f, - xmark_pos_y + xmark_size / 2 - 0.5f), - IM_COL32(0, 0, 0, 255)); - draw_list->AddLine( - ImVec2(xmark_pos_x + xmark_size / 2 - 1.75f, - xmark_pos_y - xmark_size / 2 + 0.75f), - ImVec2(xmark_pos_x - xmark_size / 2, xmark_pos_y + xmark_size / 2 - 1.0f), - IM_COL32(0, 0, 0, 255)); - - ImGui::PopStyleColor(3); ImGui::EndChild(); ImGui::PopStyleVar(); ImGui::PopStyleColor(); + RemoteClientInfoWindow(); + ImGui::End(); return 0; } + +int Render::RemoteClientInfoWindow() { + float remote_client_info_window_width = server_window_width_ * 0.75f; + float remote_client_info_window_height = + (server_window_height_ - server_window_title_bar_height_) * 0.3f; + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f); + ImGui::BeginChild( + "RemoteClientInfoWindow", + ImVec2(remote_client_info_window_width, remote_client_info_window_height), + ImGuiChildFlags_Border, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoBringToFrontOnFocus); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + + ImGui::SetWindowFontScale(0.7f); + ImGui::Text("%s", + localization::controller[localization_language_index_].c_str()); + ImGui::SameLine(); + ImGui::Text("%s", remote_client_id_.c_str()); + + ImGui::SetWindowFontScale(1.0f); + + ImGui::EndChild(); + + ImGui::SameLine(); + + float close_connection_button_width = server_window_width_ * 0.15f; + float close_connection_button_height = remote_client_info_window_height; + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0.5f, 0.5f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 5.0f); + ImGui::SetWindowFontScale(0.7f); + if (ImGui::Button(ICON_FA_XMARK, ImVec2(close_connection_button_width, + close_connection_button_height))) { + LeaveConnection(peer_, self_hosted_id_); + } + ImGui::SetWindowFontScale(1.0f); + ImGui::PopStyleColor(3); + ImGui::PopStyleVar(); + + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f)); + ImGui::BeginChild( + "RemoteClientInfoFileTransferWindow", + ImVec2(remote_client_info_window_width, remote_client_info_window_height), + ImGuiChildFlags_Border, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoBringToFrontOnFocus); + ImGui::PopStyleColor(); + + ImGui::SetWindowFontScale(0.7f); + ImGui::AlignTextToFramePadding(); + ImGui::Text( + "%s", localization::file_transfer[localization_language_index_].c_str()); + + ImGui::SameLine(); + + if (ImGui::Button( + localization::select_file[localization_language_index_].c_str())) { + std::string title = localization::select_file[localization_language_index_]; + std::string path = OpenFileDialog(title); + LOG_INFO("Selected file path: {}", path.c_str()); + } + ImGui::SetWindowFontScale(1.0f); + + ImGui::EndChild(); + + return 0; +} } // namespace crossdesk \ No newline at end of file diff --git a/xmake.lua b/xmake.lua index 2cb95f4..4059df2 100644 --- a/xmake.lua +++ b/xmake.lua @@ -70,6 +70,9 @@ target("common") set_kind("object") add_deps("rd_log") add_files("src/common/*.cpp") + if is_os("macosx") then + add_files("src/common/*.mm") + end add_includedirs("src/common", {public = true}) target("path_manager")