From 20ede6503844a27210fb1eb599563d1080bb2ca6 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 27 Oct 2026 17:07:03 +0800 Subject: [PATCH] [ui] unify floating logo hand cursors with arrow fallback --- .../src/gui/application/gui_application.cpp | 38 +++++++++++++++++++ apps/desktop/src/gui/ui/stream_window.slint | 8 +++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index c21dc488..d184ee99 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -71,6 +71,7 @@ #elif (defined(__linux__) && !defined(__OHOS__)) #include #include +#include #include #include @@ -100,6 +101,41 @@ namespace { using namespace std::chrono_literals; +bool SupportsControlHandCursor([[maybe_unused]] const slint::Window& window) { +#if defined(_WIN32) + return LoadCursorW(nullptr, IDC_HAND) != nullptr; +#elif defined(__linux__) && !defined(__OHOS__) + // Match winit's pointer aliases, without using any move/grab fallback. + constexpr const char* names[] = {"pointer", "hand2", "hand1", "hand", + "pointing_hand"}; + if (window.wayland_surface()) { + const char* theme = std::getenv("XCURSOR_THEME"); + for (const char* name : names) { + if (auto* images = XcursorLibraryLoadImages(name, theme, 24)) { + const bool available = images->nimage > 0; + XcursorImagesDestroy(images); + if (available) return true; + } + } + return false; + } + std::unique_ptr display{ + XOpenDisplay(nullptr), XCloseDisplay}; + if (!display) return false; + for (const char* name : names) { + const Cursor cursor = XcursorLibraryLoadCursor(display.get(), name); + if (cursor != None) { + XFreeCursor(display.get(), cursor); + return true; + } + } + return false; +#else + // AppKit supplies pointingHandCursor; other adapters resolve pointer. + return true; +#endif +} + std::optional GenerateRandomPassword(std::string_view current) { constexpr std::string_view alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; @@ -3640,6 +3676,8 @@ void GuiApplication::SyncStreamWindow() { if (!has_sessions) need_to_create_stream_window_ = false; if (need_to_create_stream_window_ && !ui_->stream) { ui_->stream.emplace(ui::StreamWindow::create()); + (*ui_->stream)->set_control_hand_cursor_supported( + SupportsControlHandCursor(ui_->main->window())); #if (defined(__linux__) && !defined(__OHOS__)) (*ui_->stream)->set_custom_titlebar(use_xwayland_gui_); #endif diff --git a/apps/desktop/src/gui/ui/stream_window.slint b/apps/desktop/src/gui/ui/stream_window.slint index 51293dde..c0ea4ad1 100644 --- a/apps/desktop/src/gui/ui/stream_window.slint +++ b/apps/desktop/src/gui/ui/stream_window.slint @@ -102,6 +102,7 @@ component ControlBarButton inherits Rectangle { in property show-logo: false; in property logo-pressed: false; in property logo-dragging: false; + in property hand-cursor-supported: true; in property reveal-progress: 1; in property badge: ""; in property tooltip: ""; @@ -227,7 +228,7 @@ component ControlBarButton inherits Rectangle { } touch := TouchArea { - mouse-cursor: !root.action-enabled ? default : root.show-logo ? (root.logo-pressed ? grabbing : grab) : pointer; + mouse-cursor: root.action-enabled && root.hand-cursor-supported ? pointer : default; scroll-event(event) => { accept } pointer-event(event) => { root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y); @@ -285,6 +286,7 @@ export component StreamWindow inherits Window { default-font-size: ImGuiFontStyle.base; in property custom-titlebar: false; + in property control-hand-cursor-supported: true; in property native-video-enabled: false; // Export the actual input surface in logical pixels. Native rendering and // input mapping must follow the same layout, including title and tab bars. @@ -1073,7 +1075,8 @@ export component StreamWindow inherits Window { // The logo also retains a stable owner if another button is used. if root.remote-pointer-active || root.control-drag-pending: TouchArea { z: 15; - mouse-cursor: root.control-drag-pending ? MouseCursor.grabbing + mouse-cursor: root.control-drag-pending + ? (root.control-hand-cursor-supported ? MouseCursor.pointer : MouseCursor.default) : root.remote-cursor-active ? RemoteCursor.shape(root.remote-cursor-shape) : MouseCursor.default; pointer-event(event) => { @@ -1249,6 +1252,7 @@ export component StreamWindow inherits Window { width: root.control-toolbar-height; height: root.control-toolbar-height; show-logo: true; + hand-cursor-supported: root.control-hand-cursor-supported; logo-pressed: root.control-drag-pending; logo-dragging: self.logo-pressed && root.control-dragging; tooltip: root.control-expanded ? StreamStrings.collapse-control : StreamStrings.expand-control;