feat(slint): hover tooltip on collapsed-sidebar folder/playlist rows

In the mini (icon-only) sidebar the rows show no name. Add a hover bubble
to the right of each folder/top-level-playlist row surfacing the name
(folders also append their playlist count), mirroring the native title
Tauri puts on its collapsed buttons. Rendered at the AppShell level
(SidebarTooltip + SidebarTooltipState) because the sidebar clips its
overflow; anchored with the same Flickable absolute-position correction as
the folder flyout. Closes on click so it does not linger behind the flyout.

cc
This commit is contained in:
vicrodh committed 2026-05-30 19:48:29 -06:00
1 parent 4c0a849371
commit 63a80f49dd
4 files changed
+94 -1

No files matched your search

+4
View File
@@ -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 { }
+32 -1
View File
@@ -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 <bool> 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).
@@ -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;
}
}
}
}
+14
View File
@@ -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 <bool> open: false;
in-out property <string> id; // owning row id (race-safe leave)
in-out property <string> text; // "Folder (N)" or "Playlist name"
in-out property <length> anchor-x; // window x (sidebar right edge)
in-out property <length> 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 {