diff --git a/crates/qbz-slint/ui/shell/AppShell.slint b/crates/qbz-slint/ui/shell/AppShell.slint index 02265c79d..a39f85653 100644 --- a/crates/qbz-slint/ui/shell/AppShell.slint +++ b/crates/qbz-slint/ui/shell/AppShell.slint @@ -12,6 +12,7 @@ import { Sidebar } from "Sidebar.slint"; import { HeaderBar } from "HeaderBar.slint"; import { HeaderMenuOverlay } from "HeaderMenuOverlay.slint"; import { SidebarFolderPopup } from "SidebarFolderPopup.slint"; +import { SidebarTooltip } from "SidebarTooltip.slint"; import { PlaylistPickerModal } from "../primitives/PlaylistPickerModal.slint"; import { CreatePlaylistModal } from "../primitives/CreatePlaylistModal.slint"; import { EditPlaylistModal } from "../primitives/EditPlaylistModal.slint"; @@ -299,6 +300,9 @@ export component AppShell inherits Rectangle { // Collapsed-sidebar folder flyout (mini state). Drawn above the content. SidebarFolderPopup { } + // Collapsed-sidebar hover tooltip (mini state) — name + folder count. + SidebarTooltip { } + // Global "Add to playlist" picker overlay, opened from any track // context menu. PlaylistPickerModal { } diff --git a/crates/qbz-slint/ui/shell/Sidebar.slint b/crates/qbz-slint/ui/shell/Sidebar.slint index 2f7d2b742..7885082fb 100644 --- a/crates/qbz-slint/ui/shell/Sidebar.slint +++ b/crates/qbz-slint/ui/shell/Sidebar.slint @@ -15,7 +15,7 @@ import { QbzIcon } from "../primitives/QbzIcon.slint"; import { SidebarPlaylistCollage } from "../primitives/SidebarPlaylistCollage.slint"; import { ContextMenu } from "../primitives/ContextMenu.slint"; import { ContextMenuItem } from "../primitives/ContextMenuItem.slint"; -import { ShellState, SidebarState, SidebarActions, SidebarEntry, DragState, HeaderMenuState, NavMenuEntry, SidebarFolderPopupState } from "../state.slint"; +import { ShellState, SidebarState, SidebarActions, SidebarEntry, DragState, HeaderMenuState, NavMenuEntry, SidebarFolderPopupState, SidebarTooltipState } from "../state.slint"; import { ScrollView, LineEdit } from "std-widgets.slint"; // A single context-menu item row. @@ -163,6 +163,29 @@ component SidebarRow inherits Rectangle { changed drag-py => { root.recompute-drop(); } changed drag-on => { root.recompute-drop(); } + // Collapsed (mini) hover tooltip: the row shows only an icon, so reveal the + // name (folders also show their playlist count) in a bubble to the right. + // Same Flickable absolute-position correction as the folder flyout anchor. + function show-tooltip() { + if (!ShellState.sidebar-mini) { + return; + } + SidebarTooltipState.id = root.entry.id; + SidebarTooltipState.text = root.is-folder + ? root.entry.name + " (" + root.entry.count + ")" + : root.entry.name; + SidebarTooltipState.anchor-x = root.absolute-position.x - root.x + root.width + 6px; + SidebarTooltipState.anchor-y = root.absolute-position.y - root.y; + SidebarTooltipState.open = true; + } + // Close only if this row still owns the tooltip (race-safe when the pointer + // slides directly from one row to the next). + function hide-tooltip() { + if (SidebarTooltipState.id == root.entry.id) { + SidebarTooltipState.open = false; + } + } + // True when this playlist row should show its micro-collage instead // of the generic glyph (opt-out + at least one cover available). property use-collage: !root.is-folder @@ -276,8 +299,16 @@ component SidebarRow inherits Rectangle { mouse-cursor: pointer; changed has-hover => { root.hover-changed(self.has-hover); + if (self.has-hover) { + root.show-tooltip(); + } else { + root.hide-tooltip(); + } } clicked => { + // The flyout / navigation takes over from here — drop the tooltip + // so it doesn't linger behind the opening flyout. + SidebarTooltipState.open = false; if (root.is-folder) { // Collapsed (mini) sidebar: a folder opens a flyout to the // right instead of expanding downward (Tauri parity). diff --git a/crates/qbz-slint/ui/shell/SidebarTooltip.slint b/crates/qbz-slint/ui/shell/SidebarTooltip.slint new file mode 100644 index 000000000..f133abf0a --- /dev/null +++ b/crates/qbz-slint/ui/shell/SidebarTooltip.slint @@ -0,0 +1,44 @@ +// Collapsed-sidebar hover tooltip. When the sidebar is in the mini (icon-only) +// state, hovering a folder or top-level playlist row surfaces its name in a +// small bubble to the right (folders append their playlist count). Mirrors the +// native `title` Tauri puts on the collapsed buttons. Rendered at the AppShell +// level because the sidebar clips its own overflow. Visual only (no TouchArea), +// so it never blocks the pointer. + +import { Theme } from "../foundation/semantic-colors.slint"; +import { Radius } from "../foundation/radius.slint"; +import { SidebarTooltipState, ShellState } from "../state.slint"; + +export component SidebarTooltip inherits Rectangle { + // Auto-hides if the sidebar leaves the mini state while a tooltip lingers. + visible: SidebarTooltipState.open && ShellState.sidebar-mini; + + if SidebarTooltipState.open && ShellState.sidebar-mini: bubble := Rectangle { + x: SidebarTooltipState.anchor-x; + // Center the bubble against the 32px row. + y: SidebarTooltipState.anchor-y + (32px - self.height) / 2; + width: row.preferred-width; + height: row.preferred-height; + background: Theme.surface-elevated; + border-radius: Radius.sm; + border-width: 1px; + border-color: Theme.border-muted; + drop-shadow-blur: 16px; + drop-shadow-color: Theme.card-shadow; + drop-shadow-offset-y: 4px; + + row := HorizontalLayout { + padding-left: 10px; + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; + Text { + text: SidebarTooltipState.text; + color: Theme.text-primary; + font-size: 12px; + font-weight: 500; + vertical-alignment: center; + } + } + } +} diff --git a/crates/qbz-slint/ui/state.slint b/crates/qbz-slint/ui/state.slint index 2b0b004de..2b7ac4095 100644 --- a/crates/qbz-slint/ui/state.slint +++ b/crates/qbz-slint/ui/state.slint @@ -818,6 +818,20 @@ export global SidebarFolderPopupState { in property <[SidebarEntry]> playlists: []; } +// Collapsed (mini) sidebar hover tooltip — the row shows only an icon, so a +// hover bubble surfaces the name (folders also show their playlist count). +// Mirrors Tauri's native `title` on the collapsed folder/playlist buttons. +// Rendered at the AppShell level because the sidebar clips its overflow; the +// anchor is the row's true window position (the collapsed rows live inside a +// Flickable, so SidebarRow corrects the absolute-position double-count). +export global SidebarTooltipState { + in-out property open: false; + in-out property id; // owning row id (race-safe leave) + in-out property text; // "Folder (N)" or "Playlist name" + in-out property anchor-x; // window x (sidebar right edge) + in-out property anchor-y; // window y (row top) +} + // The sidebar's playlists + folders. Populated by crate::sidebar on // shell entry and after create/delete/move. export global SidebarState {