From 6ef253f6676b441ce141b06e5778cb438ce559d2 Mon Sep 17 00:00:00 2001 From: eximora-private <198456237+eximora-private@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:10:56 +0200 Subject: [PATCH] feat: icon buttons draw the hover label they already carry --- client/chrome/TitleBar.qml | 21 ++++++++++++++++ client/components/IconButton.qml | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/client/chrome/TitleBar.qml b/client/chrome/TitleBar.qml index 4014c97..aecbd43 100644 --- a/client/chrome/TitleBar.qml +++ b/client/chrome/TitleBar.qml @@ -26,6 +26,9 @@ Rectangle { implicitHeight: Style.titleBarH color: Tokens.paper + // Above the page below it, so a hover label that hangs under the bar is not + // painted over by the sibling drawn after it. + z: 10 Component.onCompleted: root.discordOn = !!(Playback.settings && Playback.settings.discord_rpc === "true") Connections { @@ -48,6 +51,8 @@ Rectangle { icon: "sidebar" iconSize: Style.fs.lg diameter: Style.sp(8) + tip: root.sidebarOpen ? "Hide the rail" : "Show the rail" + tipBelow: true active: root.sidebarOpen onClicked: root.sidebarToggleClicked() } @@ -58,6 +63,8 @@ Rectangle { icon: "arrow-left" iconSize: Style.fs.lg diameter: Style.sp(8) + tip: "Back" + tipBelow: true enabled: Router.canGoBack onClicked: Router.pop() } @@ -65,6 +72,8 @@ Rectangle { icon: "arrow-right" iconSize: Style.fs.lg diameter: Style.sp(8) + tip: "Forward" + tipBelow: true enabled: false } @@ -173,6 +182,8 @@ Rectangle { icon: "close" iconSize: Style.fs.sm diameter: Style.sp(6) + tip: "Clear the search" + tipBelow: true onClicked: searchInput.text = "" } } @@ -223,6 +234,8 @@ Rectangle { icon: "group" iconSize: Style.fs.lg diameter: Style.sp(8) + tip: "Listen Together" + tipBelow: true active: !!(Playback.lt && Playback.lt.role && Playback.lt.role !== "none") onClicked: root.listenTogetherClicked() } @@ -230,6 +243,8 @@ Rectangle { icon: "discord" iconSize: Style.fs.lg diameter: Style.sp(8) + tip: root.discordOn ? "Discord rich presence is on" : "Discord rich presence is off" + tipBelow: true active: root.discordOn onClicked: { root.discordOn = !root.discordOn; @@ -244,6 +259,8 @@ Rectangle { icon: "panel" iconSize: Style.fs.lg diameter: Style.sp(8) + tip: root.panelOpen ? "Hide the panel" : "Show the panel" + tipBelow: true active: root.panelOpen onClicked: root.panelToggleClicked() } @@ -251,12 +268,16 @@ Rectangle { icon: "sound" iconSize: Style.fs.lg diameter: Style.sp(8) + tip: "Sound" + tipBelow: true onClicked: root.soundClicked() } IconButton { icon: "minimize" iconSize: Style.fs.lg diameter: Style.sp(8) + tip: "Mini player" + tipBelow: true onClicked: root.miniClicked() } } diff --git a/client/components/IconButton.qml b/client/components/IconButton.qml index f432ccb..334be8a 100644 --- a/client/components/IconButton.qml +++ b/client/components/IconButton.qml @@ -15,7 +15,13 @@ Item { property bool primary: false // A hairline ring, for a button that sits beside a Btn in an action row. property bool outlined: false + // The hover label, and the name assistive tech reads. It was declared here + // from the start and never drawn, so every tip: in the app set a property + // nothing rendered. property string tip: "" + // Draw the label under the button instead of over it, for a row that sits + // against the top of the window where there is no room above. + property bool tipBelow: false property color iconColor: root.primary ? Tokens.inkOnBone : !root.enabled ? Tokens.inkFaint : root.active ? Tokens.ink @@ -47,6 +53,41 @@ Item { color: root.iconColor } + // Hover label. One line, elided; fades unless reduced motion is asked for. + Rectangle { + id: tipPlate + visible: opacity > 0 + opacity: (hover.hovered && root.tip !== "") ? 1 : 0 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: root.tipBelow ? parent.bottom : undefined + anchors.bottom: root.tipBelow ? undefined : parent.top + anchors.topMargin: Style.sp(1.5) + anchors.bottomMargin: Style.sp(1.5) + implicitWidth: tipText.width + Style.sp(3) + implicitHeight: tipText.implicitHeight + Style.sp(2) + radius: Style.radius + color: Tokens.paperLift + border.width: 1 + border.color: Tokens.lineStrong + z: 100 + Behavior on opacity { enabled: !Tokens.reduceMotion; NumberAnimation { duration: Style.motion.snap } } + Text { + id: tipText + anchors.centerIn: parent + width: Math.min(Style.sp(56), implicitWidth) + text: root.tip + textFormat: Text.PlainText + color: Tokens.ink + font.family: Style.fontUi + font.pixelSize: Style.fs.xs + elide: Text.ElideRight + } + } + + Accessible.role: Accessible.Button + Accessible.name: root.tip + Accessible.onPressAction: root.clicked() + HoverHandler { id: hover; enabled: root.enabled } MouseArea { id: ma