diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index 582956c9..029af119 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -1276,7 +1276,8 @@ void GuiApplication::InitializeUi() { int(saved.at("connection_y").get() * scale)}); const float tile_width = saved.value("tile_width", 216.0f); if (std::isfinite(tile_width)) - ui_->wall->set_gallery_tile_width(std::clamp(tile_width, 184.0f, 960.0f)); + // The rendered grid constrains the restored width to available space. + ui_->wall->set_gallery_tile_width(std::max(tile_width, 184.0f)); ui_->connection_window.BeginRestore(); } } catch (...) { @@ -5369,6 +5370,13 @@ void GuiApplication::SyncGallery() { SaveGalleryWindowState(); auto& wall = ui_->gallery; + std::unordered_map group_labels; + for (const auto& member : wall.members) { + if (member.group.empty()) continue; + auto& label = group_labels[member.id]; + if (!label.empty()) label += " · "; + label += member.group; + } std::vector groups{ UiText(tr("全部设备", "All devices", "Все устройства"))}; for (const auto& group : wall.groups) groups.push_back(UiText(group)); @@ -5422,14 +5430,29 @@ void GuiApplication::SyncGallery() { ui_->wall->get_gallery_video_width(), ui_->wall->get_gallery_video_height(), window.scale_factor()) : gallery::PreviewResolution{}; + const auto gallery_preview_viewport = visible + ? gallery::PreviewResolution::Viewport( + ui_->wall->get_gallery_preview_width(), + ui_->wall->get_gallery_preview_height(), window.scale_factor()) + : gallery::PreviewResolution{}; std::set preview_ids; std::vector tiles; const auto make_tile = [&](const std::string& id, bool preview) { ui::GalleryTile tile{}; tile.id = UiText(id); tile.name = UiText(name(id)); + tile.group_names = UiText(group_labels[id]); tile.selected = wall.selected.count(id); auto props = FindRemoteSession(id); + for (const auto& [_, recent] : recent_connections_) { + if (recent.remote_id != id) continue; + tile.host_name = UiText(recent.remote_host_name); + break; + } + if (props && tile.host_name.empty()) { + std::lock_guard lock(props->remote_version_mutex_); + tile.host_name = UiText(props->remote_host_name_); + } tile.connected = props && props->connection_status_ == ConnectionStatus::Connected && !props->closing_; @@ -5455,9 +5478,10 @@ void GuiApplication::SyncGallery() { props->connection_status_.load(), localization_language_index_)); if (preview && visible && tile.connected) { preview_ids.insert(id); - props->gallery_preview_width_ = ui_->gallery_focus == id - ? std::max({320, gallery_viewport.width, gallery_viewport.height}) - : 320; + const auto viewport = ui_->gallery_focus == id + ? gallery_viewport : gallery_preview_viewport; + props->gallery_preview_width_ = + std::max({320, viewport.width, viewport.height}); std::lock_guard lock(props->gallery_frame_mutex_); if (props->gallery_rgba_) { auto& cached = ui_->gallery_previews[id]; @@ -5586,10 +5610,13 @@ void GuiApplication::SyncGallery() { const bool stream_visible = stream_window_visible && props->tab_opened_ && id == focused_remote_id_; if (props->preview_resolution_supported_) { + const auto wall_viewport = !preview_ids.count(id) + ? gallery::PreviewResolution{} + : ui_->gallery_focus == id ? gallery_viewport + : gallery_preview_viewport; const auto resolution = props->preview_resolution_.Update( wall.stream_settings.Resolution( - props->wall_managed_, ui_->gallery_focus == id - ? gallery_viewport : gallery::PreviewResolution{}, + props->wall_managed_, wall_viewport, stream_visible ? stream_viewport : gallery::PreviewResolution{}), now, needs_sync); demand.version = 2; diff --git a/apps/desktop/src/gui/features/gallery/gallery_stream_settings.h b/apps/desktop/src/gui/features/gallery/gallery_stream_settings.h index 16df2fac..f0aa4a88 100644 --- a/apps/desktop/src/gui/features/gallery/gallery_stream_settings.h +++ b/apps/desktop/src/gui/features/gallery/gallery_stream_settings.h @@ -77,7 +77,7 @@ struct StreamSettings { PreviewResolution stream_view) const { if (!wall_managed) return {}; if (resolution != 0 && Valid()) return kResolutions[resolution]; - // A square bound limits the longest edge of an unexpanded wall tile. + // Keep a bounded fallback until a rendered viewport is available. if (!wall_view.width && !stream_view.width) return {320, 320}; return {std::max(wall_view.width, stream_view.width), std::max(wall_view.height, stream_view.height)}; diff --git a/apps/desktop/src/gui/ui/gallery_view.slint b/apps/desktop/src/gui/ui/gallery_view.slint index 639266e3..ec9fb8f6 100644 --- a/apps/desktop/src/gui/ui/gallery_view.slint +++ b/apps/desktop/src/gui/ui/gallery_view.slint @@ -8,6 +8,8 @@ import { CompactButton, SecondaryButton, FontAwesomeIcons, ImGuiCheckBox } from export struct GalleryTile { id: string, name: string, + host-name: string, + group-names: string, status: string, connected: bool, online: bool, @@ -29,29 +31,6 @@ export struct GalleryActionDevice { connected: bool, } -component DeviceDetailsTip inherits Rectangle { - in property device-name; - in property device-id; - in property presence; - in property available-height; - height: min(root.available-height, 58px); - background: #22252bf2; border-width: 1px; border-color: #ffffff38; border-radius: 5px; - clip: true; - tip-name := Text { - x: 8px; y: 8px; width: parent.width - 16px; - height: 14px; - text: root.device-name; color: white; font-size: 11px; overflow: elide; vertical-alignment: top; - } - Text { - x: 8px; y: tip-name.y + tip-name.height; width: parent.width - 16px; height: 14px; - text: "ID: " + root.device-id; color: white; font-size: 11px; overflow: elide; - } - Text { - x: 8px; y: tip-name.y + tip-name.height + 14px; width: parent.width - 16px; height: 14px; - text: root.presence; color: white; font-size: 11px; overflow: elide; - } -} - export component GalleryView inherits Rectangle { in property controlling: false; in property simplified: false; @@ -61,18 +40,24 @@ export component GalleryView inherits Rectangle { private property preview-aspect-ratio: 16 / 9; private property card-inset: 6px; private property card-chrome-height: root.simplified ? root.card-inset * 2 : 36px; - private property max-card-width: max(184px, min(960px, grid.width, + // Keep the complete cell inside the current grid in both dimensions. + private property max-card-width: max(184px, min(grid.width, (grid.height - root.card-chrome-height) * root.preview-aspect-ratio + root.card-inset * 2)); private property card-width: clamp(root.tile-width * 1px, 184px, root.max-card-width); private property card-preview-height: (root.card-width - root.card-inset * 2) / root.preview-aspect-ratio; private property card-height: root.card-preview-height + root.card-chrome-height; - private property rail-preview-height: 126px / root.preview-aspect-ratio; + private property rail-preview-width: 126px; + private property rail-preview-height: root.rail-preview-width / root.preview-aspect-ratio; private property rail-card-height: root.rail-preview-height + (root.simplified ? 10px : 24px); private property focused-id: root.focused.id; private property device-tooltip-id; private property device-tooltip-text; - private property device-tooltip-x; - private property device-tooltip-y; + out property device-tooltip-x; + out property device-tooltip-y; + private property device-tooltip-tile; + // MainWindow renders the same tooltip panel for the wall and recent list. + out property hovered-device: root.device-tooltip-id == root.device-tooltip-tile.id + && !root.adding && !root.importing && !root.confirming ? root.device-tooltip-tile : {}; private property device-tooltip-visible: false; private property drag-source; private property dragged-tile; @@ -90,15 +75,15 @@ export component GalleryView inherits Rectangle { changed visible => { root.device-tooltip-id = ""; if !root.visible { root.resizing = false; root.cancel-device-drag(); add-menu.close-menu(); batch-menu.close-menu(); } } changed width => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); } changed height => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); } - changed window-active => { if !root.window-active { root.resizing = false; root.cancel-device-drag(); } } - changed page => { if root.page != root.drag-page { root.cancel-device-drag(); } } - changed group-index => { root.cancel-device-drag(); } + changed window-active => { if !root.window-active { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); } } + changed page => { root.device-tooltip-id = ""; if root.page != root.drag-page { root.cancel-device-drag(); } } + changed group-index => { root.device-tooltip-id = ""; root.cancel-device-drag(); } changed card-width => { root.cancel-device-drag(); } changed card-height => { root.cancel-device-drag(); } changed device-tooltip-id => { root.device-tooltip-visible = false; - device-tooltip-timer.running = root.device-tooltip-id != ""; - if root.device-tooltip-id != "" { device-tooltip-timer.restart(); } + device-tooltip-timer.running = root.device-tooltip-id == "resize-grid"; + if root.device-tooltip-id == "resize-grid" { device-tooltip-timer.restart(); } } device-tooltip-timer := Timer { interval: 450ms; running: false; @@ -110,6 +95,11 @@ export component GalleryView inherits Rectangle { // Pointer mapping must use the exact image viewport, including its inset. out property video-width: video.width / 1px; out property video-height: video.height / 1px; + // Non-focused previews use the grid cell or the focused view's rail. + out property preview-width: (root.focused.id == "" + ? root.card-width - root.card-inset * 2 : root.rail-preview-width) / 1px; + out property preview-height: (root.focused.id == "" + ? root.card-preview-height : root.rail-preview-height) / 1px; callback pointer-input(PointerEventButton, PointerEventKind, length, length); callback scroll-input(length, length, length, length); callback key-input(string, bool, bool, bool, bool, bool); @@ -152,6 +142,12 @@ export component GalleryView inherits Rectangle { background: white; changed adding => { if root.adding { root.action("release", ""); } } changed importing => { root.action(root.importing ? "release" : "recent-close", ""); } + function show-device-details(tile: GalleryTile, x: length, y: length) { + root.device-tooltip-tile = tile; + root.device-tooltip-x = x; + root.device-tooltip-y = y; + root.device-tooltip-id = tile.id; + } public function reset-transient() { root.resizing = false; root.cancel-device-drag(); @@ -330,8 +326,8 @@ export component GalleryView inherits Rectangle { root.tile-width = clamp(self.start-width + delta, 184px, root.max-card-width) / 1px; } changed has-hover => { self.show-hint(); } - changed mouse-x => { self.show-hint(); } - changed mouse-y => { self.show-hint(); } + changed mouse-x => { root.device-tooltip-x = grid.x + self.mouse-x; self.show-hint(); } + changed mouse-y => { root.device-tooltip-y = grid.y + self.mouse-y; self.show-hint(); } pointer-event(event) => { if event.button == PointerEventButton.left && event.kind == PointerEventKind.down { root.cancel-device-drag(); @@ -371,7 +367,7 @@ export component GalleryView inherits Rectangle { for tile[index] in root.tiles: tile-card := Rectangle { private property presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети") : tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно"); - private property details: tile.name + "\nID: " + tile.id + "\n" + self.presence; + private property details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence; accessible-role: button; accessible-label: self.details; accessible-action-default => { root.action(root.batch && !root.simplified ? "select" : "focus", tile.id); } changed details => { if tile-touch.has-hover { tile-touch.show-details(); } } @@ -394,11 +390,6 @@ export component GalleryView inherits Rectangle { horizontal-alignment: center; vertical-alignment: center; wrap: word-wrap; } - if root.device-tooltip-visible && root.device-tooltip-id == tile.id: DeviceDetailsTip { - x: 4px; y: parent.height - self.height - 4px; - width: min(320px, parent.width - 8px); available-height: parent.height - 8px; - device-name: tile.name; device-id: tile.id; presence: tile-card.presence; - } } Text { visible: !root.simplified; x: 9px; y: parent.height - 30px; @@ -424,7 +415,7 @@ export component GalleryView inherits Rectangle { function show-details() { // Avoid covering the per-device action buttons with a tooltip. if root.resizing || root.drag-source != "" || (grid.mouse-y - tile-card.y >= tile-card.height - 30px && !root.simplified) { root.device-tooltip-id = ""; return; } - root.device-tooltip-id = tile.id; + root.show-device-details(tile, grid.x + grid.mouse-x, grid.y + grid.mouse-y); } changed has-hover => { if self.has-hover { self.show-details(); } @@ -569,7 +560,7 @@ export component GalleryView inherits Rectangle { rail-card := Rectangle { private property presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети") : tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно"); - private property details: tile.name + "\nID: " + tile.id + "\n" + self.presence; + private property details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence; width: 136px; height: root.rail-card-height; border-radius: 5px; background: tile.online ? #f5f6f8 : DeviceCardStyle.offline-background; border-width: 1px; border-color: tile.online ? #d5dbe3 : DeviceCardStyle.offline-border; accessible-role: button; @@ -577,17 +568,12 @@ export component GalleryView inherits Rectangle { accessible-action-default => { root.action("focus", tile.id); } changed details => { if rail-touch.has-hover { rail-touch.show-details(); } } Rectangle { - x: 5px; y: 5px; width: 126px; height: root.rail-preview-height; background: tile.online ? #24272d : DeviceCardStyle.offline-preview; + x: 5px; y: 5px; width: root.rail-preview-width; height: root.rail-preview-height; background: tile.online ? #24272d : DeviceCardStyle.offline-preview; if tile.has-frame && tile.online: Image { width: parent.width; height: parent.height; source: tile.frame; image-fit: contain; opacity: tile.stale || !tile.connected ? 0.4 : 1; } if !root.simplified && (!tile.has-frame || tile.stale || !tile.connected): Text { text: tile.status; color: tile.online ? #e2e6ed : DeviceCardStyle.offline-placeholder; font-size: 10px; overflow: elide; horizontal-alignment: center; vertical-alignment: center; } - if root.device-tooltip-visible && root.device-tooltip-id == tile.id: DeviceDetailsTip { - x: 4px; y: parent.height - self.height - 4px; - width: parent.width - 8px; available-height: parent.height - 8px; - device-name: tile.name; device-id: tile.id; presence: rail-card.presence; - } } Text { visible: !root.simplified; @@ -597,7 +583,9 @@ export component GalleryView inherits Rectangle { } rail-touch := TouchArea { function show-details() { - root.device-tooltip-id = tile.id; + root.show-device-details(tile, + focus-view.x + rail.x + rail.viewport-x + rail-slot.x + rail-card.x + self.mouse-x, + focus-view.y + rail.y + rail.viewport-y + rail-slot.y + rail-card.y + self.mouse-y); } changed has-hover => { if self.has-hover { self.show-details(); } @@ -726,7 +714,7 @@ export component GalleryView inherits Rectangle { text: GalleryStrings.tr("换位未保存,请重试", "Couldn't save positions. Please try again.", "Не удалось сохранить порядок. Повторите попытку."); overflow: elide; } } - // Resize hints follow the pointer; device details stay inside their preview. + // Device details are rendered by MainWindow; resize hints stay local. if root.device-tooltip-visible && root.device-tooltip-id == "resize-grid": Rectangle { x: clamp(root.device-tooltip-x, 8px, parent.width - self.width - 8px); y: clamp(root.device-tooltip-y + self.height > footer.y ? root.device-tooltip-y - self.height - 8px : root.device-tooltip-y, 4px, parent.height - self.height - 4px); diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index 7756ff0c..3107a27b 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -311,6 +311,8 @@ export component MainWindow inherits Window { in property gallery-controlling: false; out property gallery-video-width: gallery.video-width; out property gallery-video-height: gallery.video-height; + out property gallery-preview-width: gallery.preview-width; + out property gallery-preview-height: gallery.preview-height; callback gallery-pointer(PointerEventButton, PointerEventKind, length, length); callback gallery-scroll(length, length, length, length); callback gallery-key(string, bool, bool, bool, bool, bool); @@ -531,13 +533,6 @@ export component MainWindow inherits Window { private property recent-tooltip-online: false; private property recent-tooltip-pointer-x: 0px; private property recent-tooltip-pointer-y: 0px; - private property recent-tooltip-width: 260px; - private property recent-tooltip-x: min( - root.width - root.recent-tooltip-width - 8px, - max(8px, - root.recent-tooltip-pointer-x > root.width * 0.7 - ? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px - : root.recent-tooltip-pointer-x + 12px)); changed menu-open => { if root.menu-open { root.gallery-action("release", ""); } } changed connection-dialog-open => { if root.connection-dialog-open { root.gallery-action("release", ""); } } @@ -1433,16 +1428,25 @@ export component MainWindow inherits Window { } } - // Match the ImGui recent-connection tooltip while keeping it outside the - // clipped ScrollView. It is deliberately limited to the thumbnail area. - // Stay under the modal layer (z >= 39): a hover tooltip that outlives the - // pointer can otherwise cover a dialog button and hide it from the user. - if root.recent-tooltip-visible && !root.connection-dialog-open: Rectangle { - x: root.recent-tooltip-x; + // Shared device details sit outside the clipped recent list and wall rail, + // below the modal layer (z >= 39). + if (root.gallery-mode ? gallery.hovered-device.id != "" : root.recent-tooltip-visible) + && !root.connection-dialog-open: device-tip := Rectangle { + private property device-name: root.gallery-mode ? gallery.hovered-device.name : root.recent-tooltip-name; + private property host-name: root.gallery-mode ? gallery.hovered-device.host-name : root.recent-tooltip-host; + private property device-id: root.gallery-mode ? gallery.hovered-device.id : root.recent-tooltip-id; + private property group-names: root.gallery-mode ? gallery.hovered-device.group-names : root.recent-tooltip-groups; + private property online: root.gallery-mode ? gallery.hovered-device.online : root.recent-tooltip-online; + private property presence-known: !root.gallery-mode || gallery.hovered-device.presence-known; + private property pointer-x: root.gallery-mode ? gallery.x + gallery.device-tooltip-x : root.recent-tooltip-pointer-x; + private property pointer-y: root.gallery-mode ? gallery.y + gallery.device-tooltip-y : root.recent-tooltip-pointer-y; + x: min(root.width - self.width - 8px, + max(8px, self.pointer-x > root.width * 0.7 + ? self.pointer-x - self.width - 12px : self.pointer-x + 12px)); y: min(root.height - root.titlebar-height - self.height - 8px, - max(8px, root.recent-tooltip-pointer-y - self.height - 8px)); - width: root.recent-tooltip-width; - height: (root.recent-tooltip-host != "" && root.recent-tooltip-host != root.recent-tooltip-name + max(8px, self.pointer-y - self.height - 8px)); + width: 260px; + height: (self.host-name != "" && self.host-name != self.device-name ? 80px : 64px) + tooltip-groups.preferred-height + 2px; background: #22252bf2; border-width: 1px; @@ -1454,34 +1458,35 @@ export component MainWindow inherits Window { padding: 8px; spacing: 2px; Text { - text: UiStrings.device-name + ": " + root.recent-tooltip-name; + text: UiStrings.device-name + ": " + device-tip.device-name; color: white; font-size: ImGuiFontStyle.small; overflow: elide; } - if root.recent-tooltip-host != "" && root.recent-tooltip-host != root.recent-tooltip-name: Text { - text: root.recent-tooltip-host; + if device-tip.host-name != "" && device-tip.host-name != device-tip.device-name: Text { + text: device-tip.host-name; color: #e1e4e8; font-size: ImGuiFontStyle.small; overflow: elide; } Text { - text: UiStrings.remote-id + ": " + root.recent-tooltip-id; + text: UiStrings.remote-id + ": " + device-tip.device-id; color: #e1e4e8; font-size: ImGuiFontStyle.small; overflow: elide; } tooltip-groups := Text { text: GalleryStrings.tr("分组", "Groups", "Группы") + ": " + - (root.recent-tooltip-groups == "" ? GalleryStrings.tr("未分组", "Ungrouped", "Без группы") : root.recent-tooltip-groups); + (device-tip.group-names == "" ? GalleryStrings.tr("未分组", "Ungrouped", "Без группы") : device-tip.group-names); color: #e1e4e8; font-size: ImGuiFontStyle.small; wrap: word-wrap; vertical-stretch: 0; } Text { - text: root.recent-tooltip-online ? UiStrings.online : UiStrings.offline; - color: root.recent-tooltip-online ? #62d98b : #d1d5db; + text: device-tip.online ? UiStrings.online : device-tip.presence-known ? UiStrings.offline + : GalleryStrings.tr("未知", "Unknown", "Неизвестно"); + color: device-tip.online ? #62d98b : #d1d5db; font-size: ImGuiFontStyle.small; } }