Files
qbz/crates/qbz-slint/ui/primitives/ExpandableSearch.slint
T
vicrodh de826bf560 feat(slint): playlist detail polish, sort (incl. custom) + WIP track drag
Polish + consistency:
  - Compact playlist header (cover 150px; nav->header spacing
    homologated with AlbumView); description truncated to ~2 lines +
    "Read more" modal; custom-artwork buttons only on cover hover.
  - New Typography.section (18px) token for hero/section titles,
    applied across Album / Artist / Mix / Playlist + Home carousels.
  - Shorter track rows app-wide (50px row, 36px artwork, -1 fonts);
    +3px column-header/divider gap; wider track-number column.
  - Reusable ExpandableSearch (the JumpNavBar page-search pattern:
    magnifier -> opens left -> X to close) in the playlist action row.
  - Custom ListScrollbar (thicker, min thumb height, own gutter) since
    Slint's ListView scrollbar can't be styled.

Sort:
  - Default / Title / Artist / Album / Duration with asc/desc per
    field, plus Custom (manual order persisted in library.db with
    up/down reorder). Client-side search + sort combined.

WIP: track drag-and-drop onto sidebar playlists
  - DragState/DragActions globals, drag source on the shared TrackRow,
    follow-cursor DragGhost, sidebar playlist rows self-detect the drop
    via absolute-position, multi-select group drag, add_tracks_to_playlist
    on drop, Flickables freeze scroll during a drag.
  - KNOWN ISSUE: the ghost is offset from the cursor (the row
    absolute-position + pointer mapping is off, esp. inside Flickables).
    Diagnostic logging is left in (TrackRow debug() + a start log) to
    pin down the wrong term before the generic fix.

cc
2026-05-22 18:26:55 -06:00

138 lines
4.7 KiB
Plaintext

// Reusable expandable page-search — the exact pattern from the JUMP TO
// bar (ArtistView): a magnifier that, on click, opens a right-anchored
// input that grows to the LEFT (over whatever sits to its left), with
// a leading magnifier glyph + a trailing X that clears and closes.
//
// Self-contained: occupies a small fixed footprint when closed; the
// open overlay overflows to the left, so nothing to its right moves.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { QbzIcon } from "QbzIcon.slint";
export component ExpandableSearch inherits Rectangle {
in property <string> placeholder: "Search…";
in property <length> max-open-width: 216px;
in-out property <bool> open: false;
in-out property <string> query;
callback edited(string);
changed open => {
if (root.open) {
input.focus();
}
}
// Closed footprint — just the magnifier slot.
width: 34px;
height: 34px;
// Closed: the magnifier toggle.
Rectangle {
visible: !root.open;
width: 34px;
height: 34px;
border-radius: 6px;
background: open-ta.has-hover ? Theme.surface-hover : transparent;
QbzIcon {
source: @image-url("../assets/icons/search.svg");
width: 16px;
height: 16px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: open-ta.has-hover ? Theme.text-primary : Theme.text-muted;
}
open-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.open = true;
}
}
}
// Open: right-anchored overlay that grows left.
overlay := Rectangle {
visible: root.open;
x: parent.width - self.width;
width: root.open ? root.max-open-width : 0px;
height: 34px;
background: Theme.surface-card;
border-radius: 6px;
border-width: 1px;
border-color: input.has-focus ? Theme.accent : Theme.border-subtle;
clip: true;
animate width { duration: 200ms; easing: ease-in-out; }
HorizontalLayout {
padding-left: 11px;
padding-right: 5px;
spacing: 8px;
// Leading magnifier glyph (decorative).
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/search.svg");
width: 14px;
height: 14px;
tint: Theme.text-muted;
}
}
// Input + placeholder.
Rectangle {
horizontal-stretch: 1;
clip: true;
input := TextInput {
width: 100%;
height: 100%;
color: Theme.text-primary;
font-size: Typography.body;
vertical-alignment: center;
single-line: true;
text <=> root.query;
edited => {
root.edited(self.text);
}
}
Text {
visible: input.text == "";
width: 100%;
height: 100%;
text: root.placeholder;
color: Theme.text-muted;
font-size: Typography.body;
vertical-alignment: center;
overflow: elide;
}
}
// Trailing X — clears + closes (back to the magnifier).
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
Rectangle {
width: 24px;
height: 24px;
border-radius: 4px;
background: clear-ta.has-hover ? Theme.surface-elevated : transparent;
QbzIcon {
source: @image-url("../assets/icons/x.svg");
width: 12px;
height: 12px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: clear-ta.has-hover ? Theme.text-primary : Theme.text-muted;
}
clear-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.query = "";
root.edited("");
root.open = false;
}
}
}
}
}
}
}