// 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 placeholder: "Search…"; // Trimmed 20px so the open field doesn't ride over the controls to its // left on small screens. in property max-open-width: 196px; in-out property open: false; in-out property query; // Bootstrap-style small variant. Default is the standard size. in property sm: false; callback edited(string); // Defer the focus: focusing the input synchronously inside `changed open` // races the open animation/layout (and can panic with "Recursion // detected"). A one-shot timer grabs focus a tick later, reliably. focus-timer := Timer { interval: 30ms; running: false; triggered => { input.focus(); self.running = false; } } changed open => { if (root.open) { focus-timer.running = true; focus-timer.restart(); } } // Closed footprint — just the magnifier slot. width: root.sm ? 30px : 34px; height: root.sm ? 30px : 34px; // Closed: the magnifier toggle. Fades (not toggled) so open/close is // smooth in both directions. Rectangle { opacity: root.open ? 0.0 : 1.0; animate opacity { duration: 120ms; easing: ease-in-out; } width: root.sm ? 30px : 34px; height: root.sm ? 30px : 34px; border-radius: 6px; background: open-ta.has-hover ? Theme.surface-hover : transparent; QbzIcon { source: @image-url("../assets/icons/search.svg"); width: root.sm ? 14px : 16px; height: root.sm ? 14px : 16px; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; tint: open-ta.has-hover ? Theme.text-primary : Theme.text-muted; } open-ta := TouchArea { enabled: !root.open; mouse-cursor: pointer; clicked => { root.open = true; } } } // Open: right-anchored overlay that grows left. Always rendered; its width // animates to 0 when closed (clip hides it), so the shrink is also smooth. overlay := Rectangle { x: parent.width - self.width; width: root.open ? root.max-open-width : 0px; height: root.sm ? 30px : 34px; background: Theme.surface-card; border-radius: 6px; border-width: root.open ? 1px : 0px; 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: root.sm ? 22px : 24px; height: root.sm ? 22px : 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: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; 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; } } } } } } }