Files
qbz/crates/qbz-slint/ui/primitives/CircleAction.slint
T
vicrodh 16e6232220 refactor(myqbz): button audit — app primitives + fix glyph centering
Phase-2 Slice 12c (user directive: use the app's circular controls, no invented
buttons; fix the glyph-stuck-left bug).
- Hero §3.6: deleted the local ActionCircle; the 4 hero controls are now the
  shared CircleAction (Play primary = filled white disc + black glyph; the loading
  spinner is now available = more 1:1). Added `enabled` to CircleAction (additive).
- All MyQBZ modal footers (Create / Edit / Mix / AddTo / Discography) now use the
  shared QbzPrimaryButton (+ a new `destructive` variant for Delete) and a new
  shared SecondaryButton (promoted from AppearanceSettings' inline copy).
  QbzPrimaryButton auto-sizes as a footer chip while staying full-width in login
  (verified: login stretches it in a VerticalLayout; footers take preferred width).
- Kept legitimately-local icon+label controls (grid "+ New" CTA, AddToMixtape
  picker chips, toolbar sort/filter/view-mode pills) but verified every glyph is
  centered.
- Fixed 3 glyph-stuck-left bugs (DiscographyBuilder checkboxes used non-snapped
  centering -> the Math.round(.../1px)*1px crisp idiom).

Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 08:29:11 -06:00

77 lines
3.1 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 } 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;
in property <length> diameter: 40px;
// 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;
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.
background: root.primary
? (ta.has-hover ? #ffffffd6 : #ffffff)
: ((ta.has-hover || root.active) ? #ffffff3d : #ffffff24);
border-width: root.primary ? 0px : 1.5px;
border-color: #ffffffcc;
if !root.loading: QbzIcon {
source: root.icon;
width: root.primary ? 19px : 17px;
height: root.primary ? 19px : 17px;
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.primary ? #000000 : #ffffff);
}
if root.loading: LoadingSpinner {
size: root.diameter * 0.45;
// Dark on the solid white primary disc, white on the ghost variant.
tint: root.primary ? #000000 : #ffffff;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
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;
}
}
}
}