refactor(slint): split qbz-slint into qbz-ui lib + qbz bin; reclaim qbz binary

Slint cannot split the .slint compilation across crates, but a Slint LIBRARY
crate exposes its generated Rust types to dependent crates (verified with a
minimal proof). So:

- NEW crate `qbz-ui` (lib): owns the whole .slint tree + build.rs + bundled
  translations; `slint::include_modules!()` + the generated types (AppWindow,
  MiniPlayerWindow, all globals/structs) are re-exported. Translation domain is
  now `qbz-ui` (the .po files renamed accordingly).
- `qbz-slint` (bin) -> renamed to `qbz` (package + `[[bin]] name = "qbz"`),
  reclaiming the binary name from the retired Tauri app. It depends on `qbz-ui`
  and does `pub use qbz_ui::*;` so existing `crate::AppWindow` / `crate::*State`
  references resolve unchanged.

This splits the single ~20 GB rustc into two crates (UI/codegen vs the 75k LOC
of logic) — the foundation for lower build peak, incremental caching, and the
upcoming per-feature crate extraction.

Also fixed for the move: qbz-i18n bundled-.po include paths, the font/WGSL
`include_bytes!`/`include_str!` paths (-> ../../qbz-ui/ui/), workspace members,
dev scripts (`-p qbz`), and the CI workflows. `cargo check -p qbz` is green.

cc
This commit is contained in:
vicrodh committed 2026-06-25 17:51:09 -06:00
1 parent b9eb300516
commit c8ef2a1b1d
465 files changed
+129 -89

No files matched your search

@@ -0,0 +1,109 @@
// 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;
// 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;
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;
// 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.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;
}
// 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;
}
}
}
}