From 3043b5f110b870bdbb33e16aa9f0fcd045f888b9 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Wed, 14 Oct 2026 10:20:31 +0800 Subject: [PATCH] [fix] follow the selected device cursor in DeskHub --- .../src/gui/application/gui_application.cpp | 19 ++++++++++- apps/desktop/src/gui/ui/gallery_view.slint | 4 +++ apps/desktop/src/gui/ui/main_window.slint | 2 ++ apps/desktop/src/gui/ui/remote_cursor.slint | 34 +++++++++++++++++++ apps/desktop/src/gui/ui/stream_window.slint | 33 ++---------------- docs/deskhub.md | 1 + 6 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 apps/desktop/src/gui/ui/remote_cursor.slint diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index 07ef490e..75e4826d 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -4395,6 +4395,8 @@ void GuiApplication::ReleaseControl() { ReleasePressedInput(); gallery_group_control_id_.clear(); ui_->wall->set_gallery_group_controlling(false); + ui_->wall->set_gallery_remote_cursor_shape( + static_cast(RemoteCursorShape::default_cursor)); if (!ui_->gallery_control_id.empty()) { ui_->gallery_control_id.clear(); ui_->wall->set_gallery_controlling(false); @@ -5148,12 +5150,27 @@ void GuiApplication::SyncGallery() { ReleaseControl(); ui_->wall->set_gallery_fullscreen(fullscreen); } + auto cursor_shape = RemoteCursorShape::default_cursor; if (!ui_->gallery_control_id.empty()) { + // Only the selected control source owns the wall cursor, including group + // control. Followers can report different shapes without replacing it. auto controlled = FindRemoteSession(ui_->gallery_control_id); if (!visible || !controlled || controlled->closing_ || - controlled->connection_status_ != ConnectionStatus::Connected) + controlled->connection_status_ != ConnectionStatus::Connected) { ReleaseControl(); + } else { + HostPlatform platform; + { + std::lock_guard lock(controlled->remote_version_mutex_); + platform = controlled->remote_platform_; + } + std::lock_guard lock(controlled->remote_cursor_state_mutex_); + if (controlled->remote_cursor_state_received_) + cursor_shape = controlled->cursor_presentation_.Resolve( + SDL_GetTicks(), platform); + } } + ui_->wall->set_gallery_remote_cursor_shape(static_cast(cursor_shape)); const auto now = std::chrono::steady_clock::now(); const auto action = transition.Advance(now, current_size.width, current_size.height, window.scale_factor(), fullscreen, placement.Restoring()); diff --git a/apps/desktop/src/gui/ui/gallery_view.slint b/apps/desktop/src/gui/ui/gallery_view.slint index 2c4cb395..eb7bb2e1 100644 --- a/apps/desktop/src/gui/ui/gallery_view.slint +++ b/apps/desktop/src/gui/ui/gallery_view.slint @@ -1,4 +1,5 @@ import { GalleryStrings } from "device_strings.slint"; +import { RemoteCursor } from "remote_cursor.slint"; import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.slint"; import { GalleryMenuButton } from "gallery_menu_button.slint"; import { GroupFilter } from "group_filter.slint"; @@ -44,6 +45,7 @@ export struct GalleryActionDevice { export component GalleryView inherits Rectangle { in property controlling: false; + in property remote-cursor-shape: 0; in property group-controlling: false; in property input-suspended: false; in property simplified: false; @@ -620,6 +622,8 @@ export component GalleryView inherits Rectangle { accept } TouchArea { + mouse-cursor: root.controlling && !root.input-suspended + ? RemoteCursor.shape(root.remote-cursor-shape) : MouseCursor.default; pointer-event(event) => { if !root.input-suspended { if event.kind == PointerEventKind.down { video-focus.focus(); } diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index 38c611cd..29d1e037 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -309,6 +309,7 @@ export component MainWindow inherits Window { } in-out property connection-input-error; in property gallery-controlling: false; + in property gallery-remote-cursor-shape: 0; in property gallery-group-controlling: false; out property gallery-video-width: gallery.video-width; out property gallery-video-height: gallery.video-height; @@ -1323,6 +1324,7 @@ export component MainWindow inherits Window { focus-rail-height <=> root.gallery-focus-rail-height; network-open => { root.gallery-network-open = true; } controlling: root.gallery-controlling; + remote-cursor-shape: root.gallery-remote-cursor-shape; group-controlling: root.gallery-group-controlling; input-suspended: root.gallery-transitioning; pointer-input(button, kind, x, y) => { root.gallery-pointer(button, kind, x, y); } diff --git a/apps/desktop/src/gui/ui/remote_cursor.slint b/apps/desktop/src/gui/ui/remote_cursor.slint new file mode 100644 index 00000000..1e08b26f --- /dev/null +++ b/apps/desktop/src/gui/ui/remote_cursor.slint @@ -0,0 +1,34 @@ +export global RemoteCursor { + // Numeric values mirror crossdesk::RemoteCursorShape on the wire. + public pure function shape(value: int) -> MouseCursor { + return value == 1 ? MouseCursor.none + : value == 2 ? MouseCursor.help + : value == 3 ? MouseCursor.pointer + : value == 4 ? MouseCursor.progress + : value == 5 ? MouseCursor.wait + : value == 6 ? MouseCursor.crosshair + : value == 7 ? MouseCursor.text + : value == 8 ? MouseCursor.alias + : value == 9 ? MouseCursor.copy + : value == 10 ? MouseCursor.move + : value == 11 ? MouseCursor.no-drop + : value == 12 ? MouseCursor.not-allowed + : value == 13 ? MouseCursor.grab + : value == 14 ? MouseCursor.grabbing + : value == 15 ? MouseCursor.col-resize + : value == 16 ? MouseCursor.row-resize + : value == 17 ? MouseCursor.n-resize + : value == 18 ? MouseCursor.e-resize + : value == 19 ? MouseCursor.s-resize + : value == 20 ? MouseCursor.w-resize + : value == 21 ? MouseCursor.ne-resize + : value == 22 ? MouseCursor.nw-resize + : value == 23 ? MouseCursor.se-resize + : value == 24 ? MouseCursor.sw-resize + : value == 25 ? MouseCursor.ew-resize + : value == 26 ? MouseCursor.ns-resize + : value == 27 ? MouseCursor.nesw-resize + : value == 28 ? MouseCursor.nwse-resize + : MouseCursor.default; + } +} diff --git a/apps/desktop/src/gui/ui/stream_window.slint b/apps/desktop/src/gui/ui/stream_window.slint index bd4b24d7..1e3e54ff 100644 --- a/apps/desktop/src/gui/ui/stream_window.slint +++ b/apps/desktop/src/gui/ui/stream_window.slint @@ -1,4 +1,5 @@ import { ComboBox, ScrollView } from "std-widgets.slint"; +import { RemoteCursor } from "remote_cursor.slint"; import { FontAwesomeIcons, IconButton, PrimaryButton, DialogSurface, ImGuiLineStyle, ImGuiFontStyle, FloatingBarStyle } from "common.slint"; export struct StreamTab { @@ -714,36 +715,8 @@ export component StreamWindow inherits Window { accept } pointer := TouchArea { - mouse-cursor: !root.remote-cursor-active ? MouseCursor.default - : root.remote-cursor-shape == 1 ? MouseCursor.none - : root.remote-cursor-shape == 2 ? MouseCursor.help - : root.remote-cursor-shape == 3 ? MouseCursor.pointer - : root.remote-cursor-shape == 4 ? MouseCursor.progress - : root.remote-cursor-shape == 5 ? MouseCursor.wait - : root.remote-cursor-shape == 6 ? MouseCursor.crosshair - : root.remote-cursor-shape == 7 ? MouseCursor.text - : root.remote-cursor-shape == 8 ? MouseCursor.alias - : root.remote-cursor-shape == 9 ? MouseCursor.copy - : root.remote-cursor-shape == 10 ? MouseCursor.move - : root.remote-cursor-shape == 11 ? MouseCursor.no-drop - : root.remote-cursor-shape == 12 ? MouseCursor.not-allowed - : root.remote-cursor-shape == 13 ? MouseCursor.grab - : root.remote-cursor-shape == 14 ? MouseCursor.grabbing - : root.remote-cursor-shape == 15 ? MouseCursor.col-resize - : root.remote-cursor-shape == 16 ? MouseCursor.row-resize - : root.remote-cursor-shape == 17 ? MouseCursor.n-resize - : root.remote-cursor-shape == 18 ? MouseCursor.e-resize - : root.remote-cursor-shape == 19 ? MouseCursor.s-resize - : root.remote-cursor-shape == 20 ? MouseCursor.w-resize - : root.remote-cursor-shape == 21 ? MouseCursor.ne-resize - : root.remote-cursor-shape == 22 ? MouseCursor.nw-resize - : root.remote-cursor-shape == 23 ? MouseCursor.se-resize - : root.remote-cursor-shape == 24 ? MouseCursor.sw-resize - : root.remote-cursor-shape == 25 ? MouseCursor.ew-resize - : root.remote-cursor-shape == 26 ? MouseCursor.ns-resize - : root.remote-cursor-shape == 27 ? MouseCursor.nesw-resize - : root.remote-cursor-shape == 28 ? MouseCursor.nwse-resize - : MouseCursor.default; + mouse-cursor: root.remote-cursor-active + ? RemoteCursor.shape(root.remote-cursor-shape) : MouseCursor.default; pointer-event(event) => { let over-control = self.mouse-x >= control.x && self.mouse-x <= control.x + control.width diff --git a/docs/deskhub.md b/docs/deskhub.md index f16e2a10..151ecb0d 100644 --- a/docs/deskhub.md +++ b/docs/deskhub.md @@ -26,6 +26,7 @@ - 主窗口菜单中的全局“设置”顶部并排提供“常规”和“桌面墙”两个选项卡;“桌面墙”可统一配置桌面墙设备的传输分辨率上限和帧率,桌面墙处于普通、精简、展开或全屏视图时,也可通过主窗口菜单打开设置。切换选项卡保留未保存修改,底部统一保存或取消。分辨率支持自动、320 × 180、640 × 360、1280 × 720、1920 × 1080、2560 × 1440、3840 × 2160和原始分辨率;固定上限适用于缩略图和展开画面,保持比例且不放大源画面。帧率支持跟随设备设置、15、30、60 FPS。保存后立即更新当前桌面墙会话,也用于后续桌面墙连接,不影响普通连接,跨分组和分页生效;取消不应用修改,恢复默认需保存后生效。配置保存在本地,重启后保留;保存失败时保留面板和输入。打开设置会释放远程操控。 - 服务器连接状态、指示灯和重连入口仅在主窗口显示,桌面墙的普通、精简、展开、批量选择和全屏视图均不显示服务器状态栏。桌面墙底部操作栏左侧以普通文字显示已连接设备数量,旁边是紧凑的“网络状态”按钮;点击按钮可打开当前已建立连接的设备列表,空白区域不响应点击,也不显示整条悬停提示。弹窗采用紧凑宽度,高度随设备数量调整,最多同时显示 6 行,更多设备通过滚动查看。网络列表不受桌面墙分组和分页限制;离线、未连接和连接中的设备不列入,断开后自动移除。列表显示设备名称、ID、直连/中继、实际协商的 SRTP 加密状态、控制端接收与发送速率、视频丢包率、视频延时、实际接收分辨率与帧率,打开期间每秒刷新。加密状态紧随连接方式显示为“已加密/未加密”,尚无统计或报告超过 3 秒未更新时显示“待确认”。分辨率与帧率在解码交付时统计,墙内预览和后台会话均可用;尚未收到画面时显示“—”,画面停止更新超过 1 秒时帧率显示 0 FPS。网络数值尚无统计或报告超过 3 秒未更新时显示“—”。每台设备采用两行布局:画面列显示分辨率和帧率,视频延时列显示延时和丢包率,不再显示 RTT。视频延时直接显示毫秒数,取最近 1 秒内新提交预览帧的平均值;从被控端采集时间戳计时,包含编码、传输、解码、预览转换和 UI 等待,按采集到画面提交估算,不包含提交后到屏幕实际显示的等待。当前无墙内预览样本时,可使用该设备独立串流窗口的延时统计。没有新显示帧、时间戳不可用或无有效样本时显示“—”,不把 RTT 当作视频延时。可通过关闭按钮、Esc 或点击列表外关闭;打开列表会释放远程操控。 - 点卡片进入只读放大视图,点“接管操控”才开始发送键鼠。点“释放操控”或按 Ctrl/⌘ + Shift + Esc 可以释放。切换设备、翻页、打开本地操作面板、切换模式及窗口失焦也会释放。 +- 接管期间,主画面内的鼠标形态跟随当前主选设备,支持文本输入、链接、忙碌、窗口缩放及隐藏等状态;群控时也只采用主选设备的光标状态,其他设备的反馈不会覆盖它。释放操控或主选设备断开后恢复默认指针,工具栏与缩略图保留本地鼠标形态。 - 放大视图中点击“开启群控”,以当前设备为主控,同时接管操控;鼠标移动、点击、拖拽、滚轮、键盘和组合键同步到所有其他已连接设备,包含其他分组、分页及未显示在墙内的连接。所有设备使用主控设备映射后的相同坐标和按键,不针对屏幕比例另做适配;新建立的连接也会接收后续输入,未连接和正在断开的设备跳过。点击“关闭群控”先释放各设备已按下的键鼠,再恢复当前设备的单独操控;释放操控、切换设备、窗口失焦或主控设备断开时自动结束群控。群控状态不保存,重新接管需要手动开启。 - 桌面墙连接默认静音,包含新建连接、重连、复用已有连接、添加或导入已有会话,以及切入桌面墙时已有的连接。展开、全屏或接管操控不会自动开启声音;需要声音时,可通过批量“取消静音”或在独立串流窗口主动开启。两个入口共用同一静音状态,主动开启的声音在桌面墙可见时播放,翻页、展开及释放操控保留声音设置;每台设备使用独立音频流同时播放,静音一台不会清空其他设备的声音。新版本对端按会话关闭音频订阅;旧版本发送的音频由控制端丢弃,不通过停止对端的全局声音采集影响其他控制端。 - 放大视图使用一行设备操作栏,显示返回、设备名称、状态和接管/释放操控按钮,右侧保留精简模式及全屏入口;选中的桌面在下方放大显示,同页其他桌面的缩略图统一在上方横向排列,超出宽度时可横向滚动。点击上方缩略图可切换下方桌面,当前桌面不在缩略图中重复显示;同页只有一台设备时隐藏缩略图栏。普通模式缩略图下方以一行显示设备名,长名称省略,不显示状态圆点;不在线设备的缩略图和下方放大预览统一使用灰色,精简模式同样生效;悬停可查看设备名称、ID 和在线状态,精简模式也支持悬停查看。缺少画面或画面停滞时保留状态占位;滚动条单独留出空间。普通和精简模式的缩略图预览区域均固定为 16:9,普通模式的设备名称单独显示在画面下方。普通、精简、宽窗口及全屏均保持上下布局,上下区域之间提供带抓手的分隔条:向上拖动扩大下方主画面,向下拖动同步放大上方缩略图;选中分隔条后也可用上下方向键微调。缩略图始终保持 16:9,并保留主画面的最小可用高度;窗口缩小时临时限制尺寸,恢复窗口后沿用原先尺寸。普通和精简模式共用此尺寸,切换设备后保持,松开鼠标后保存并在重启时恢复;与网格的单元格尺寸独立。同页只有一台设备时同时隐藏缩略图栏和分隔条。拖动分隔条不会切换设备或发送远程键鼠,Esc、窗口失焦及窗口大小变化会结束拖动。调整尺寸后仍保留当前设备所在页。主预览和缩略图均完整等比显示,鼠标坐标按实际预览区域映射。