Files
qbz/crates/qbz-ui/ui/discover/ArtistCarousel.slint
T
vicrodh a0f5cf2bf5 feat(shell): metaball ambient + translucent bars/panels/controls, drop carousel fades
Smoke rounds 3-4 (owner feedback). Checkpoint during iteration.

Ambient (the big one): rewrote ambient.wgsl from translating gaussian blobs to a
METABALL field (r²/d² summed + smoothstep iso-surface) with 2-octave domain warp
— lobes fuse/stretch/split like an amoeba / lava lamp (the Cider motion that was
missing), faster clock, more contrast.

Surfaces made background-aware (translucent when the dynamic background is on):
- Search results view (root + toolbar), artist Network panel + its tab-row
  header (transparent to avoid double-translucent compounding), JUMP TO bar
  (JumpNavBar bar-bg passed in — can't import ShellState without a cycle).
- Discover toolbars + tab pill + BrowseHeaderTools; SearchResultsView toolbar.
- CircleAction (shared: playlist/album/artist/etc. action buttons) on-surface
  secondary fill goes glass; playlist Sort dropdown; QbzSelect closed control.
- New app-background-bar-alpha knob (0.3, env QBZ_BG_BAR_ALPHA): thin bars go
  MORE transparent than panels (surface-alpha 0.5).
- Content well: frosted tint (0.22) + 1px hairline border → distinct panel.
- Album/Artist header gradient auto-suppressed under the dynamic background
  (they clashed); header text stays light on the dynamic backdrop.
- Discover carousel edge-fade "shadows" hidden when active (Carousel /
  PlaylistCarousel / ArtistCarousel / PinnedCarousel).

Base: 76ea5a82. Prior checkpoint: 4e405338.

cc
2026-07-17 08:08:33 -06:00

206 lines
7.4 KiB
Plaintext

// Artist carousel — title, "View all" link, prev/next page controls, and
// a clipped sliding track of artist cards. Same paging mechanism as
// Carousel. Used by the search results All tab.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { SlimItem, ShellState } from "../state.slint";
import { ArtistCard } from "ArtistCard.slint";
import { ArtistGridCard } from "ArtistGridCard.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
component NavButton inherits Rectangle {
in property <image> icon;
in property <bool> enabled: true;
callback clicked();
width: 28px;
height: 28px;
border-radius: 14px;
opacity: root.enabled ? 1.0 : 0.4;
background: ta.has-hover && root.enabled
? Theme.surface-hover
: Theme.surface-elevated;
QbzIcon {
source: root.icon;
width: 15px;
height: 15px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: root.enabled ? Theme.text-primary : Theme.text-muted;
}
ta := TouchArea {
enabled: root.enabled;
mouse-cursor: root.enabled ? pointer : default;
clicked => {
root.clicked();
}
}
}
component ViewAllLink inherits Rectangle {
callback clicked();
width: link-text.preferred-width + 16px;
height: 26px;
border-radius: 4px;
background: ta.has-hover ? Theme.surface-hover : transparent;
link-text := Text {
text: @tr("View all");
color: Theme.accent;
font-size: 14px;
font-weight: Typography.medium;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.clicked();
}
}
}
export component ArtistCarousel inherits VerticalLayout {
in property <string> title;
in property <[SlimItem]> artists;
in property <bool> show-view-all: false;
// Follow-chip behavior + kind for the cards (see ArtistCard).
// Defaults keep the artist-search usage unchanged.
in property <string> card-follow-mode: "auto";
in property <string> card-follow-kind: "artist";
// Render the 200x246 ArtistGridCard (album/playlist parity, hover overlay
// with follow/play/more) — the default across the app. Set false to keep
// the legacy 160x220 ArtistCard, used by non-artist carousels whose items
// are labels/awards (the artist-specific play/open actions don't apply).
in property <bool> grid-cards: true;
callback artist-clicked(string);
callback media-action(string, string, string);
callback view-all-clicked();
// Grid cards are wider (200) and taller (246); the legacy card stays 160.
property <length> card-width: root.grid-cards ? 200px : 160px;
// Overridable so a host view (Discover Recommendations, whose cards carry a
// "Similar to…" subtitle) can give the row a taller viewport + card.
in property <length> card-height: 220px;
property <length> eff-height: root.grid-cards ? 246px : root.card-height;
property <length> gap: 16px;
property <int> count: root.artists.length;
property <int> per-page: Math.max(
1,
Math.floor((viewport.width + root.gap) / (root.card-width + root.gap)));
property <int> page-count: Math.max(1, Math.ceil(root.count / root.per-page));
property <int> page: 0;
property <int> current-page: Math.min(Math.max(0, root.page), root.page-count - 1);
property <length> step: root.per-page * (root.card-width + root.gap);
property <length> content-width:
root.count * root.card-width + Math.max(0, root.count - 1) * root.gap;
spacing: 12px;
HorizontalLayout {
spacing: 8px;
Text {
text: root.title;
color: Theme.text-primary;
font-size: 18px;
font-weight: Typography.semibold;
vertical-alignment: center;
horizontal-stretch: 1;
}
VerticalLayout {
alignment: center;
NavButton {
icon: @image-url("../assets/icons/chevron-left.svg");
enabled: root.current-page > 0;
clicked => {
root.page = root.current-page - 1;
}
}
}
VerticalLayout {
alignment: center;
NavButton {
icon: @image-url("../assets/icons/chevron-right.svg");
enabled: root.current-page < root.page-count - 1;
clicked => {
root.page = root.current-page + 1;
}
}
}
if root.show-view-all: VerticalLayout {
alignment: center;
ViewAllLink {
clicked => {
root.view-all-clicked();
}
}
}
}
viewport := Rectangle {
height: root.eff-height;
clip: true;
Rectangle {
x: -root.current-page * root.step;
y: 0px;
width: root.content-width;
height: root.eff-height;
animate x { duration: 220ms; easing: ease-in-out; }
HorizontalLayout {
spacing: root.gap;
// One fixed-width slot per artist; the slot hosts the grid card
// OR the legacy card (each sets its own width to match).
for artist in root.artists: Rectangle {
width: root.card-width;
height: root.eff-height;
if root.grid-cards: ArtistGridCard {
artist: artist;
follow-mode: root.card-follow-mode;
follow-kind: root.card-follow-kind;
clicked(id) => {
root.artist-clicked(id);
}
media-action(k, i, a) => {
root.media-action(k, i, a);
}
}
if !root.grid-cards: ArtistCard {
artist: artist;
card-height: root.card-height;
follow-mode: root.card-follow-mode;
follow-kind: root.card-follow-kind;
clicked(id) => {
root.artist-clicked(id);
}
media-action(k, i, a) => {
root.media-action(k, i, a);
}
}
}
}
}
Rectangle {
x: 0px;
width: 56px;
background: @linear-gradient(90deg, Theme.surface-main 0%, transparent 100%);
opacity: root.current-page > 0 ? 1.0 : 0.0;
visible: !ShellState.app-background-active;
animate opacity { duration: 150ms; }
}
Rectangle {
x: parent.width - self.width;
width: 56px;
background: @linear-gradient(90deg, transparent 0%, Theme.surface-main 100%);
opacity: root.current-page < root.page-count - 1 ? 1.0 : 0.0;
visible: !ShellState.app-background-active;
animate opacity { duration: 150ms; }
}
}
}