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:
20 files changed
+169
-87
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user