feat(slint): jump-nav scroll-to-section + hide empty tabs + UX fixes
Brings the JUMP TO bar from a visual placeholder to a working nav,
plus two interaction fixes the user flagged on the previous build.
JUMP TO bar — functional scroll-to:
- JumpNavTab gains an `anchor-y: length` field. JumpNavBar's
tab-clicked callback now emits (id, anchor-y); the parent
(ArtistPageView) scrolls page-flickable to land the section
just below the sticky bar.
- Tab list is built in Rust (`build_jump_tabs` in artist.rs)
from the artist's actual data and pushed to a new
`ArtistState.jump-tabs` model. Tabs are emitted only for
sections that exist (top tracks present, "Discography" /
"EPs & Singles" / "Live Albums" / "Compilations" / "Others"
only when the corresponding release_sections entry is there);
the Playlists tab — not implemented yet — never appears.
- anchor-y estimates use layout-derived constants per section
type (popular-tracks header + visible row count, release grid
headers + ceil(album_count / 5) rows). Not pixel-perfect for
long bios or grids that wrap differently, but lands the
viewport inside the right section.
- Removed the hardcoded ArtistPageView property — tabs and
visibility are now data-driven.
Sticky positioning — dynamic:
- jump-nav.natural-top now binds to
`body-row.absolute-position.y - page-flickable.absolute-y -
self.height`, so the bar always sits flush in the gap above
the body row regardless of artist-header height (long-bio
artists stop pushing the row out from under the bar).
- Sidebar natural-top follows the same body-row anchor, so it
too tracks where the body actually starts.
Search input UX:
- The outer search-icon slot is now removed from layout (via
`if !search-open`, not `visible:`) when the input opens, so
the expanded input runs all the way to the bar's right
padding — right under the user's cursor where the lupa just
was, no leftover gap on the right.
- `changed search-open` auto-focuses the inline TextInput, so
typing starts immediately after clicking the lupa with no
second click in the input.
cc
This commit is contained in:
4 files changed
+146
-38
No files matched your search
@@ -279,17 +279,6 @@ export component ArtistPageView inherits Rectangle {
|
||||
// future pass will compute it from scroll position so the bar
|
||||
// self-tracks the visible section.
|
||||
property <string> active-jump-tab: "popular-tracks";
|
||||
// Static tab list — same labels Tauri uses. The Slint tabs trigger
|
||||
// scroll-to-section via tab-clicked; until the section anchors are
|
||||
// wired, the click just updates the active state.
|
||||
property <[JumpNavTab]> jump-tabs: [
|
||||
{ id: "about", label: "About" },
|
||||
{ id: "popular-tracks", label: "Popular Tracks" },
|
||||
{ id: "discography", label: "Discography" },
|
||||
{ id: "eps-singles", label: "EPs & Singles" },
|
||||
{ id: "compilations", label: "Compilations" },
|
||||
{ id: "playlists", label: "Playlists" },
|
||||
];
|
||||
// Width handed down to the bio paragraph. The artist header sits
|
||||
// above the body row where the sidebar lives, so the bio never
|
||||
// needs to shrink for the sidebar — it always uses the full
|
||||
@@ -495,16 +484,22 @@ export component ArtistPageView inherits Rectangle {
|
||||
// release grids) flanked by an empty 300px reservation when
|
||||
// the network sidebar is open, so the actual sidebar
|
||||
// overlay (rendered below) never covers the page content.
|
||||
HorizontalLayout {
|
||||
// jump-nav.natural-top binds to `body-row.absolute-position.y
|
||||
// - 44px`, so the bar always sits flush in the gap above
|
||||
// this row regardless of how tall the artist header above
|
||||
// it grew (long bios push the body row down with no manual
|
||||
// tuning).
|
||||
body-row := HorizontalLayout {
|
||||
spacing: 0px;
|
||||
main-col := VerticalLayout {
|
||||
horizontal-stretch: 1;
|
||||
spacing: 0px;
|
||||
|
||||
// Reservation slot for the JUMP TO bar — the bar is an
|
||||
// absolute-positioned overlay, so the body column has to
|
||||
// skip its 44px (plus a small visual gap above and
|
||||
// below) for content to land below it, not behind it.
|
||||
// absolute-positioned overlay (sticky-positioned above
|
||||
// this row), so the body column has to skip its 44px
|
||||
// height plus a small visual gap for content to land
|
||||
// below it rather than behind it.
|
||||
Rectangle { height: 52px; }
|
||||
|
||||
if ArtistState.loading && ArtistState.top-tracks.length == 0: Rectangle {
|
||||
@@ -689,20 +684,29 @@ export component ArtistPageView inherits Rectangle {
|
||||
// y is the row just below the artist header in the page layout
|
||||
// (NavButtons + spacer + artist header + small gap, ~290px).
|
||||
jump-nav := JumpNavBar {
|
||||
// Sits in the 52px reservation slot at the top of the body
|
||||
// column (artist header + 22 spacer + 4 gap = 291). Sticks
|
||||
// to viewport top once the user scrolls past the header.
|
||||
property <length> natural-top: 291px;
|
||||
// Dynamic anchor — sits 44px above the body row, regardless
|
||||
// of how tall the artist header grew. As the user scrolls
|
||||
// past the header, the bar pins to viewport top
|
||||
// (sticky-top = 0).
|
||||
property <length> natural-top:
|
||||
body-row.absolute-position.y
|
||||
- page-flickable.absolute-position.y
|
||||
- self.height;
|
||||
property <length> sticky-top: 0px;
|
||||
|
||||
x: 0;
|
||||
y: max(self.natural-top + page-flickable.viewport-y, self.sticky-top);
|
||||
width: root.width;
|
||||
tabs: root.jump-tabs;
|
||||
tabs: ArtistState.jump-tabs;
|
||||
active-tab-id: root.active-jump-tab;
|
||||
search-open: root.search-visible;
|
||||
tab-clicked(id) => {
|
||||
tab-clicked(id, anchor-y) => {
|
||||
root.active-jump-tab = id;
|
||||
// Scroll the page so the target section lands right
|
||||
// below the sticky JUMP TO bar. anchor-y is the
|
||||
// target's page-local y (content-coords); subtract
|
||||
// self.height to put the section just below the bar.
|
||||
page-flickable.viewport-y = -(anchor-y - self.height);
|
||||
}
|
||||
search-clicked => {
|
||||
root.search-visible = !root.search-visible;
|
||||
@@ -723,12 +727,13 @@ export component ArtistPageView inherits Rectangle {
|
||||
// bottom edge is always flush with the now-playing bar
|
||||
// (height = root.height - y), so no gap appears when sticky.
|
||||
if NetworkSidebarState.open: sidebar-panel := Rectangle {
|
||||
// 335px = jump-nav natural-top (291) + jump-nav height
|
||||
// (44). Starts the sidebar flush with the bottom edge of
|
||||
// the JUMP TO bar, never sliding behind it. As the user
|
||||
// scrolls down, y tracks page-flickable.viewport-y until
|
||||
// it hits sticky-top.
|
||||
property <length> natural-top: 335px;
|
||||
// Anchored to the body row's actual top, so the sidebar
|
||||
// always starts flush with the bottom of the JUMP TO bar
|
||||
// (which itself sits 44px above the body row). Matches
|
||||
// long-bio artist pages where the header pushes
|
||||
// everything further down.
|
||||
property <length> natural-top:
|
||||
body-row.absolute-position.y - page-flickable.absolute-position.y;
|
||||
// 44px parks the sidebar just below the sticky JUMP TO
|
||||
// bar (jump-nav's height) once the header scrolls away.
|
||||
property <length> sticky-top: 44px;
|
||||
|
||||
@@ -21,6 +21,11 @@ import { QbzIcon } from "QbzIcon.slint";
|
||||
export struct JumpNavTab {
|
||||
id: string,
|
||||
label: string,
|
||||
// Page-local y the tab scrolls to when clicked. Measured from the
|
||||
// top of the Flickable content (not the viewport), so the parent
|
||||
// can set `viewport-y = -(anchor-y - sticky-bar-height)` to land
|
||||
// the section just below the sticky bar.
|
||||
anchor-y: length,
|
||||
}
|
||||
|
||||
export component JumpNavBar inherits Rectangle {
|
||||
@@ -28,7 +33,17 @@ export component JumpNavBar inherits Rectangle {
|
||||
in property <string> active-tab-id;
|
||||
in-out property <bool> search-open: false;
|
||||
in-out property <string> search-text;
|
||||
callback tab-clicked(string /* tab-id */);
|
||||
|
||||
// When the input opens, focus it so the caret lands inside the
|
||||
// text field without an extra click. Without this the user has
|
||||
// to move the cursor from the search button into the input
|
||||
// before typing.
|
||||
changed search-open => {
|
||||
if (root.search-open) {
|
||||
search-input.focus();
|
||||
}
|
||||
}
|
||||
callback tab-clicked(string /* tab-id */, length /* anchor-y */);
|
||||
callback search-clicked();
|
||||
callback search-text-edited(string);
|
||||
|
||||
@@ -113,7 +128,7 @@ export component JumpNavBar inherits Rectangle {
|
||||
mouse-cursor: pointer;
|
||||
enabled: !root.search-open;
|
||||
clicked => {
|
||||
root.tab-clicked(tab.id);
|
||||
root.tab-clicked(tab.id, tab.anchor-y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,14 +238,15 @@ export component JumpNavBar inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Outer search toggle — only visible when the input is
|
||||
// closed. Opening it expands the inline input above (which
|
||||
// carries its own X close button), so the bar never shows
|
||||
// two competing search affordances.
|
||||
VerticalLayout {
|
||||
// Outer search toggle — only present when the input is
|
||||
// closed. `if` (not `visible:`) so the slot is reclaimed by
|
||||
// the center region when the input opens, letting the
|
||||
// expanded input run all the way to the bar's right padding
|
||||
// (right where the user just clicked the lupa). The inline
|
||||
// input carries its own X close button.
|
||||
if !root.search-open: VerticalLayout {
|
||||
alignment: center;
|
||||
horizontal-stretch: 0;
|
||||
visible: !root.search-open;
|
||||
Rectangle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
@@ -250,7 +266,6 @@ export component JumpNavBar inherits Rectangle {
|
||||
}
|
||||
search-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
enabled: !root.search-open;
|
||||
clicked => {
|
||||
root.search-clicked();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
// The Rust side builds these models on a worker thread, then sets them
|
||||
// on the global from the Slint event loop (see src/home.rs, src/main.rs).
|
||||
|
||||
import { JumpNavTab } from "primitives/JumpNavBar.slint";
|
||||
export { JumpNavTab }
|
||||
|
||||
export struct AlbumCardItem {
|
||||
id: string,
|
||||
title: string,
|
||||
@@ -231,6 +234,11 @@ export global ArtistState {
|
||||
// Similar artists from the /artist/page response — feed the Similar
|
||||
// Artists section of the network sidebar.
|
||||
in property <[SimilarEntry]> similar-artists: [];
|
||||
// Filtered JUMP TO tabs for this artist — only the sections
|
||||
// that actually have content. Built in artist::apply_artist
|
||||
// from the page response. anchor-y is the page-local y the
|
||||
// ArtistPageView will scroll to when the tab is clicked.
|
||||
in property <[JumpNavTab]> jump-tabs: [];
|
||||
in property <bool> loading: false;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user