diff --git a/crates/qbz-qt/qml/rows/TrackRow.qml b/crates/qbz-qt/qml/rows/TrackRow.qml index f602df425..4b699b2d0 100644 --- a/crates/qbz-qt/qml/rows/TrackRow.qml +++ b/crates/qbz-qt/qml/rows/TrackRow.qml @@ -990,13 +990,28 @@ Rectangle { // Main artist first, then the featured performers, one comma // separated list. Local / media-server rows carry no // `featured` and stay as they are. With links enabled and - // featured names present, every name is its own StyledText - // link: the main artist opens its page as before; a featured + // featured names present, every name is its own clickable + // SPAN: the main artist opens its page as before; a featured // name opens its page when the album credits gave it an id - // and the search for that name otherwise. StyledText elides - // like plain text (probed: truncated stays true on one line). + // and the search for that name otherwise. Spans, not `` + // anchors: StyledText underlines every anchor unconditionally + // (qquickstyledtext.cpp, setFontUnderline(true)) and nothing + // else in the app underlines a link, so the hit test is done + // by measuring the prefix up to each name with the same font + // (TextMetrics below). StyledText still elides like plain + // text (probed: truncated stays true on one line). readonly property var featured: root.showFeaturedArtists ? (root.item.featured || []) : [] readonly property bool linkMode: root.artistLink && featured.length > 0 + /// The names as displayed, in order: {name, href} — href "" + /// = the main artist with no page to open. + readonly property var spans: { + var out = [{ "name": String(root.item.artist || ""), "href": root.item.artistId ? "main" : "" }] + for (var i = 0; i < featured.length; i++) + out.push({ "name": String(featured[i].name || ""), "href": "f:" + i }) + return out + } + /// The span under the pointer (-1 = none). + property int hotSpan: -1 function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">") } @@ -1005,51 +1020,84 @@ Rectangle { return ("0" + Math.round(v * 255).toString(16)).slice(-2) }).join("") } - function link(href, name) { - var hot = artistLine.hoveredLink === href - return '' + esc(name) + '' + function span(i) { + var s = artistLine.spans[i] + var hot = artistLine.hotSpan === i && s.href !== "" + return '' + esc(s.name) + '' } textFormat: linkMode ? Text.StyledText : Text.PlainText text: { var names = featured.map(function (f) { return f.name }) if (!linkMode) return (root.item.artist || "") + (names.length ? ", " + names.join(", ") : "") - var parts = [root.item.artistId ? link("main", root.item.artist) : esc(root.item.artist)] - for (var i = 0; i < featured.length; i++) - parts.push(link("f:" + i, featured[i].name)) + var parts = [] + for (var i = 0; i < spans.length; i++) + parts.push(span(i)) return parts.join(", ") } - linkColor: theme.textMuted color: root.artistLink && root.item.artistId && artistLinkArea.containsMouse ? theme.textPrimary : theme.textMuted font.pixelSize: 12 elide: Text.ElideRight - onLinkActivated: function (href) { - if (href === "main") { + /// Which span sits at `x` (-1 = a separator or past the text): + /// the prefix up to each name's end is measured with this + /// Text's own font, so the edges match what is drawn. + function spanAt(x) { + if (!linkMode || x < 0) + return -1 + var prefix = "" + for (var i = 0; i < spans.length; i++) { + spanMetrics.text = prefix + var from = spanMetrics.advanceWidth + prefix += spans[i].name + spanMetrics.text = prefix + if (x >= from && x < spanMetrics.advanceWidth) + return i + prefix += ", " + } + return -1 + } + function activate(i) { + var s = artistLine.spans[i] + if (!s || s.href === "") + return + if (s.href === "main") { if (root.routeGoToExternally) root.goToRequested("artist") else QbzArtist.openArtist(root.item.artistId) return } - var f = featured[parseInt(href.slice(2))] + var f = featured[parseInt(s.href.slice(2))] if (!f) return if (f.id) QbzArtist.openArtist(f.id) else QbzSearch.searchSubmit(f.name) } - HoverHandler { - enabled: artistLine.linkMode - cursorShape: artistLine.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor + TextMetrics { + id: spanMetrics + font: artistLine.font } MouseArea { id: artistLinkArea anchors.fill: parent - enabled: root.artistLink && !!root.item.artistId && !artistLine.linkMode + enabled: root.artistLink && (artistLine.linkMode || !!root.item.artistId) hoverEnabled: true - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + cursorShape: (artistLine.linkMode + ? (artistLine.hotSpan >= 0 && artistLine.spans[artistLine.hotSpan].href !== "") + : enabled) ? Qt.PointingHandCursor : Qt.ArrowCursor + onPositionChanged: function (mouse) { + if (artistLine.linkMode) + artistLine.hotSpan = artistLine.spanAt(mouse.x) + } + onExited: artistLine.hotSpan = -1 // Same routing as the menu's "Go to artist" — Slint drives // both through one handler, so gating only the menu would // leave the LINK navigating to the track's own artist. - onClicked: { + onClicked: function (mouse) { + if (artistLine.linkMode) { + var i = artistLine.spanAt(mouse.x) + if (i >= 0) + artistLine.activate(i) + return + } if (root.routeGoToExternally) root.goToRequested("artist") else QbzArtist.openArtist(root.item.artistId) }