Files
qbz/crates/qbz-ui/ui/primitives/CircleAction.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

133 lines
5.8 KiB
Plaintext

// Circular header/action button — matches the album-card hover OVERLAY
// buttons: a semi-transparent white fill (stable hit surface) with a bright
// 1.5px ring, white icon; `primary` is a solid white circle with a dark icon
// (the play affordance). Shared by the album/artist/label page headers, their
// Popular Tracks clusters, and the ephemeral pane.
import { Theme } from "../foundation/semantic-colors.slint";
import { QbzIcon } from "QbzIcon.slint";
import { LoadingSpinner } from "LoadingSpinner.slint";
import { TooltipState, ShellState, AppearanceState } from "../state.slint";
export component CircleAction inherits Rectangle {
in property <image> icon;
in property <bool> primary: false;
// Stays "pressed" — same surface as hover, icon tinted accent.
in property <bool> active: false;
// While the album's first track is fetching, the icon swaps for a spinner.
in property <bool> loading: false;
// Homologated header sizing — the size hierarchy lives HERE, not in each
// caller: the primary (Play) button is prominent (44px), every secondary
// action is 80% of the old 40px default (32px). Callers no longer set
// `diameter` for header buttons; an explicit override is only for special
// contexts (e.g. a small card-overlay play).
in property <length> diameter: root.primary ? 44px : 32px;
// When false the button dims and the click is gated (e.g. an empty
// collection's hero CTAs). Additive — existing callers default to enabled.
in property <bool> enabled: true;
// Optional hover tooltip (empty = none), using the global TooltipOverlay.
in property <string> tooltip;
// Theme-aware variant for header rows rendered WITHOUT the dark
// atmosphere (album-header-gradient OFF): the default light-on-dark
// palette (white fills/rings/icons) vanishes on light themes' plain
// surface. `on-surface: true` swaps to the app's standard on-page
// circular look (MixView pattern): accent play disc with a white glyph,
// elevated-surface ghost buttons with a themed ring and text-primary
// icon. Additive — every existing caller keeps the overlay palette.
in property <bool> on-surface: false;
callback clicked();
width: root.diameter;
height: root.diameter;
border-radius: root.diameter / 2;
opacity: root.enabled ? 1.0 : 0.4;
// Overlay-matched contrast: solid white for the primary play, semi-
// transparent white fill + bright 1.5px ring otherwise. The on-surface
// variant mirrors the theme-aware circular buttons used on plain page
// backgrounds instead.
background: root.on-surface
? (root.primary
? (ta.has-hover ? Theme.accent-hover : Theme.accent)
: ((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));
border-width: root.primary ? 0px : 1.5px;
border-color: root.on-surface ? Theme.border-strong : #ffffffcc;
if !root.loading: QbzIcon {
source: root.icon;
// Glyph scales with the tier so the smaller secondary buttons don't
// look icon-heavy (primary keeps the larger 19px glyph).
width: root.primary ? 19px : 15px;
height: root.primary ? 19px : 15px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: root.active
? Theme.accent
: root.on-surface
? (root.primary ? #ffffff : Theme.text-primary)
: (root.primary ? #000000 : #ffffff);
}
if root.loading: LoadingSpinner {
size: root.diameter * 0.45;
// Dark on the solid white primary disc, white on the ghost variant;
// themed tints for the on-surface variant.
tint: root.primary
? (root.on-surface ? #ffffff : #000000)
: (root.on-surface ? Theme.text-primary : #ffffff);
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
// Keyboard activation: Enter/Space fire the same click. The TouchArea below
// stays the pointer source; this only adds key activation + focusability.
fs := FocusScope {
enabled: root.enabled;
key-pressed(event) => {
if (event.text == Key.Return || event.text == " ") {
root.clicked();
return accept;
}
return reject;
}
}
// Non-layout focus ring — transparent-bordered overlay shown only on
// keyboard focus, circular to match the button so it never reflows.
Rectangle {
x: 0;
y: 0;
width: parent.width;
height: parent.height;
background: transparent;
border-width: 2px;
border-color: fs.has-focus ? Theme.focus-ring : transparent;
border-radius: root.diameter / 2;
}
ta := TouchArea {
mouse-cursor: root.enabled ? pointer : default;
clicked => {
if (root.enabled) {
root.clicked();
}
}
changed has-hover => {
if (self.has-hover && root.tooltip != "") {
TooltipState.text = root.tooltip;
TooltipState.anchor-x = root.absolute-position.x + root.width / 2;
TooltipState.anchor-y = root.absolute-position.y;
TooltipState.show = true;
} else if (!self.has-hover) {
TooltipState.show = false;
}
}
}
}