Files
qbz/crates/qbz-slint/ui/primitives/QbzSelect.slint
T
vicrodh 89734ae3ab fix(slint): instant local queue, mouse nav buttons, narrower toolbar controls
- Local Library Tracks: build the play queue from the already-loaded rows
  (instant) instead of re-querying the DB + filling covers on every click.
- Mouse back/forward buttons now drive history nav over track rows and album
  cards (a TouchArea swallows them, so each interactive surface forwards them;
  the AppShell catcher only saw the gaps).
- ExpandableSearch open width trimmed 20px (no longer rides over left controls
  on small screens).
- QbzSelect toolbar (Group/Sort/filter) trimmed 40px total.

cc
2026-05-30 16:53:11 -06:00

185 lines
7.3 KiB
Plaintext

// Dropdown select primitive — the standard option picker for settings.
//
// The QBZ UI uses dropdowns, never pill-style option buttons. The select
// shows the current option and opens a PopupWindow list on click; picking
// an option emits `selected(index)` and the caller updates the bound
// state.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { QbzIcon } from "QbzIcon.slint";
// "BP" marker for bit-perfect-capable options. Plain accent text, no
// background — the QBZ UI never uses pill-style chips.
component BpBadge inherits Text {
text: "BP";
color: Theme.accent;
font-size: Typography.legal;
font-weight: Typography.semibold;
letter-spacing: 0.5px;
vertical-alignment: center;
}
// Non-selectable section header row shown in the open list above the
// first option of a group. Plain muted-caps text, no background — the
// QBZ UI never uses pill-style chips.
component SectionHeader inherits Text {
color: Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.semibold;
letter-spacing: 0.5px;
vertical-alignment: center;
}
export component QbzSelect inherits Rectangle {
in property <[string]> options;
// Optional, parallel to `options`: a true entry shows a "BP" badge.
in property <[bool]> badges: [];
// Optional, parallel to `options`: a non-empty entry renders a
// non-selectable section-header row immediately above that option.
// Empty (the default) means no headers — backward compatible with
// every other QbzSelect caller (backend, sample-rate, ...).
in property <[string]> groups: [];
in property <int> current-index: 0;
in property <length> menu-width: 240px;
// Optional: a wider open list than the collapsed control, for long
// option labels (device names). 0px keeps the list at `menu-width`.
in property <length> popup-width: 0px;
in property <bool> enabled: true;
// Bootstrap-style small variant. Default is the standard size (used by
// Settings); toolbars opt in with `sm: true`.
in property <bool> sm: false;
callback selected(int);
// Toolbar (sm) selects — Group / Sort / filter — were unnecessarily wide;
// trim 40px from the effective width (button + list).
private property <length> eff-width: root.menu-width - (root.sm ? 40px : 0px);
// Resolved width of the open list.
private property <length> list-width:
root.popup-width > root.eff-width ? root.popup-width : root.eff-width;
// Height of one option row and one section-header row.
private property <length> row-height: 32px;
private property <length> header-height: 24px;
width: root.eff-width;
height: root.sm ? 30px : 34px;
// sm matches the favorites toolbar buttons (6px, no outline); the
// standard size keeps the settings-style bordered look.
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;
opacity: root.enabled ? 1.0 : 0.4;
HorizontalLayout {
padding-left: root.sm ? 10px : 12px;
padding-right: root.sm ? 8px : 10px;
spacing: 8px;
Text {
text: root.current-index >= 0 && root.current-index < root.options.length
? root.options[root.current-index]
: "";
color: Theme.text-primary;
font-size: root.sm ? 12px : Typography.body;
vertical-alignment: center;
horizontal-stretch: 1;
overflow: elide;
}
if root.current-index >= 0
&& root.current-index < root.badges.length
&& root.badges[root.current-index]: VerticalLayout {
alignment: center;
BpBadge { }
}
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/chevron-down.svg");
width: root.sm ? 14px : 16px;
height: root.sm ? 14px : 16px;
tint: Theme.text-muted;
}
}
}
ta := TouchArea {
mouse-cursor: root.enabled ? pointer : default;
clicked => {
if (root.enabled) {
popup.show();
}
}
}
popup := PopupWindow {
// Right-anchored so a list wider than the control grows leftward
// and stays on-screen.
x: root.eff-width - root.list-width;
y: root.height + 4px;
width: root.list-width;
height: list-layout.preferred-height;
Rectangle {
background: Theme.surface-main;
border-radius: Radius.sm;
border-width: 1px;
border-color: Theme.border-muted;
drop-shadow-blur: 20px;
drop-shadow-color: Theme.card-shadow;
drop-shadow-offset-y: 5px;
list-layout := VerticalLayout {
padding-top: 5px;
padding-bottom: 5px;
// Each entry is a group-header row (optional, non-selectable)
// stacked above its option row. A `groups[i]` of "" means the
// option carries no header — every non-device QbzSelect caller
// leaves `groups` empty, so they render exactly as before.
for opt[i] in root.options: VerticalLayout {
if i < root.groups.length && root.groups[i] != "": Rectangle {
height: root.header-height;
HorizontalLayout {
padding-left: 12px;
padding-right: 12px;
padding-top: 4px;
SectionHeader {
text: root.groups[i];
horizontal-stretch: 1;
}
}
}
Rectangle {
height: root.row-height;
background: opt-ta.has-hover ? Theme.surface-hover : transparent;
HorizontalLayout {
padding-left: 12px;
padding-right: 12px;
spacing: 8px;
Text {
text: opt;
color: i == root.current-index ? Theme.accent : Theme.text-secondary;
font-size: root.sm ? 12px : Typography.body;
vertical-alignment: center;
horizontal-stretch: 1;
overflow: elide;
}
if i < root.badges.length && root.badges[i]: VerticalLayout {
alignment: center;
BpBadge { }
}
}
opt-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.selected(i);
}
}
}
}
}
}
}
}