137 lines
4.3 KiB
QML
137 lines
4.3 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import "root:/config"
|
|
import "root:/components"
|
|
|
|
// Focused window title. Rapid application title churn is settled before it is
|
|
// painted, then the stable caption lands like a misregistered print layer.
|
|
Item {
|
|
id: root
|
|
|
|
required property var screenRef
|
|
|
|
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(screenRef)
|
|
readonly property HyprlandToplevel toplevel: Hyprland.activeToplevel
|
|
|
|
// Only speak for the monitor this bar lives on.
|
|
readonly property bool mine: toplevel !== null
|
|
&& (root.monitor === null || toplevel.monitor === null
|
|
|| toplevel.monitor.id === root.monitor.id)
|
|
|
|
readonly property string incomingTitle: mine && toplevel.title ? toplevel.title : "—"
|
|
property string displayedTitle: "—"
|
|
|
|
// The bar shrinks this when the three clusters would otherwise collide.
|
|
property real maxWidth: 240
|
|
|
|
implicitWidth: Math.min(maxWidth, Math.max(70, label.implicitWidth + 4))
|
|
implicitHeight: 20
|
|
|
|
Behavior on implicitWidth {
|
|
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutExpo }
|
|
}
|
|
|
|
Item {
|
|
id: titleStage
|
|
anchors.fill: parent
|
|
clip: true
|
|
|
|
Text {
|
|
id: label
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: parent.width
|
|
text: root.displayedTitle
|
|
color: root.mine ? Theme.subtext : Theme.muted
|
|
elide: Text.ElideRight
|
|
font.pixelSize: Theme.fsSmall
|
|
font.family: Theme.fontDisplay
|
|
font.italic: true
|
|
font.weight: Font.DemiBold
|
|
renderType: Text.NativeRendering
|
|
}
|
|
|
|
// Crimson duplicate: visible only during the landing transition. The
|
|
// small opposing offset gives the title Persona's rough print snap
|
|
// without leaving permanent motion in the bar.
|
|
Text {
|
|
id: ghost
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: parent.width
|
|
text: root.displayedTitle
|
|
color: Theme.accent
|
|
opacity: 0
|
|
elide: Text.ElideRight
|
|
font: label.font
|
|
renderType: Text.NativeRendering
|
|
}
|
|
}
|
|
|
|
function commitTitle(): void {
|
|
if (root.displayedTitle === root.incomingTitle)
|
|
return;
|
|
root.displayedTitle = root.incomingTitle;
|
|
titleLanding.restart();
|
|
}
|
|
|
|
// Browser tabs, terminals and editors can update titles several times in a
|
|
// frame. Keep the old caption steady until the stream settles instead of
|
|
// restarting an animation for every intermediate value.
|
|
Timer {
|
|
id: titleSettle
|
|
interval: 120
|
|
repeat: false
|
|
onTriggered: root.commitTitle()
|
|
}
|
|
|
|
onIncomingTitleChanged: titleSettle.restart()
|
|
Component.onCompleted: displayedTitle = incomingTitle
|
|
|
|
// A quick crimson wipe under the title on focus change.
|
|
Rectangle {
|
|
id: wipe
|
|
anchors.bottom: parent.bottom
|
|
height: 2
|
|
width: 0
|
|
color: Theme.accent
|
|
opacity: 0
|
|
}
|
|
|
|
SequentialAnimation {
|
|
id: titleLanding
|
|
ParallelAnimation {
|
|
NumberAnimation {
|
|
target: wipe; property: "width"
|
|
from: 0; to: root.width
|
|
duration: Theme.durBase; easing.type: Easing.OutExpo
|
|
}
|
|
NumberAnimation {
|
|
target: wipe; property: "opacity"
|
|
from: 0; to: 1; duration: Theme.durFast
|
|
}
|
|
NumberAnimation {
|
|
target: label; property: "x"
|
|
from: -10; to: 0
|
|
duration: Theme.durBase; easing.type: Easing.OutBack
|
|
}
|
|
NumberAnimation {
|
|
target: label; property: "opacity"
|
|
from: 0.35; to: 1; duration: Theme.durFast
|
|
}
|
|
NumberAnimation {
|
|
target: ghost; property: "x"
|
|
from: 9; to: -3
|
|
duration: Theme.durBase; easing.type: Easing.OutExpo
|
|
}
|
|
NumberAnimation {
|
|
target: ghost; property: "opacity"
|
|
from: 0.8; to: 0; duration: Theme.durBase
|
|
}
|
|
}
|
|
NumberAnimation {
|
|
target: wipe; property: "opacity"
|
|
to: 0; duration: Theme.durSlow
|
|
}
|
|
}
|
|
}
|