From 4855b2d3b6e76626f56c9a6543054ed2023f65fb Mon Sep 17 00:00:00 2001 From: dijunkun Date: Wed, 14 Oct 2026 04:17:23 +0800 Subject: [PATCH] [fix] preserve DeskHub previews and control during fullscreen changes --- .../src/gui/application/gui_application.cpp | 38 +++++++++++-------- .../src/gui/application/gui_application.h | 1 + .../gallery/gallery_fullscreen_transition.h | 32 ++++++---------- apps/desktop/src/gui/ui/gallery_view.slint | 13 ++++--- apps/desktop/src/gui/ui/main_window.slint | 11 ++---- docs/deskhub.md | 2 +- 6 files changed, 46 insertions(+), 51 deletions(-) diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index bede67f9..55e9f9bb 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -1413,15 +1413,16 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) { main->on_gallery_pointer([this](slint::PointerEventButton button, slint::PointerEventKind kind, float x, float y) { - if (!ui_->gallery_control_id.empty()) + if (!ui_->gallery_control_id.empty() && !ui_->gallery_transition.Active()) SendPointerInput(static_cast(button), static_cast(kind), x, y); }); main->on_gallery_scroll([this](float dx, float dy, float x, float y) { - if (!ui_->gallery_control_id.empty()) SendScrollInput(dx, dy, x, y); + if (!ui_->gallery_control_id.empty() && !ui_->gallery_transition.Active()) + SendScrollInput(dx, dy, x, y); }); main->on_gallery_key([this](slint::SharedString text, bool pressed, bool ctrl, bool alt, bool shift, bool meta) { - if (!ui_->gallery_control_id.empty()) + if (!ui_->gallery_control_id.empty() && !ui_->gallery_transition.Active()) SendKeyInput(std::string(text), pressed, ctrl, alt, shift, meta); }); main->on_open_gallery([this] { ShowGalleryWindow(); }); @@ -4381,9 +4382,7 @@ void GuiApplication::SyncAudioPlayback() { } } -void GuiApplication::ReleaseControl() { - const auto gallery_target = ui_->gallery_control_id; - devices_.StopKeyboardCapturer(); +void GuiApplication::ReleasePressedInput() { keyboard_.ForceReleasePressedKeys(); if (auto props = FindRemoteSession(controlled_remote_id_); props && props->peer_) { @@ -4400,11 +4399,17 @@ void GuiApplication::ReleaseControl() { props->mouse_label_.c_str()); } } + ui_->pressed_mouse_buttons.clear(); +} + +void GuiApplication::ReleaseControl() { + const auto gallery_target = ui_->gallery_control_id; + devices_.StopKeyboardCapturer(); + ReleasePressedInput(); if (!ui_->gallery_control_id.empty()) { ui_->gallery_control_id.clear(); ui_->wall->set_gallery_controlling(false); } - ui_->pressed_mouse_buttons.clear(); SyncAudioPlayback(); std::shared_lock lock(remote_sessions_mutex_); for (const auto& [id, props] : remote_sessions_) { @@ -4476,7 +4481,6 @@ void GuiApplication::HideGalleryWindow() { SyncWindowGeometry(true); ui_->gallery_transition.Cancel(); ui_->wall->set_gallery_transitioning(false); - ui_->wall->set_gallery_content_visible(true); ui_->window_saved = {}; SaveGalleryWindowState(); #if defined(__APPLE__) @@ -4503,9 +4507,10 @@ void GuiApplication::ToggleGalleryFullscreen() { const bool target = !(ui_->gallery_window.Restoring() ? ui_->gallery_window.wall_fullscreen : fullscreen); ui_->gallery_transition.Begin(target, std::chrono::steady_clock::now()); - ReleaseControl(); + // Fullscreen only changes presentation. End held gestures before blocking + // input, but retain the controlled session and its keyboard focus. + if (!ui_->gallery_control_id.empty()) ReleasePressedInput(); ui_->wall->set_gallery_transitioning(true); - ui_->wall->set_gallery_content_visible(false); ui_->gallery_synced = {}; } @@ -5096,7 +5101,8 @@ void GuiApplication::SyncGallery() { if (ui_->observed_gallery_size && (current_size.width != ui_->observed_gallery_size->width || current_size.height != ui_->observed_gallery_size->height)) { - if (!ui_->gallery_control_id.empty()) ReleaseControl(); + if (!ui_->gallery_control_id.empty() && !ui_->gallery_transition.Active()) + ReleaseControl(); // Native resize can redraw before the usual 200ms preview refresh. ui_->gallery_synced = {}; } @@ -5112,7 +5118,8 @@ void GuiApplication::SyncGallery() { const bool fullscreen = window.is_fullscreen(); #endif if (wall_mode && !placement.Restoring()) { - if (fullscreen != ui_->wall->get_gallery_fullscreen()) ReleaseControl(); + if (fullscreen != ui_->wall->get_gallery_fullscreen() && !transition.Active()) + ReleaseControl(); ui_->wall->set_gallery_fullscreen(fullscreen); } if (!ui_->gallery_control_id.empty()) { @@ -5125,13 +5132,13 @@ void GuiApplication::SyncGallery() { const auto action = transition.Advance(now, current_size.width, current_size.height, window.scale_factor(), fullscreen, placement.Restoring()); if (action == gallery::FullscreenTransition::Action::Apply) { - // Update custom chrome before native resizing; keep content hidden until - // both the compositor geometry and the gallery model have settled. + // Keep previews visible while native resizing and the gallery model catch + // up. Only input and geometry persistence wait for the window to settle. ui_->wall->set_gallery_fullscreen(transition.Target()); #if defined(__APPLE__) if (!SetGalleryWindowFullscreen(transition.Target())) { ui_->wall->set_gallery_fullscreen(fullscreen); - transition.Reveal(now); + transition.Cancel(); } else #else window.set_fullscreen(transition.Target()); @@ -5142,7 +5149,6 @@ void GuiApplication::SyncGallery() { ui_->gallery_synced = {}; } ui_->wall->set_gallery_transitioning(transition.Active()); - ui_->wall->set_gallery_content_visible(transition.ContentVisible()); if (!transition.Active() && now - ui_->gallery_synced < (visible ? 16ms : 200ms)) return; diff --git a/apps/desktop/src/gui/application/gui_application.h b/apps/desktop/src/gui/application/gui_application.h index 5159ba60..fd27f9de 100644 --- a/apps/desktop/src/gui/application/gui_application.h +++ b/apps/desktop/src/gui/application/gui_application.h @@ -75,6 +75,7 @@ private: void DeviceGroupAction(const std::string& action, const std::string& value, bool gallery_window); bool SaveDeviceGroups(const std::string& new_group, bool gallery_window); + void ReleasePressedInput(); void ReleaseControl(); void SyncAudioPlayback(); void SaveGalleryWindowState(); diff --git a/apps/desktop/src/gui/features/gallery/gallery_fullscreen_transition.h b/apps/desktop/src/gui/features/gallery/gallery_fullscreen_transition.h index 7803c5f0..d167e406 100644 --- a/apps/desktop/src/gui/features/gallery/gallery_fullscreen_transition.h +++ b/apps/desktop/src/gui/features/gallery/gallery_fullscreen_transition.h @@ -12,17 +12,18 @@ namespace crossdesk::gallery { +// Guard input while native geometry settles, without hiding the live content. // Driven by the existing UI tick; never pumps events or creates nested timers. class FullscreenTransition { public: using Clock = std::chrono::steady_clock; - enum class Action { Idle, Apply, Reveal, Finished }; + enum class Action { Idle, Apply, Finished }; bool Begin(bool fullscreen, Clock::time_point now) { if (Active()) return false; target_ = fullscreen; - phase_ = Phase::FadingOut; + phase_ = Phase::Pending; phase_at_ = now; width_ = height_ = 0; scale_ = 0; @@ -33,13 +34,13 @@ public: Action Advance(Clock::time_point now, uint32_t width, uint32_t height, float scale, bool fullscreen, bool restoring) { using namespace std::chrono_literals; - if (phase_ == Phase::FadingOut) { + if (phase_ == Phase::Pending) { if (restoring && now - phase_at_ >= 1500ms) { - Reveal(now); - return Action::Reveal; + Cancel(); + return Action::Finished; } - // Slint fades out for 60ms; allow another frame before native resizing. - if (now - phase_at_ < 80ms || restoring) + // Apply on the next UI tick as soon as window restoration is complete. + if (restoring) return Action::Idle; phase_ = Phase::Settling; phase_at_ = stable_at_ = now; @@ -65,30 +66,19 @@ public: (geometry_changed_ || now - phase_at_ >= 300ms) && now - stable_at_ >= 32ms) || now - phase_at_ >= 1500ms) { - Reveal(now); - return Action::Reveal; + Cancel(); + return Action::Finished; } } - if (phase_ == Phase::FadingIn && now - phase_at_ >= 80ms) { - Cancel(); - return Action::Finished; - } return Action::Idle; } - void Reveal(Clock::time_point now) { - phase_ = Phase::FadingIn; - phase_at_ = now; - } void Cancel() { phase_ = Phase::Idle; } bool Active() const { return phase_ != Phase::Idle; } bool Target() const { return target_; } - bool ContentVisible() const { - return phase_ == Phase::Idle || phase_ == Phase::FadingIn; - } private: - enum class Phase { Idle, FadingOut, Settling, FadingIn }; + enum class Phase { Idle, Pending, Settling }; Phase phase_ = Phase::Idle; bool target_ = false; bool geometry_changed_ = false; diff --git a/apps/desktop/src/gui/ui/gallery_view.slint b/apps/desktop/src/gui/ui/gallery_view.slint index ed1e7920..dc8a1281 100644 --- a/apps/desktop/src/gui/ui/gallery_view.slint +++ b/apps/desktop/src/gui/ui/gallery_view.slint @@ -43,6 +43,7 @@ export struct GalleryActionDevice { export component GalleryView inherits Rectangle { in property controlling: false; + in property input-suspended: false; in property simplified: false; in property fullscreen: false; callback toggle-simplified; @@ -578,7 +579,7 @@ export component GalleryView inherits Rectangle { focus-on-click: true; focus-lost => { if root.controlling { root.action("release", ""); } } key-pressed(event) => { - if root.controlling { + if root.controlling && !root.input-suspended { if event.text == Key.Escape && event.modifiers.shift && (event.modifiers.control || event.modifiers.meta) { root.action("release", ""); } else { root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); } @@ -586,16 +587,18 @@ export component GalleryView inherits Rectangle { accept } key-released(event) => { - if root.controlling { root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); } + if root.controlling && !root.input-suspended { root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); } accept } TouchArea { pointer-event(event) => { - if event.kind == PointerEventKind.down { video-focus.focus(); } - if root.controlling { root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y); } + if !root.input-suspended { + if event.kind == PointerEventKind.down { video-focus.focus(); } + if root.controlling { root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y); } + } } scroll-event(event) => { - if root.controlling { root.scroll-input(event.delta-x, event.delta-y, self.mouse-x, self.mouse-y); } + if root.controlling && !root.input-suspended { root.scroll-input(event.delta-x, event.delta-y, self.mouse-x, self.mouse-y); } accept } } diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index 758e9b77..096240cc 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -299,10 +299,10 @@ export component MainWindow inherits Window { } in property gallery-fullscreen: false; in property gallery-transitioning: false; - in property gallery-content-visible: true; changed gallery-transitioning => { if root.gallery-transitioning { - focus-sink.focus(); + // Preserve the preview's keyboard focus when only changing size. + if !root.gallery-controlling { focus-sink.focus(); } root.menu-open = false; root.recent-tooltip-visible = false; } @@ -754,12 +754,6 @@ export component MainWindow inherits Window { width: root.width; height: root.height - self.y; clip: true; - opacity: root.gallery-content-visible || !root.gallery-mode ? 1 : 0; - animate opacity { - duration: 60ms; - easing: ease-in-out; - enabled: root.gallery-mode; - } Rectangle { width: parent.width; @@ -1325,6 +1319,7 @@ export component MainWindow inherits Window { tile-width <=> root.gallery-tile-width; network-open => { root.gallery-network-open = true; } controlling: root.gallery-controlling; + input-suspended: root.gallery-transitioning; pointer-input(button, kind, x, y) => { root.gallery-pointer(button, kind, x, y); } scroll-input(dx, dy, x, y) => { root.gallery-scroll(dx, dy, x, y); } key-input(text, pressed, ctrl, alt, shift, meta) => { root.gallery-key(text, pressed, ctrl, alt, shift, meta); } diff --git a/docs/deskhub.md b/docs/deskhub.md index 53ca92d2..636fd99d 100644 --- a/docs/deskhub.md +++ b/docs/deskhub.md @@ -4,7 +4,7 @@ 桌面墙内容区不再重复显示“桌面墙”标题,顶部仅保留一行工具栏,距窗口内容区顶部 12 逻辑像素;分组、设备数量、“添加”、“选择/完成”、纯文字“精简模式”按钮和全屏图标共用这一行,不显示菜单及消息、更新角标。“添加”、“选择”和“精简模式”默认统一使用浅灰背景、灰色边框及相同的悬停、按下样式;进入选择或精简模式后,对应按钮以蓝色高亮表示当前状态。按钮按当前语言的文字宽度加统一左右内边距自适应,普通文字按钮宽度至少为 48 逻辑像素;内边距默认为 12 逻辑像素,窗口内容区宽度小于 720 时统一收紧为 8,保证最窄窗口下的多语言操作和已选数量可见。下拉按钮额外预留箭头及间距;“选择/完成”和“精简模式/退出精简”按两种状态中较宽的文字预留空间,切换时保持宽度稳定。“选择/完成”固定在视图控制按钮左侧,设备列表紧接工具栏下方,释放原有标题行占用的空间。桌面墙可以单独调整尺寸及全屏,不改变连接窗口的尺寸和位置。全局设置从主窗口菜单进入;设备操作和桌面墙连接的密码弹窗显示在桌面墙窗口内。 -全屏按钮的提示绘制在设备网格和放大视图上方,靠右对齐并保持在窗口内。全屏按钮通过短暂淡出、等待窗口尺寸稳定、淡入完成切换,常规总时长约 0.2 秒;过渡期间屏蔽设备操作与重复全屏点击。窗口尺寸变化会立即更新网格,避免在新窗口中先显示旧的行列布局。过渡复用现有 UI 更新循环,不启动嵌套事件循环;失败或超时会恢复内容与交互。 +全屏按钮的提示绘制在设备网格和放大视图上方,靠右对齐并保持在窗口内。全屏切换期间持续显示设备列表和已有预览画面,不再淡出内容区或等待淡入;窗口恢复完成后,在下一次 UI 更新中立即发起全屏切换,窗口尺寸变化会及时更新网格。尺寸稳定前暂时屏蔽设备操作与重复全屏点击,并暂停保存过渡尺寸。进入和退出全屏保留当前接管设备及键盘焦点,仅在切换前释放已按下的键鼠,切换期间不发送新的远程输入;尺寸稳定后直接继续操控,无需再次点击接管。真正切换到其他窗口、关闭桌面墙或设备断开时仍会释放操控。切换复用现有 UI 更新循环,不启动嵌套事件循环;失败或超时会解除交互屏蔽。 可见的普通窗口若低于连接页最小尺寸,桌面墙恢复上次有效大小,连接页恢复固定大小;最小化、隐藏和全屏窗口不会触发这一恢复。窗口恢复以实际尺寸确认完成,期间不记录过渡尺寸;macOS 的最大化恢复使用无动画原生窗口调整,避免系统缩放动画嵌套事件循环。