// Track-row leading cell — the play affordance shared by every track row. // // Two variants, chosen by `show-artwork`: // - artwork: a cover thumbnail with a dark play/pause overlay; // - number: a track-number cell that swaps to a play/pause glyph. // // States (ported 1:1 from the Tauri track rows): // rest -> number / plain artwork // row hovered (not current) -> play triangle // current + playing -> animated equalizer bars // current + playing + hover -> pause glyph // current + paused -> play triangle // Clicking a non-current cell plays the track; clicking the current cell // toggles play/pause. import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { Radius } from "../foundation/radius.slint"; import { NowPlayingState } from "../state.slint"; import { QbzIcon } from "QbzIcon.slint"; // One equalizer bar — oscillates continuously while it is shown // (`animation-tick()` drives the height; a 1s triangle wave). component EqBar inherits Rectangle { in property base; in property delay; // 0..1 phase offset width: 3px; border-radius: 1.5px; background: Theme.accent; property phase: Math.mod((animation-tick() / 1ms) / 1000.0 + root.delay, 1.0); property tri: 1.0 - Math.abs(root.phase - 0.5) * 2.0; height: root.base * (0.5 + 0.5 * root.tri); } // Three-bar equalizer; bars grow from a fixed baseline so the block // stays perfectly centered while the bar heights oscillate. component EqualizerBars inherits Rectangle { width: row.preferred-width; height: 16px; row := HorizontalLayout { height: 16px; spacing: 2px; VerticalLayout { height: 16px; alignment: end; EqBar { base: 10px; delay: 0.0; } } VerticalLayout { height: 16px; alignment: end; EqBar { base: 14px; delay: 0.15; } } VerticalLayout { height: 16px; alignment: end; EqBar { base: 8px; delay: 0.3; } } } } export component TrackPlayCell inherits Rectangle { in property track-id; in property number; in property artwork; in property show-artwork: true; in property row-hovered: false; in property art-size: 40px; in property number-width: 32px; // True while the pointer is over the cell itself. The row OR's this // with its other TouchAreas to drive `row-hovered` (Slint's has-hover // does not propagate to ancestor TouchAreas). out property hovered: ta.has-hover; // Fired when a non-current track should start playing. callback play(); property is-active: NowPlayingState.has-track && NowPlayingState.track-id == root.track-id; property is-playing: root.is-active && NowPlayingState.playing; property show-overlay: root.row-hovered || root.is-active; // The animated bars show only while playing and the row is at rest. property show-bars: root.is-playing && !root.row-hovered; width: root.show-artwork ? root.art-size : root.number-width; height: root.show-artwork ? root.art-size : 40px; // --- Artwork variant ------------------------------------------------ if root.show-artwork: Rectangle { width: root.art-size; height: root.art-size; border-radius: Radius.sm; clip: true; background: Theme.surface-elevated; QbzIcon { source: @image-url("../assets/icons/music.svg"); width: root.art-size * 0.45; height: root.art-size * 0.45; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; tint: Theme.text-muted; } Image { width: 100%; height: 100%; source: root.artwork; image-fit: cover; } Rectangle { width: 100%; height: 100%; background: #000000; opacity: root.show-overlay ? (root.row-hovered ? 0.72 : 0.6) : 0.0; animate opacity { duration: 150ms; } } if root.show-bars: EqualizerBars { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; } if root.show-overlay && !root.show-bars: QbzIcon { source: (root.is-playing && root.row-hovered) ? @image-url("../assets/icons/pause.svg") : @image-url("../assets/icons/play-fill.svg"); width: 20px; height: 20px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; tint: #ffffff; } } // --- Number variant ------------------------------------------------- if !root.show-artwork: Rectangle { width: root.number-width; height: 40px; if !root.show-overlay: Text { text: root.number; color: Theme.text-muted; font-size: Typography.body; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; } if root.show-bars: EqualizerBars { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; } if root.show-overlay && !root.show-bars: QbzIcon { source: (root.is-playing && root.row-hovered) ? @image-url("../assets/icons/pause.svg") : @image-url("../assets/icons/play-fill.svg"); width: 16px; height: 16px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; tint: #ffffff; } } ta := TouchArea { mouse-cursor: pointer; clicked => { if (root.is-active) { NowPlayingState.toggle-play(); } else { root.play(); } } } }