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:
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user