Shell & navigation: - Three-state sidebar (open/mini/closed); collapse cycles via header button. - Section nav as right-flyouts (sidebar) + compact header nav (closed) + full header tabs (nav-in-sidebar off), via a shared HeaderMenuOverlay. - Collapsed-folder right-flyout popup (max 4 + scrollbar), Rust-populated. - Thin auto-hiding sidebar scrollbar pinned to the edge (Flickable, drag-safe). - Slack-style bezel: rounded inset content panel over surface-card chrome. - Header restructure: nav buttons, window-centered search, app menu. Themes & components: - Live theme switching: Dark / OLED Black / Tokyo Night / System (std-widgets). - SegmentedTabBar (Discovery-style) with count badges for Library + Local Library. - MP3 quality-badge tier (format-first classification) across badges. - QbzSelect (Group/Sort) trimmed 20px. Behavior fixes: - Artist + Local Library Folder cover art now reach the rendered (visible/ grouped) models, not just the source set. - Favorites Playlists/Labels random buttons. - Playlist track click plays from the clicked track in visible order. - Dropdown idle-close pauses while the cursor rests on the menu. cc
164 lines
7.2 KiB
Plaintext
164 lines
7.2 KiB
Plaintext
// In-window overlay that draws the open header nav-tab dropdown.
|
|
//
|
|
// Why this exists: the four header tabs used to each own a PopupWindow.
|
|
// A PopupWindow grabs the pointer while open, so hovering a sibling tab
|
|
// delivered no hover event — the menus could not be hover-switched, the
|
|
// new menu only appeared after the old one's idle timer fired.
|
|
//
|
|
// This overlay is a plain Rectangle, so it does NOT grab the pointer:
|
|
// the header tabs stay hoverable while a menu is open. It must be the
|
|
// LAST child of the AppShell root so it draws on top of the content
|
|
// area (the header itself is only 48px tall — a menu hosted inside it
|
|
// would be clipped by the content region below).
|
|
//
|
|
// Tabs publish their identity into HeaderMenuState (open-index, anchor-x,
|
|
// items, home-item). Hovering a different tab overwrites the global, so
|
|
// the menu switches instantly. open-index == -1 means nothing is open.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Radius } from "../foundation/radius.slint";
|
|
import { Layout } from "../foundation/layout.slint";
|
|
import { HeaderMenuState, NavState, ContentView, ShellState } from "../state.slint";
|
|
import { NavMenuItem } from "NavMenuItem.slint";
|
|
|
|
export component HeaderMenuOverlay inherits Rectangle {
|
|
// Fills the whole shell; only visible (and only consumes events)
|
|
// while a menu is open.
|
|
visible: HeaderMenuState.open-index >= 0;
|
|
|
|
// Bubbles a row's destination route (e.g. "favorites-tracks") up
|
|
// to the shell so Rust can run the navigation + data load.
|
|
callback navigate(string);
|
|
|
|
// True while the pointer rests on the panel body (set by panel-ta, which
|
|
// lives inside the conditional panel and so can't be referenced directly).
|
|
property <bool> panel-hovered: false;
|
|
// Which row is hovered (-1 = none). Rows don't overlap, so one at a time.
|
|
property <int> hovered-row: -1;
|
|
// True while the pointer rests anywhere on the menu (panel body OR a row).
|
|
// The idle timer is paused while this holds, so resting the cursor on the
|
|
// menu — even without moving it — never lets the menu close.
|
|
property <bool> menu-hovered:
|
|
root.panel-hovered || root.hovered-row >= 0 || HeaderMenuState.trigger-hovered;
|
|
|
|
// Closes the menu.
|
|
function close-menu() {
|
|
HeaderMenuState.open-index = -1;
|
|
}
|
|
|
|
// Clear the panel/row hover trackers whenever the open menu changes, so a
|
|
// stale state from the previous menu can't wrongly pause the timer.
|
|
// NOTE: trigger-hovered is deliberately NOT reset here — the trigger owns
|
|
// it (resetting it on open would clear the flag the instant the hovered
|
|
// trigger opens the menu, defeating the whole purpose).
|
|
changed open-index => {
|
|
root.hovered-row = -1;
|
|
root.panel-hovered = false;
|
|
}
|
|
property <int> open-index: HeaderMenuState.open-index;
|
|
|
|
// Closes the menu after 1s idle (project rule). The timer runs only while a
|
|
// menu is open AND the pointer is NOT over it — so hovering the menu pauses
|
|
// the countdown, and leaving it restarts a fresh 1s.
|
|
idle-timer := Timer {
|
|
interval: 1000ms;
|
|
running: HeaderMenuState.open-index >= 0 && !root.menu-hovered;
|
|
triggered => {
|
|
HeaderMenuState.open-index = -1;
|
|
}
|
|
}
|
|
|
|
// Transparent scrim — a click outside the panel closes the menu.
|
|
// It starts BELOW the header so the tabs stay hoverable while a menu
|
|
// is open: a full-window scrim would intercept the tabs' hover
|
|
// events and block hover-switching, exactly like the old
|
|
// PopupWindow's pointer grab did.
|
|
if HeaderMenuState.open-index >= 0: TouchArea {
|
|
// Header dropdowns: scrim starts below the header so the tabs stay
|
|
// hoverable. Sidebar flyouts: scrim starts right of the sidebar so the
|
|
// section rows stay clickable.
|
|
x: HeaderMenuState.from-sidebar ? ShellState.sidebar-rendered-width : 0px;
|
|
y: HeaderMenuState.from-sidebar ? 0px : Layout.header-height;
|
|
width: parent.width - self.x;
|
|
height: parent.height - self.y;
|
|
clicked => {
|
|
root.close-menu();
|
|
}
|
|
}
|
|
|
|
// The menu panel — same styling as the former PopupWindow content:
|
|
// surface-main background, 1px border-muted, Radius.sm, drop shadow.
|
|
if HeaderMenuState.open-index >= 0: panel := Rectangle {
|
|
x: HeaderMenuState.anchor-x;
|
|
// Header dropdown: snug under the tabs (~45px). Sidebar flyout: aligned
|
|
// to the row's window y, opening to the right.
|
|
y: HeaderMenuState.from-sidebar ? HeaderMenuState.anchor-y : 45px;
|
|
width: 188px;
|
|
height: panel-box.preferred-height;
|
|
|
|
panel-box := Rectangle {
|
|
background: Theme.surface-main;
|
|
border-radius: Radius.sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-muted;
|
|
drop-shadow-blur: 24px;
|
|
drop-shadow-color: Theme.card-shadow;
|
|
drop-shadow-offset-y: 6px;
|
|
|
|
// Keeps the pointer "inside" the menu so the scrim's click does
|
|
// not pass through, and reports panel-body hover so the idle timer
|
|
// pauses while the cursor rests on the menu.
|
|
panel-ta := TouchArea {
|
|
changed has-hover => {
|
|
root.panel-hovered = self.has-hover;
|
|
}
|
|
}
|
|
|
|
VerticalLayout {
|
|
padding-top: 6px;
|
|
padding-bottom: 6px;
|
|
// Optional section-name header + hairline (used by the compact
|
|
// header nav shown when the sidebar is fully closed).
|
|
if HeaderMenuState.title != "": HorizontalLayout {
|
|
padding-left: 14px;
|
|
padding-right: 14px;
|
|
Text {
|
|
text: HeaderMenuState.title;
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
height: 26px;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
if HeaderMenuState.title != "": Rectangle {
|
|
height: 1px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
for entry[idx] in HeaderMenuState.items: NavMenuItem {
|
|
label: entry.label;
|
|
icon: entry.icon;
|
|
has-glyph: entry.has-glyph;
|
|
clicked => {
|
|
if (entry.route != "") {
|
|
root.navigate(entry.route);
|
|
} else if (idx == HeaderMenuState.home-item) {
|
|
NavState.view = ContentView.home;
|
|
}
|
|
root.close-menu();
|
|
}
|
|
// Track per-row hover so the idle timer stays paused while
|
|
// the cursor rests on a row (not just while it moves).
|
|
hover-changed(h) => {
|
|
if (h) {
|
|
root.hovered-row = idx;
|
|
} else if (root.hovered-row == idx) {
|
|
root.hovered-row = -1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|