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)
}