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
This commit is contained in:
vicrodh committed 2026-07-17 08:08:33 -06:00
1 parent 4e40533870
commit a0f5cf2bf5
20 files changed
+169 -87

No files matched your search

+18 -16
View File
@@ -158,23 +158,25 @@ export component AlbumPageView inherits Rectangle {
? AlbumState.description-shorter
: AlbumState.description-short;
// Header text colors: the ImmersiveAtmosphere behind the header is
// always dark, so when the gradient setting is ON the header text must
// be light regardless of theme (light themes' Theme.text-* are dark and
// vanish on the atmosphere). With the gradient OFF the header sits on
// Theme.surface-main and keeps the normal theme colors.
property <color> hdr-strong: AppearanceState.album-header-gradient
// Header text colors: the header sits on a DARK backdrop when either the
// album-header-gradient atmosphere is on OR the app-wide dynamic background
// is active — in both cases the text must be light regardless of theme.
// `header-atmo-on` gates the album's OWN blurred atmosphere: it is suppressed
// under the dynamic background (they clash — owner 2026-07-17), so the header
// shows the dynamic background instead.
property <bool> header-light: AppearanceState.album-header-gradient || ShellState.app-background-active;
property <bool> header-atmo-on: AppearanceState.album-header-gradient && !ShellState.app-background-active;
property <color> hdr-strong: header-light
? #ffffff : Theme.text-primary;
property <color> hdr-body: AppearanceState.album-header-gradient
property <color> hdr-body: header-light
? #ffffff.with-alpha(0.88) : Theme.text-secondary;
property <color> hdr-muted: AppearanceState.album-header-gradient
property <color> hdr-muted: header-light
? #ffffff.with-alpha(0.62) : Theme.text-muted;
// With the gradient OFF the header's CircleActions sit on the plain
// theme surface — switch them to the theme-aware on-surface variant so
// they stay visible on light themes (their default palette is white-on-
// dark, tuned for the atmosphere).
property <bool> hdr-on-surface: !AppearanceState.album-header-gradient;
// With no dark header backdrop the CircleActions sit on the plain theme
// surface — switch them to the theme-aware on-surface variant so they stay
// visible on light themes (their default palette is white-on-dark).
property <bool> hdr-on-surface: !header-light;
// Atmosphere/gradient extent: the background blocks must end EXACTLY at
// the header/track-list divider — the header height varies (long
@@ -218,7 +220,7 @@ export component AlbumPageView inherits Rectangle {
// Static Immersive-style blurred artwork field behind the header;
// scrolls with content when the appearance setting is enabled.
if AppearanceState.album-header-gradient && AlbumState.header-atmosphere.width > 0: ImmersiveAtmosphere {
if root.header-atmo-on && AlbumState.header-atmosphere.width > 0: ImmersiveAtmosphere {
x: 0;
y: 0;
width: parent.width;
@@ -230,7 +232,7 @@ export component AlbumPageView inherits Rectangle {
base-opacity: 0.92;
warp-opacity: 0.38;
}
if AppearanceState.album-header-gradient && AlbumState.header-atmosphere.width <= 0: Rectangle {
if root.header-atmo-on && AlbumState.header-atmosphere.width <= 0: Rectangle {
x: 0;
y: 0;
width: parent.width;
@@ -240,7 +242,7 @@ export component AlbumPageView inherits Rectangle {
AlbumState.header-color 16%,
transparent 100%);
}
if AppearanceState.album-header-gradient: Rectangle {
if root.header-atmo-on: Rectangle {
x: 0;
y: 0;
width: parent.width;
+25 -11
View File
@@ -120,15 +120,19 @@ export component ArtistPageView inherits Rectangle {
// theme (light themes' Theme.text-* are dark and vanish on the
// atmosphere). With the gradient OFF the header sits on
// Theme.surface-main and keeps the normal theme colors.
property <color> hdr-strong: AppearanceState.album-header-gradient
// Header sits on a DARK backdrop when the gradient atmosphere is on OR the
// app-wide dynamic background is active → light text either way. The album's
// own atmosphere is suppressed under the dynamic background (they clash).
property <bool> header-light: AppearanceState.album-header-gradient || ShellState.app-background-active;
property <bool> header-atmo-on: AppearanceState.album-header-gradient && !ShellState.app-background-active;
property <color> hdr-strong: header-light
? #ffffff : Theme.text-primary;
property <color> hdr-body: AppearanceState.album-header-gradient
property <color> hdr-body: header-light
? #ffffff.with-alpha(0.88) : Theme.text-secondary;
// With the gradient OFF the header's CircleActions sit on the plain
// theme surface — switch them to the theme-aware on-surface variant so
// they stay visible on light themes.
property <bool> hdr-on-surface: !AppearanceState.album-header-gradient;
// With no dark header backdrop the CircleActions sit on the plain theme
// surface — switch them to the theme-aware on-surface variant.
property <bool> hdr-on-surface: !header-light;
// Atmosphere/gradient extent: end EXACTLY at the header/content
// boundary — `body-row.y` is where the body content starts within
@@ -203,7 +207,7 @@ export component ArtistPageView inherits Rectangle {
// Static Immersive-style blurred artwork field behind the header;
// scrolls with content when the appearance setting is enabled.
if AppearanceState.album-header-gradient && ArtistState.header-atmosphere.width > 0: ImmersiveAtmosphere {
if root.header-atmo-on && ArtistState.header-atmosphere.width > 0: ImmersiveAtmosphere {
x: 0;
y: 0;
width: parent.width;
@@ -215,7 +219,7 @@ export component ArtistPageView inherits Rectangle {
base-opacity: 0.92;
warp-opacity: 0.38;
}
if AppearanceState.album-header-gradient && ArtistState.header-atmosphere.width <= 0: Rectangle {
if root.header-atmo-on && ArtistState.header-atmosphere.width <= 0: Rectangle {
x: 0;
y: 0;
width: parent.width;
@@ -225,7 +229,7 @@ export component ArtistPageView inherits Rectangle {
ArtistState.header-color 16%,
transparent 100%);
}
if AppearanceState.album-header-gradient: Rectangle {
if root.header-atmo-on: Rectangle {
x: 0;
y: 0;
width: parent.width;
@@ -1022,6 +1026,9 @@ 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 {
bar-bg: ShellState.app-background-active
? Theme.surface-main.with-alpha(AppearanceState.app-background-bar-alpha)
: Theme.surface-main;
// body-row.absolute-position.y already moves up with scroll
// (it's a window-coord lookup), so `natural-top` here is
// the LIVE viewport-relative y the bar wants to occupy at
@@ -1101,7 +1108,9 @@ export component ArtistPageView inherits Rectangle {
y: max(self.natural-top, self.sticky-top);
width: 300px;
height: root.height - self.y;
background: Theme.surface-card;
background: ShellState.app-background-active
? Theme.surface-card.with-alpha(AppearanceState.app-background-surface-alpha)
: Theme.surface-card;
// Left border line — 1px Theme.border-subtle, matching the
// Tauri `.network-sidebar-column { border-left: 1px solid
@@ -1121,7 +1130,12 @@ export component ArtistPageView inherits Rectangle {
// on the right.
Rectangle {
height: 44px;
background: Theme.surface-main;
// Transparent under the dynamic background: it sits ON the
// already-translucent panel body, so a second translucent
// layer would compound to near-opaque. Let the body show.
background: ShellState.app-background-active
? transparent
: Theme.surface-main;
HorizontalLayout {
padding-left: 12px;
@@ -4,7 +4,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { SlimItem } from "../state.slint";
import { SlimItem, ShellState } from "../state.slint";
import { ArtistCard } from "ArtistCard.slint";
import { ArtistGridCard } from "ArtistGridCard.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
@@ -190,6 +190,7 @@ export component ArtistCarousel inherits VerticalLayout {
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 {
@@ -197,6 +198,7 @@ export component ArtistCarousel inherits VerticalLayout {
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; }
}
}
@@ -8,7 +8,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { GenreFilterState, UiFocusState } from "../state.slint";
import { GenreFilterState, UiFocusState, ShellState, AppearanceState } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ToggleButton } from "../primitives/ToggleButton.slint";
@@ -23,7 +23,7 @@ export component BrowseSearch inherits Rectangle {
height: 34px;
width: 200px;
border-radius: 6px;
background: Theme.surface-elevated;
background: ShellState.app-background-active ? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha) : Theme.surface-elevated;
border-width: 1px;
border-color: input.has-focus ? Theme.accent : Theme.border-subtle;
@@ -152,7 +152,7 @@ export component ViewModeToggle inherits Rectangle {
width: 68px;
height: 34px;
border-radius: 6px;
background: Theme.surface-elevated;
background: ShellState.app-background-active ? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha) : Theme.surface-elevated;
clip: true;
HorizontalLayout {
ToggleButton {
+3 -1
View File
@@ -7,7 +7,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { DiscoverSection } from "../state.slint";
import { DiscoverSection, ShellState } from "../state.slint";
import { AlbumCard } from "AlbumCard.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { QbzSelect } from "../primitives/QbzSelect.slint";
@@ -299,6 +299,7 @@ export component Carousel inherits VerticalLayout {
width: 56px;
background: @linear-gradient(90deg, Theme.surface-main 0%, transparent 100%);
opacity: flick.viewport-x < -1px ? 1.0 : 0.0;
visible: !ShellState.app-background-active;
animate opacity { duration: 150ms; }
}
Rectangle {
@@ -306,6 +307,7 @@ export component Carousel inherits VerticalLayout {
width: 56px;
background: @linear-gradient(90deg, transparent 0%, Theme.surface-main 100%);
opacity: flick.viewport-x > -root.max-scroll + 1px ? 1.0 : 0.0;
visible: !ShellState.app-background-active;
animate opacity { duration: 150ms; }
}
}
@@ -10,7 +10,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { DiscoverBrowseState, DiscoverBrowseActions, GenreFilterState, GenreFilterActions, NavState, ShellState } from "../state.slint";
import { DiscoverBrowseState, DiscoverBrowseActions, GenreFilterState, GenreFilterActions, NavState, ShellState, AppearanceState } from "../state.slint";
import { AlbumCollectionView } from "AlbumCollectionView.slint";
import { BrowseSearch, GenreButton, ViewModeToggle } from "BrowseHeaderTools.slint";
import { GenreFilterPopup } from "GenreFilterPopup.slint";
@@ -43,7 +43,7 @@ export component DiscoverBrowseView inherits Rectangle {
// below scrolls.
Rectangle {
height: 56px;
background: Theme.surface-main;
background: ShellState.app-background-active ? Theme.surface-main.with-alpha(AppearanceState.app-background-bar-alpha) : Theme.surface-main;
// Left cluster — nav buttons then the title, inline.
HorizontalLayout {
+3 -3
View File
@@ -8,7 +8,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { HomeState, HomeActions, DiscoverState, DiscoverActions, PinnedState, GenreFilterState, GenreFilterActions, NavState, ContentView, ShellState, SettingsState, TooltipState } from "../state.slint";
import { HomeState, HomeActions, DiscoverState, DiscoverActions, PinnedState, GenreFilterState, GenreFilterActions, NavState, ContentView, ShellState, AppearanceState, SettingsState, TooltipState } from "../state.slint";
import { Carousel } from "Carousel.slint";
import { SlimCarousel } from "SlimCarousel.slint";
import { ArtistCarousel } from "ArtistCarousel.slint";
@@ -179,7 +179,7 @@ export component HomeView inherits Rectangle {
// true horizontal center regardless of the side controls' widths.
Rectangle {
height: 56px;
background: Theme.surface-main;
background: ShellState.app-background-active ? Theme.surface-main.with-alpha(AppearanceState.app-background-bar-alpha) : Theme.surface-main;
// History pair + the three-tab nav bar, grouped at the left
// with a gap delimiting the two clusters. Explicit width
@@ -206,7 +206,7 @@ export component HomeView inherits Rectangle {
tab-pill := Rectangle {
width: tab-row.preferred-width;
height: tab-row.preferred-height;
background: Theme.surface-elevated;
background: ShellState.app-background-active ? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha) : Theme.surface-elevated;
border-radius: 6px;
tab-row := HorizontalLayout {
padding: 3px;
@@ -7,7 +7,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { PinnedItem } from "../state.slint";
import { PinnedItem, ShellState } from "../state.slint";
import { AlbumCard } from "AlbumCard.slint";
import { ArtistGridCard } from "ArtistGridCard.slint";
import { PlaylistCard } from "PlaylistCard.slint";
@@ -158,6 +158,7 @@ export component PinnedCarousel inherits VerticalLayout {
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 {
@@ -165,6 +166,7 @@ export component PinnedCarousel inherits VerticalLayout {
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; }
}
}
@@ -4,7 +4,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { SearchPlaylistItem } from "../state.slint";
import { SearchPlaylistItem, ShellState } from "../state.slint";
import { PlaylistCard } from "PlaylistCard.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
@@ -163,6 +163,7 @@ export component PlaylistCarousel inherits VerticalLayout {
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 {
@@ -170,6 +171,7 @@ export component PlaylistCarousel inherits VerticalLayout {
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; }
}
}
+4 -1
View File
@@ -12,7 +12,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { LabelState, LabelActions, TrackItem, DragState, NavState, BulkAction, ShellState } from "../state.slint";
import { LabelState, LabelActions, TrackItem, DragState, NavState, BulkAction, ShellState, AppearanceState } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { TrackRow } from "../primitives/TrackRow.slint";
import { CircleAction } from "../primitives/CircleAction.slint";
@@ -398,6 +398,9 @@ export component LabelPageView inherits Rectangle {
// header scrolls away, then pins to y = 0. No inline search (Tauri's
// label jump-nav has none).
if LabelState.jump-tabs.length > 1: JumpNavBar {
bar-bg: ShellState.app-background-active
? Theme.surface-main.with-alpha(AppearanceState.app-background-bar-alpha)
: Theme.surface-main;
property <length> natural-top:
jump-anchor.absolute-position.y - page-flickable.absolute-position.y;
x: 0;
+6 -2
View File
@@ -7,7 +7,7 @@ import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { PlaylistState, PlaylistActions, PinnedActions, DragState, NavState, OfflineState, BulkAction, SettingsState } from "../state.slint";
import { PlaylistSuggestionsState, PlaylistSuggestionsActions, PlaylistSuggestionRow, TooltipState, ShellState } from "../state.slint";
import { PlaylistSuggestionsState, PlaylistSuggestionsActions, PlaylistSuggestionRow, TooltipState, ShellState, AppearanceState } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { CircleAction } from "../primitives/CircleAction.slint";
import { TrackRow } from "../primitives/TrackRow.slint";
@@ -840,7 +840,11 @@ export component PlaylistView inherits Rectangle {
height: 34px;
width: 132px;
border-radius: 6px;
background: sort-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
background: sort-ta.has-hover
? Theme.surface-hover
: (ShellState.app-background-active
? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha)
: Theme.surface-elevated);
border-width: 1px;
border-color: Theme.border-subtle;
sort-row := HorizontalLayout {
@@ -7,7 +7,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { QbzIcon } from "QbzIcon.slint";
import { LoadingSpinner } from "LoadingSpinner.slint";
import { TooltipState } from "../state.slint";
import { TooltipState, ShellState, AppearanceState } from "../state.slint";
export component CircleAction inherits Rectangle {
in property <image> icon;
@@ -48,7 +48,11 @@ export component CircleAction inherits Rectangle {
background: root.on-surface
? (root.primary
? (ta.has-hover ? Theme.accent-hover : Theme.accent)
: ((ta.has-hover || root.active) ? Theme.surface-hover : Theme.surface-elevated))
: ((ta.has-hover || root.active)
? Theme.surface-hover
: (ShellState.app-background-active
? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha)
: Theme.surface-elevated)))
: (root.primary
? (ta.has-hover ? #ffffffd6 : #ffffff)
: ((ta.has-hover || root.active) ? #ffffff3d : #ffffff24));
+5 -1
View File
@@ -32,6 +32,10 @@ export struct JumpNavTab {
export component JumpNavBar inherits Rectangle {
in property <[JumpNavTab]> tabs;
in property <string> active-tab-id;
// Bar background — passed in so the parent can make it translucent under the
// app-wide dynamic background (JumpNavBar can't import ShellState: state.slint
// imports JumpNavTab from here, so the reverse import would cycle).
in property <brush> bar-bg: Theme.surface-main;
// Whether the inline page-search affordance is offered. Artist pages
// have it; the label page does not (Tauri's label jump-nav has no
// search), so the label view passes show-search: false.
@@ -53,7 +57,7 @@ export component JumpNavBar inherits Rectangle {
callback search-text-edited(string);
height: 44px;
background: Theme.surface-main;
background: root.bar-bg;
border-width: 0px;
// Bottom border so the bar reads as a separate strip when
+8 -2
View File
@@ -21,7 +21,7 @@ import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { QbzIcon } from "QbzIcon.slint";
import { TextUtil, UiFocusState } from "../state.slint";
import { TextUtil, UiFocusState, ShellState, AppearanceState } from "../state.slint";
// "BP" marker for bit-perfect-capable options. Plain accent text, no
// background — the QBZ UI never uses pill-style chips.
@@ -105,7 +105,13 @@ export component QbzSelect inherits Rectangle {
border-radius: root.sm ? 6px : Radius.sm;
border-width: root.sm ? 0px : 1px;
border-color: Theme.border-subtle;
background: ta.has-hover && root.enabled ? Theme.surface-hover : Theme.surface-elevated;
// Glass look when the app-wide dynamic background is active: the resting
// fill goes translucent so the moving background shows through the control.
background: ta.has-hover && root.enabled
? Theme.surface-hover
: (ShellState.app-background-active
? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha)
: Theme.surface-elevated);
opacity: root.enabled ? 1.0 : 0.4;
HorizontalLayout {
+2 -4
View File
@@ -27,7 +27,7 @@ import { Theme } from "../foundation/semantic-colors.slint";
import { Radius } from "../foundation/radius.slint";
import { Layout } from "../foundation/layout.slint";
import { Typography } from "../foundation/typography.slint";
import { SearchState, SearchActions, CortinillaRow, CortinillaSection, ShellState, AppearanceState } from "../state.slint";
import { SearchState, SearchActions, CortinillaRow, CortinillaSection, ShellState } from "../state.slint";
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
// A single SlimCard-style result row inside the cortinilla — 40x40 thumbnail
@@ -307,9 +307,7 @@ export component Cortinilla inherits Rectangle {
hover-ta := TouchArea {
changed has-hover => { root.panel-hovered = self.has-hover; }
panel-box := Rectangle {
background: ShellState.app-background-active
? Theme.surface-main.with-alpha(AppearanceState.app-background-surface-alpha)
: Theme.surface-main;
background: Theme.surface-main;
border-radius: Radius.sm;
border-width: 1px;
border-color: Theme.border-muted;
@@ -8,7 +8,7 @@
// per-type tabs show the full loaded set as a card grid or row list with
// a load-more button.
import { SearchState, SearchActions, TrackItem, SearchPlaylistItem, AlbumCardItem, SlimItem, DragState, NavState } from "../state.slint";
import { SearchState, SearchActions, TrackItem, SearchPlaylistItem, AlbumCardItem, SlimItem, DragState, NavState, ShellState, AppearanceState } from "../state.slint";
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
@@ -371,7 +371,7 @@ export component SearchResultsView inherits Rectangle {
|| SearchState.artists.length > 0
|| SearchState.playlists.length > 0;
background: Theme.surface-main;
background: ShellState.app-background-active ? transparent : Theme.surface-main;
VerticalLayout {
spacing: 0px;
@@ -383,7 +383,9 @@ export component SearchResultsView inherits Rectangle {
// inline right after the nav pair.
Rectangle {
height: 56px;
background: Theme.surface-main;
background: ShellState.app-background-active
? Theme.surface-main.with-alpha(AppearanceState.app-background-bar-alpha)
: Theme.surface-main;
// Left cluster — nav buttons then the title, inline.
HorizontalLayout {
+49 -28
View File
@@ -84,6 +84,14 @@ fn blob(uv: vec2<f32>, c: vec2<f32>, r: f32) -> f32 {
return exp(-(d * d) / (r * r));
}
// Metaball potential (r²/d²): summed over several movers, the iso-surface merges
// and splits organically — the "amoeba / lava-lamp" morph. Unlike a gaussian
// blob this has a long tail, so nearby balls fuse into stretched shapes.
fn mball(uv: vec2<f32>, c: vec2<f32>, r: f32) -> f32 {
let d = uv - c;
return (r * r) / (dot(d, d) + 0.0009);
}
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
// Aspect-correct so the drift looks even on wide windows.
@@ -94,47 +102,60 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
// Clock. FAST enough that the flow is clearly visible in seconds (blob
// orbits are ~10-18s) — a subtle drift reads as "not moving". `level_smooth`
// adds a gentle breathe on top when audio is flowing.
let t = u.time * 0.5;
let breathe = 1.0 + 0.10 * u.level_smooth;
let t = u.time * 0.75;
let breathe = 1.0 + 0.12 * u.level_smooth;
// Stronger domain warp so the blobs flow like fluid, not on rigid circles.
let warp = vec2<f32>(
fbm(uv * 1.8 + vec2<f32>(t * 0.6, t * 0.2)),
fbm(uv * 1.8 + vec2<f32>(-t * 0.25, t * 0.55)),
// Two-octave domain warp — heavy, so the metaball field distorts organically
// (amoeba edges, not circles) and churns as it flows.
let w1 = vec2<f32>(
fbm(uv * 1.4 + vec2<f32>(t * 0.5, t * 0.2)),
fbm(uv * 1.4 + vec2<f32>(-t * 0.3, t * 0.45)),
);
let p = uv + (warp - 0.5) * 0.55;
let w2 = vec2<f32>(
fbm(uv * 3.1 + vec2<f32>(-t * 0.7, t * 0.5)),
fbm(uv * 3.1 + vec2<f32>(t * 0.6, -t * 0.4)),
);
let p = uv + (w1 - 0.5) * 0.80 + (w2 - 0.5) * 0.30;
// Three album-colored blobs sweeping across most of the screen (big travel
// so the motion is obvious), each on its own period so they never lock.
let cA = vec2<f32>((0.35 + 0.34 * sin(t * 0.41)) * aspect,
0.42 + 0.30 * cos(t * 0.33));
let cB = vec2<f32>((0.66 + 0.32 * sin(t * 0.37 + 2.1)) * aspect,
0.58 + 0.34 * cos(t * 0.29 + 1.3));
let cC = vec2<f32>((0.50 + 0.36 * cos(t * 0.31 + 4.0)) * aspect,
0.34 + 0.30 * sin(t * 0.44 + 3.2));
// Four album-colored METABALLS on big wandering orbits. Their r²/d² fields
// sum, so where two get close they FUSE into a stretched amoeba lobe, and
// pull apart as they separate — the morphing Cider/lava-lamp motion.
let c4 = mix(u.primary.rgb, u.accent.rgb, 0.5);
let cA = vec2<f32>((0.32 + 0.30 * sin(t * 0.40)) * aspect, 0.42 + 0.30 * cos(t * 0.33));
let cB = vec2<f32>((0.66 + 0.30 * sin(t * 0.35 + 2.1)) * aspect, 0.56 + 0.32 * cos(t * 0.29 + 1.3));
let cC = vec2<f32>((0.50 + 0.34 * cos(t * 0.31 + 4.0)) * aspect, 0.36 + 0.30 * sin(t * 0.45 + 3.2));
let cD = vec2<f32>((0.46 + 0.32 * sin(t * 0.27 + 5.3)) * aspect, 0.64 + 0.28 * cos(t * 0.49 + 0.7));
let r = 0.62 * breathe;
let wA = blob(p, cA, r);
let wB = blob(p, cB, r * 0.92);
let wC = blob(p, cC, r * 0.85);
let wSum = wA + wB + wC + 0.0001;
let rr = 0.34 * breathe;
let fA = mball(p, cA, rr);
let fB = mball(p, cB, rr * 0.95);
let fC = mball(p, cC, rr * 0.88);
let fD = mball(p, cD, rr * 0.82);
let field = fA + fB + fC + fD;
var col = (u.primary.rgb * wA + u.secondary.rgb * wB + u.accent.rgb * wC) / wSum;
// Metaball-weighted album color (which lobe dominates here).
var col = (u.primary.rgb * fA + u.secondary.rgb * fB + u.accent.rgb * fC + c4 * fD)
/ (field + 0.0001);
// Push saturation/contrast a touch so the album hues read as a living
// gradient rather than a muddy average.
// The AMOEBA structure: the iso-surface. `shape` ~1 inside the fused lobes,
// ~0 in the gaps between them → bright morphing shapes over a darker base, so
// the motion is obvious instead of a flat smear.
let shape = smoothstep(0.85, 2.6, field);
col = col * mix(0.32, 1.18, shape);
// Push saturation/contrast so the lobes read as vivid amoebas, not a muddy
// average.
let luma = dot(col, vec3<f32>(0.299, 0.587, 0.114));
col = mix(vec3<f32>(luma), col, 1.25);
col = mix(vec3<f32>(luma), col, 1.4);
// Vertical falloff → a touch darker at the very top/bottom edges so chrome
// (titlebar, player bar) sits on calmer color. Kept subtle.
let vshade = 1.0 - 0.16 * pow(abs(in.uv.y - 0.5) * 2.0, 2.0);
col = col * vshade;
// Overall brightness: brighter than before — the content area now shows the
// full background, and the Slint scrim (QBZ_BG_DIM) provides the legibility
// dim, so the base can stay vivid without glaring.
col = clamp(col, vec3<f32>(0.0), vec3<f32>(1.0)) * 0.82;
// Overall brightness — vivid; the Slint scrim (QBZ_BG_DIM) provides the
// legibility dim, so the base can stay bright without glaring.
col = clamp(col, vec3<f32>(0.0), vec3<f32>(1.0)) * 0.92;
// A whisper of grain to avoid banding on the smooth gradient.
let grain = (vnoise(in.uv * u.resolution * 0.5) - 0.5) * 0.015;
+11 -5
View File
@@ -325,14 +325,20 @@ export component AppShell inherits Rectangle {
- (ShellState.npb-mode == 2 ? Layout.header-height : Layout.player-bar-height)
- 8px;
vertical-stretch: 1;
// Fully transparent when the background is active so the
// dynamic background shows through the content area (the
// views inside also go transparent); the root scrim keeps
// text legible. Opaque theme surface otherwise.
// When the background is active the content area reads as a
// distinct frosted-glass PANEL: a faint tint (so it's set
// apart from the sharper sidebar/background) plus a 1px
// hairline border framing it — the Cider content-area look.
// (True backdrop blur isn't a Slint primitive; the tint +
// border approximate the "frosted, bordered panel" read.)
background: ShellState.app-background-active
? transparent
? Theme.surface-main.with-alpha(0.22)
: Theme.surface-main;
border-radius: Radius.md;
border-width: ShellState.app-background-active ? 1px : 0px;
border-color: ShellState.app-background-active
? #ffffff.with-alpha(0.10)
: transparent;
clip: true;
// Offline: every Qobuz-only view below mounts only while
+4
View File
@@ -3254,6 +3254,10 @@ export global AppearanceState {
// of the shell chrome when the background is active.
in-out property <float> app-background-dim: 0.35;
in-out property <float> app-background-surface-alpha: 0.5;
// Thin BARS (jump-nav, discover toolbar, network tab row, playlist action
// bar) go MORE transparent than the panels — they read as chrome strips over
// the frosted content panel, not solid bars. Env QBZ_BG_BAR_ALPHA.
in-out property <float> app-background-bar-alpha: 0.3;
// Callbacks. Unwired in this replica — the Rust side may bind them
// later. Kept generic (key + value) so the controller can dispatch.
+6
View File
@@ -7196,6 +7196,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
{
ap.set_app_background_surface_alpha(f.clamp(0.0, 1.0));
}
if let Some(f) = std::env::var("QBZ_BG_BAR_ALPHA")
.ok()
.and_then(|v| v.trim().parse::<f32>().ok())
{
ap.set_app_background_bar_alpha(f.clamp(0.0, 1.0));
}
}
// Kiosk profile (2.0.2 frente #3, axis A): QBZ_PROFILE=kiosk (env wins,
// else the persisted ui_prefs.profile key). The small-panel touch appliance