Compare commits
2
Commits
08146fe16f
...
ccb54f01d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccb54f01d9 | ||
|
|
58e0ba2f99 |
+1
-1
@@ -1 +1 @@
|
||||
|
||||
monitor=eDP-1, disable
|
||||
|
||||
@@ -128,16 +128,11 @@ PanelWindow {
|
||||
implicitWidth: Theme.padM * 2
|
||||
implicitHeight: Theme.barHeight
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
anchors.centerIn: parent
|
||||
width: 1
|
||||
height: 20
|
||||
color: Theme.alpha(Theme.outline, 0.9)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 20 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,13 @@ Item {
|
||||
readonly property real ruleInset: 5
|
||||
readonly property real ruleBand: root.standalone ? 0 : root.ruleHeight + root.ruleInset
|
||||
|
||||
// Modules with a notion of progress (the player's track position) draw it in
|
||||
// the same band, so it can never be struck through the content. A module
|
||||
// cannot do this for itself: anything it declares becomes a child of the
|
||||
// content holder, whose bottom edge is above the band, and the rule lands
|
||||
// across the text instead of under it. Negative means "no progress".
|
||||
property real progress: -1
|
||||
|
||||
// Total horizontal inset a module must add on top of its content width.
|
||||
readonly property real contentPad: standalone
|
||||
? (panelLoader.item?.contentPad ?? root.padding)
|
||||
@@ -100,16 +107,24 @@ Item {
|
||||
height: root.ruleHeight
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
// Track progress, shown until the pointer arrives and the hover rule
|
||||
// takes the band over.
|
||||
Skew {
|
||||
height: parent.height
|
||||
width: parent.width * Math.max(0, Math.min(1, root.progress))
|
||||
color: Theme.accent
|
||||
visible: root.progress >= 0 && !root.hovered && !root.active
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 480; easing.type: Easing.OutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
Skew {
|
||||
height: parent.height
|
||||
width: root.hovered || root.active ? parent.width : 0
|
||||
color: root.accent
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 2 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
|
||||
}
|
||||
|
||||
@@ -50,15 +50,11 @@ BarPill {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 2
|
||||
height: 22
|
||||
color: Theme.alpha(Theme.accent, 0.6)
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
SplitText {
|
||||
|
||||
@@ -16,6 +16,9 @@ BarPill {
|
||||
padding: Theme.padS
|
||||
visible: Media.hasPlayer || width > 1
|
||||
|
||||
// Track position, drawn by the pill in its own rule band.
|
||||
progress: Media.canSeek ? Media.progress : -1
|
||||
|
||||
implicitWidth: Media.hasPlayer
|
||||
? layout.implicitWidth + contentPad * 2
|
||||
: 0
|
||||
@@ -83,12 +86,21 @@ BarPill {
|
||||
}
|
||||
}
|
||||
|
||||
// Title and artist size to the text, up to a cap.
|
||||
//
|
||||
// These used to be a flat 130px wide each, which meant a short title
|
||||
// still reserved 130px and left a hand-sized hole between the artist and
|
||||
// the equaliser — the pill looked broken rather than compact. They take
|
||||
// what they need now and only start scrolling past the cap.
|
||||
Column {
|
||||
id: meta
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 0
|
||||
|
||||
readonly property real cap: 150
|
||||
|
||||
Marquee {
|
||||
width: 130
|
||||
width: Math.min(meta.cap, implicitWidth)
|
||||
height: 14
|
||||
text: Media.title || Media.identity
|
||||
color: Theme.text
|
||||
@@ -99,7 +111,7 @@ BarPill {
|
||||
}
|
||||
|
||||
Marquee {
|
||||
width: 130
|
||||
width: Math.min(meta.cap, implicitWidth)
|
||||
height: 12
|
||||
text: Media.artist || Media.album || Media.identity
|
||||
color: Theme.muted
|
||||
@@ -146,22 +158,6 @@ BarPill {
|
||||
}
|
||||
}
|
||||
|
||||
// Track progress, hairline along the bottom of the pill.
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 3
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.leanInset
|
||||
width: Math.max(0, (root.width - root.leanInset * 2) * Media.progress)
|
||||
height: 2
|
||||
color: Theme.accent
|
||||
visible: Media.canSeek
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 480; easing.type: Easing.OutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: if (visible) titleGlitch.restart()
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -35,7 +35,7 @@ BarPill {
|
||||
}
|
||||
|
||||
// Unread badge, chopped like everything else.
|
||||
Rectangle {
|
||||
Skew {
|
||||
visible: Notifs.unread > 0 && !Notifs.dnd
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 2
|
||||
@@ -45,6 +45,8 @@ BarPill {
|
||||
height: 12
|
||||
color: Theme.accent
|
||||
|
||||
// The badge count stays upright. It used to inherit the shear from the
|
||||
// transform on the block below, which at 8px left the digits smeared.
|
||||
Text {
|
||||
id: badge
|
||||
anchors.centerIn: parent
|
||||
@@ -55,11 +57,6 @@ BarPill {
|
||||
font.weight: Font.Bold
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 12 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -177,17 +177,12 @@ PanelWindow {
|
||||
implicitWidth: Theme.railWidth
|
||||
implicitHeight: Theme.padM + 4
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
anchors.centerIn: parent
|
||||
width: 22
|
||||
height: 1
|
||||
vertical: true
|
||||
color: Theme.alpha(Theme.outline, 0.9)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, 0, 0, 0,
|
||||
-Theme.skew, 1, 0, 22 * Theme.skew / 2,
|
||||
0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,15 +49,11 @@ BarPill {
|
||||
}
|
||||
}
|
||||
|
||||
component Divider: Rectangle {
|
||||
component Divider: Skew {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 1
|
||||
height: 18
|
||||
color: Theme.alpha(Theme.outline, 0.8)
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 18 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// The CPU/MEM/GPU labels are gone: three characters of chrome per metric,
|
||||
|
||||
@@ -5,6 +5,7 @@ import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/popouts"
|
||||
|
||||
// StatusNotifier tray, as a vertical column for the right-hand rail. Left click
|
||||
// activates, right click opens the app's own menu, middle click is the
|
||||
@@ -72,7 +73,7 @@ Column {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: {
|
||||
if (entry.modelData.onlyMenu) {
|
||||
menuAnchor.open();
|
||||
menu.toggle();
|
||||
} else {
|
||||
entry.modelData.activate();
|
||||
}
|
||||
@@ -81,7 +82,9 @@ Column {
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: menuAnchor.open()
|
||||
onTapped: {
|
||||
if (entry.modelData.hasMenu) menu.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
@@ -90,13 +93,10 @@ Column {
|
||||
}
|
||||
|
||||
// Menus open to the left, into the screen rather than off its edge.
|
||||
QsMenuAnchor {
|
||||
id: menuAnchor
|
||||
menu: entry.modelData.menu
|
||||
anchor.item: entry
|
||||
anchor.edges: Edges.Left
|
||||
anchor.gravity: Edges.Left
|
||||
anchor.margins.right: Theme.popoutGap
|
||||
TrayMenu {
|
||||
id: menu
|
||||
anchorItem: entry
|
||||
menuHandle: entry.modelData.menu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
id: tick
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -89,18 +89,10 @@ Item {
|
||||
if (pip.occupied) return Theme.alpha(Theme.text, 0.5);
|
||||
return "transparent";
|
||||
}
|
||||
border.color: pip.occupied || pip.focused
|
||||
borderColor: pip.occupied || pip.focused
|
||||
? "transparent"
|
||||
: Theme.alpha(Theme.text, 0.35)
|
||||
border.width: 1
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(
|
||||
1, -Theme.skew, 0, pip.height * Theme.skew,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1)
|
||||
}
|
||||
borderWidth: 1
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durBase }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// A segmented load meter — slanted ticks that light up left to right. Reads as
|
||||
// a gauge at a glance and keeps the panel language consistent.
|
||||
@@ -31,9 +32,10 @@ Item {
|
||||
width: root.segmentWidth
|
||||
height: root.height
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
width: root.segmentWidth
|
||||
height: root.height
|
||||
lean: root.lean
|
||||
color: parent.index < root.litCount ? root.activeColor : root.idleColor
|
||||
|
||||
// Peak segments glow, so a pegged CPU is visible peripherally.
|
||||
@@ -41,14 +43,6 @@ Item {
|
||||
? (parent.index >= root.segments - 2 ? 1.0 : 0.9)
|
||||
: 1.0
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(
|
||||
1, -root.lean, 0, root.height * root.lean,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1)
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durBase }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// One notification, used both in the history list and as a floating toast.
|
||||
@@ -17,14 +18,20 @@ Item {
|
||||
|
||||
signal dismissed()
|
||||
|
||||
// The card's own geometry, published so anything drawn along its edges —
|
||||
// the toast countdown rule — can stay inside the chopped corners instead of
|
||||
// running out past them.
|
||||
readonly property real lean: 0.05
|
||||
readonly property real chop: 12
|
||||
|
||||
implicitHeight: panel.height
|
||||
|
||||
P5Panel {
|
||||
id: panel
|
||||
width: parent.width
|
||||
height: layout.implicitHeight + Theme.padM * 2
|
||||
lean: 0.05
|
||||
chop: 12
|
||||
lean: root.lean
|
||||
chop: root.chop
|
||||
fill: Theme.surface
|
||||
fillOpacity: 0.97
|
||||
border: Theme.alpha(root.tone, hover.hovered ? 0.95 : 0.5)
|
||||
@@ -117,18 +124,13 @@ Item {
|
||||
implicitWidth: actionLabel.implicitWidth + Theme.padM
|
||||
implicitHeight: 22
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
anchors.fill: parent
|
||||
color: actionHover.hovered
|
||||
? Theme.alpha(root.tone, 0.3)
|
||||
: Theme.alpha(root.tone, 0.1)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(root.tone, 0.6)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
borderWidth: 1
|
||||
borderColor: Theme.alpha(root.tone, 0.6)
|
||||
}
|
||||
|
||||
Text {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// Leaning slider with a chopped handle. Drag or click anywhere on the rail.
|
||||
Item {
|
||||
@@ -17,33 +18,23 @@ Item {
|
||||
readonly property real _lean: Theme.skew * height
|
||||
|
||||
// Rail
|
||||
Rectangle {
|
||||
Skew {
|
||||
id: rail
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 6
|
||||
color: Theme.alpha(Theme.outline, 0.7)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 6 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Fill
|
||||
Rectangle {
|
||||
Skew {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
width: Math.max(0, Math.min(1, root.value)) * root.width
|
||||
height: 6
|
||||
color: root.enabledControl ? root.accent : Theme.muted
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 6 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
enabled: !drag.active
|
||||
NumberAnimation { duration: Theme.durFast }
|
||||
@@ -55,7 +46,7 @@ Item {
|
||||
}
|
||||
|
||||
// Handle
|
||||
Rectangle {
|
||||
Skew {
|
||||
id: handle
|
||||
width: 6
|
||||
height: hover.hovered || drag.active ? 18 : 14
|
||||
@@ -63,11 +54,6 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: Math.max(0, Math.min(1, root.value)) * root.width - width / 2
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, height * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// A leaning block — the small-scale counterpart to P5Panel.
|
||||
//
|
||||
// Every tick, rule, meter segment and slider rail in this shell leans at
|
||||
// Theme.skew. Shearing a plain Rectangle with a Matrix4x4 does that, but Qt
|
||||
// rasterises a Rectangle's geometry with no antialiasing at all, so the slanted
|
||||
// edges come out as a hard pixel staircase — at 18px tall that staircase is
|
||||
// most of what you see, and it reads as broken rendering rather than as a
|
||||
// deliberate angle. Drawing the parallelogram as a Shape with the curve
|
||||
// renderer gets analytic coverage AA on the diagonals instead.
|
||||
//
|
||||
// Drop-in for a sheared Rectangle: set width/height as before and use `color`,
|
||||
// `borderColor` and `borderWidth` the same way.
|
||||
Shape {
|
||||
id: root
|
||||
|
||||
property color color: "transparent"
|
||||
property color borderColor: "transparent"
|
||||
property real borderWidth: 0
|
||||
|
||||
// Lean of the vertical edges as a fraction of height, matching P5Panel.
|
||||
property real lean: Theme.skew
|
||||
property bool leanLeft: false
|
||||
|
||||
// Wide, short pieces — the rail's hairline rules — slant their horizontal
|
||||
// edges by a fraction of width instead, exactly as P5Panel does for the
|
||||
// rail slab.
|
||||
property bool vertical: false
|
||||
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
asynchronous: false
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.color
|
||||
strokeColor: root.borderWidth > 0 ? root.borderColor : "transparent"
|
||||
strokeWidth: root.borderWidth
|
||||
joinStyle: ShapePath.MiterJoin
|
||||
capStyle: ShapePath.FlatCap
|
||||
|
||||
PathPolyline {
|
||||
path: {
|
||||
const w = root.width, h = root.height, l = root.lean;
|
||||
if (root.vertical)
|
||||
return Geom.closed(root.leanLeft ? Geom.parallelogramVL(w, h, l)
|
||||
: Geom.parallelogramV(w, h, l));
|
||||
return Geom.closed(root.leanLeft ? Geom.parallelogramL(w, h, l)
|
||||
: Geom.parallelogram(w, h, l));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,17 +231,12 @@ Item {
|
||||
Repeater {
|
||||
model: Math.min(surface.entry.length, 22)
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
required property int index
|
||||
width: 10
|
||||
height: 20
|
||||
color: surface.failed ? Theme.accent : Theme.primary
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 20 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
|
||||
// Each new block snaps in.
|
||||
Component.onCompleted: pop.start()
|
||||
NumberAnimation {
|
||||
|
||||
@@ -82,12 +82,24 @@ PanelWindow {
|
||||
}
|
||||
|
||||
// Countdown rule that drains along the bottom edge.
|
||||
Rectangle {
|
||||
//
|
||||
// It used to be a full-bleed rectangle across the whole toast,
|
||||
// which meant it ran straight through the card's chopped
|
||||
// bottom-left corner and out the other side — a stray bar
|
||||
// hanging off the card at the one place the silhouette is
|
||||
// supposed to be cut. It lives inside the cut now, and ends
|
||||
// where the leaning right edge does.
|
||||
readonly property real fuseSpan: Math.max(0,
|
||||
width - card.chop - card.implicitHeight * card.lean)
|
||||
|
||||
Skew {
|
||||
id: fuse
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.bottomMargin: 1
|
||||
x: card.chop
|
||||
height: 2
|
||||
width: parent.width
|
||||
width: toast.fuseSpan
|
||||
lean: card.lean
|
||||
color: Theme.alpha(card.tone, 0.7)
|
||||
visible: life.running
|
||||
}
|
||||
@@ -96,7 +108,7 @@ PanelWindow {
|
||||
id: life
|
||||
target: fuse
|
||||
property: "width"
|
||||
from: toast.width
|
||||
from: toast.fuseSpan
|
||||
to: 0
|
||||
running: false
|
||||
duration: Notifs.timeoutFor(toast.modelData)
|
||||
|
||||
@@ -127,17 +127,12 @@ Popout {
|
||||
height: 30
|
||||
|
||||
// Today gets a leaning crimson slab.
|
||||
Rectangle {
|
||||
Skew {
|
||||
anchors.centerIn: parent
|
||||
width: 26
|
||||
height: 24
|
||||
visible: cell.modelData.today
|
||||
color: Theme.accent
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 24 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
@@ -229,16 +224,11 @@ Popout {
|
||||
implicitWidth: 22
|
||||
implicitHeight: 22
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
anchors.fill: parent
|
||||
color: stepHover.hovered ? Theme.alpha(Theme.accent, 0.25) : "transparent"
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.outline, 0.7)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
borderWidth: 1
|
||||
borderColor: Theme.alpha(Theme.outline, 0.7)
|
||||
}
|
||||
|
||||
Icon {
|
||||
|
||||
@@ -11,7 +11,11 @@ Popout {
|
||||
id: root
|
||||
|
||||
contentWidth: 400
|
||||
contentHeight: 200 + (Media.players.length > 1 ? 34 : 0)
|
||||
|
||||
// The seek row used to sit flush against the bottom edge, where the slab's
|
||||
// chamfer cuts the corner away — the elapsed time was half outside the
|
||||
// panel. The extra height is the room that row needs to clear the cut.
|
||||
contentHeight: 224 + (Media.players.length > 1 ? 34 : 0)
|
||||
|
||||
PopoutSurface {
|
||||
id: surface
|
||||
@@ -176,8 +180,10 @@ Popout {
|
||||
id: seekArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: Theme.padS
|
||||
anchors.rightMargin: Theme.padS
|
||||
anchors.bottom: picker.visible ? picker.top : parent.bottom
|
||||
anchors.bottomMargin: Theme.padS
|
||||
anchors.bottomMargin: picker.visible ? Theme.padS : Theme.padM
|
||||
height: 26
|
||||
visible: Media.hasPlayer
|
||||
|
||||
@@ -236,18 +242,13 @@ Popout {
|
||||
implicitWidth: chipLabel.implicitWidth + Theme.padM
|
||||
implicitHeight: 22
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
anchors.fill: parent
|
||||
color: chip.current
|
||||
? Theme.alpha(Theme.primary, 0.22)
|
||||
: (chipHover.hovered ? Theme.alpha(Theme.primary, 0.1) : "transparent")
|
||||
border.width: 1
|
||||
border.color: chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
borderWidth: 1
|
||||
borderColor: chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7)
|
||||
}
|
||||
|
||||
Text {
|
||||
@@ -285,23 +286,18 @@ Popout {
|
||||
implicitWidth: primary ? 38 : 30
|
||||
implicitHeight: primary ? 38 : 30
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
anchors.fill: parent
|
||||
color: {
|
||||
if (!tb.enabledControl) return "transparent";
|
||||
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.alpha(Theme.primary, 0.22);
|
||||
return tb.primary ? Theme.alpha(Theme.primary, 0.18) : "transparent";
|
||||
}
|
||||
border.width: tb.primary ? 2 : 1
|
||||
border.color: tb.enabledControl
|
||||
borderWidth: tb.primary ? 2 : 1
|
||||
borderColor: tb.enabledControl
|
||||
? (tbHover.hovered ? Theme.text : Theme.alpha(Theme.primary, 0.6))
|
||||
: Theme.alpha(Theme.outline, 0.4)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, tb.height * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durFast }
|
||||
}
|
||||
|
||||
@@ -202,16 +202,11 @@ Popout {
|
||||
|
||||
implicitHeight: 40
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
anchors.fill: parent
|
||||
color: Theme.alpha(rateBlock.tone, 0.08)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(rateBlock.tone, 0.35)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, rateBlock.height * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
borderWidth: 1
|
||||
borderColor: Theme.alpha(rateBlock.tone, 0.35)
|
||||
}
|
||||
|
||||
Row {
|
||||
|
||||
@@ -59,15 +59,12 @@ Item {
|
||||
color: Theme.alpha(root.accent, 0.8)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Skew {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: 16
|
||||
height: 2
|
||||
color: Theme.accent
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -1.4, 0, 2.8,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
lean: 1.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// The tray's context menu, on the shell's own slab.
|
||||
//
|
||||
// Quickshell can hand a StatusNotifier menu straight to Qt's platform menu
|
||||
// implementation, but that draws a stock widget popup — square corners, system
|
||||
// palette, no lean — hanging off the corner of a shell that has none of those
|
||||
// things. This walks the menu with QsMenuOpener instead and draws it as a
|
||||
// P5Panel, so a right click on a tray icon produces the same object language as
|
||||
// everything else on the rail.
|
||||
//
|
||||
// Dismissal is a Hyprland focus grab: click anywhere outside and the compositor
|
||||
// tells us to close.
|
||||
PopupWindow {
|
||||
id: root
|
||||
|
||||
required property Item anchorItem
|
||||
property var menuHandle: null
|
||||
|
||||
// Room around the slab for the entry overshoot and the offset black copy.
|
||||
readonly property int bleed: 20
|
||||
readonly property int minWidth: 160
|
||||
readonly property int maxWidth: 320
|
||||
|
||||
// The slab's own lean and padding, which the entries sit inside. Sizing the
|
||||
// window to the entry widths alone left every label short by that inset and
|
||||
// elided perfectly ordinary items.
|
||||
readonly property real slabLean: 0.06
|
||||
readonly property real slabPad: Theme.padS
|
||||
|
||||
readonly property real contentHeight: list.implicitHeight + slabPad * 2
|
||||
readonly property real contentWidth:
|
||||
Math.max(root.minWidth, Math.min(root.maxWidth, list.menuWidth))
|
||||
+ (slabPad + contentHeight * slabLean) * 2
|
||||
|
||||
anchor.item: anchorItem
|
||||
anchor.edges: Edges.Left
|
||||
anchor.gravity: Edges.Left
|
||||
anchor.adjustment: PopupAdjustment.SlideY
|
||||
anchor.margins.right: Theme.popoutGap
|
||||
|
||||
implicitWidth: contentWidth + bleed * 2
|
||||
implicitHeight: contentHeight + bleed * 2
|
||||
color: "transparent"
|
||||
visible: shown
|
||||
|
||||
property bool shown: false
|
||||
|
||||
|
||||
function open() {
|
||||
if (!menuHandle) return;
|
||||
closeAnim.stop();
|
||||
shown = true;
|
||||
openAnim.restart();
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!shown) return;
|
||||
openAnim.stop();
|
||||
closeAnim.restart();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shown) close(); else open();
|
||||
}
|
||||
|
||||
// The grab can only take hold once the popup is actually mapped. Asked for
|
||||
// in the same tick as `shown`, Hyprland has no surface to hand it and
|
||||
// clears it immediately — which closed the menu the instant it opened.
|
||||
Timer {
|
||||
id: grabDelay
|
||||
interval: 60
|
||||
running: root.shown
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
active: root.shown && !grabDelay.running
|
||||
windows: [root]
|
||||
onCleared: root.close()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: holder
|
||||
x: root.bleed
|
||||
y: root.bleed
|
||||
width: root.contentWidth
|
||||
height: root.contentHeight
|
||||
|
||||
opacity: 0
|
||||
scale: 0.94
|
||||
transformOrigin: Item.Right
|
||||
|
||||
PopoutSurface {
|
||||
anchors.fill: parent
|
||||
tone: Theme.accent
|
||||
padding: root.slabPad
|
||||
chop: 12
|
||||
lean: root.slabLean
|
||||
|
||||
TrayMenuList {
|
||||
id: list
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
// Only walk the menu while it is on screen. Bound unconditionally,
|
||||
// every tray app would get a GetLayout the moment the shell
|
||||
// starts, for a menu nobody has asked for yet.
|
||||
menuHandle: root.shown ? root.menuHandle : null
|
||||
onActivated: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: openAnim
|
||||
NumberAnimation {
|
||||
target: holder; property: "opacity"
|
||||
to: 1.0; duration: Theme.durBase; easing.type: Easing.OutExpo
|
||||
}
|
||||
NumberAnimation {
|
||||
target: holder; property: "scale"
|
||||
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack
|
||||
}
|
||||
NumberAnimation {
|
||||
target: holder; property: "x"
|
||||
from: root.bleed + 14; to: root.bleed
|
||||
duration: Theme.durSlow; easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: closeAnim
|
||||
NumberAnimation {
|
||||
target: holder; property: "opacity"
|
||||
to: 0.0; duration: Theme.durFast; easing.type: Easing.InQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
target: holder; property: "scale"
|
||||
to: 0.96; duration: Theme.durFast; easing.type: Easing.InQuad
|
||||
}
|
||||
onFinished: {
|
||||
root.shown = false;
|
||||
holder.x = root.bleed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// One level of a StatusNotifier menu, drawn in the shell's own language.
|
||||
//
|
||||
// Submenus expand inline rather than opening a second window: a tray menu is
|
||||
// two or three items deep at most, and a stack of leaning slabs chasing each
|
||||
// other across the screen would read as clutter. The type nests itself for
|
||||
// child levels, so this file is the whole menu.
|
||||
Column {
|
||||
id: root
|
||||
|
||||
// A QsMenuHandle — either the item's root menu or a submenu entry.
|
||||
required property var menuHandle
|
||||
|
||||
// Raised by any level when an entry is actually invoked, so the window can
|
||||
// close itself.
|
||||
signal activated()
|
||||
|
||||
// What the window should size itself to. Deliberately NOT `implicitWidth`.
|
||||
//
|
||||
// A Column takes its implicit width from how wide its children ended up, and
|
||||
// the rows are stretched to the Column's own width so a hover block can span
|
||||
// the entry. Sizing the window off that closes the loop — window width feeds
|
||||
// row width feeds Column implicit width feeds window width — and it does not
|
||||
// settle: each pass adds a float epsilon, so the shell spins at 100% CPU
|
||||
// forever. Slack's menu was the first to hit it and it took the whole bar
|
||||
// down with it, past the reach of anything but SIGKILL. Measuring the
|
||||
// strings against the font instead keeps sizing entirely out of the layout.
|
||||
readonly property real menuWidth: {
|
||||
let w = 0;
|
||||
for (const e of (opener.children?.values ?? [])) {
|
||||
if (e.isSeparator) continue;
|
||||
let ew = fm.advanceWidth(root.clean(e.text)) + Theme.padM * 2 + Theme.padS * 2;
|
||||
if (e.buttonType !== QsMenuButtonType.None || e.icon !== "")
|
||||
ew += 16 + Theme.padS;
|
||||
if (e.hasChildren) ew += 18;
|
||||
w = Math.max(w, ew);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
// DBus menus carry Qt's mnemonic underscores; nothing here handles
|
||||
// accelerators, so they would just be stray marks.
|
||||
function clean(s: string): string {
|
||||
return String(s ?? "").replace(/_([^_])/g, "$1");
|
||||
}
|
||||
|
||||
spacing: 0
|
||||
|
||||
FontMetrics {
|
||||
id: fm
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Theme.weightBody
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: opener
|
||||
menu: root.menuHandle
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: opener.children
|
||||
|
||||
Item {
|
||||
id: row
|
||||
|
||||
required property var modelData
|
||||
readonly property var entry: modelData
|
||||
readonly property bool interactive: entry.enabled && !entry.isSeparator
|
||||
readonly property bool hovered: rowHover.hovered && interactive
|
||||
|
||||
// Submenus stay collapsed until asked for.
|
||||
property bool expanded: false
|
||||
|
||||
// Height only. Width is the Column's business — see `menuWidth`.
|
||||
implicitHeight: entry.isSeparator ? 9 : 26 + sub.height
|
||||
|
||||
width: root.width
|
||||
|
||||
// ------------------------------------------------------ separator
|
||||
//
|
||||
// A horizontal rule slants by a fraction of its own width, so at
|
||||
// menu width the house lean turns a hairline into a 30px diagonal
|
||||
// gash straight across the entries above and below it. The rise is
|
||||
// pinned in pixels instead, matching the rail's dividers.
|
||||
Skew {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: row.entry.isSeparator
|
||||
width: parent.width - Theme.padM * 2
|
||||
x: Theme.padM
|
||||
height: 1
|
||||
vertical: true
|
||||
lean: width > 0 ? 5 / width : 0
|
||||
color: Theme.alpha(Theme.outline, 0.9)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- item
|
||||
Item {
|
||||
id: line
|
||||
width: parent.width
|
||||
height: row.entry.isSeparator ? 0 : 26
|
||||
visible: !row.entry.isSeparator
|
||||
|
||||
// Hover is a solid crimson block, the same statement the
|
||||
// workspace ticks and the rail icons make.
|
||||
Skew {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.padS
|
||||
anchors.rightMargin: Theme.padS
|
||||
color: row.hovered ? Theme.accent : "transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durFast }
|
||||
}
|
||||
}
|
||||
|
||||
// Check mark / radio dot, or the entry's own icon. Both occupy
|
||||
// the same slot so the labels line up either way.
|
||||
Item {
|
||||
id: leading
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: row.entry.buttonType !== QsMenuButtonType.None
|
||||
|| row.entry.icon !== "" ? 16 : 0
|
||||
height: 12
|
||||
|
||||
Skew {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: row.entry.buttonType !== QsMenuButtonType.None
|
||||
width: 10
|
||||
height: 10
|
||||
borderWidth: 1
|
||||
borderColor: row.hovered ? Theme.ink : Theme.alpha(Theme.text, 0.6)
|
||||
color: row.entry.checkState === Qt.Checked
|
||||
? (row.hovered ? Theme.ink : Theme.accent)
|
||||
: "transparent"
|
||||
}
|
||||
|
||||
IconImage {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: row.entry.buttonType === QsMenuButtonType.None
|
||||
&& row.entry.icon !== ""
|
||||
width: 14
|
||||
height: 14
|
||||
source: row.entry.icon
|
||||
asynchronous: true
|
||||
|
||||
// Same flattening the tray icons get — brand colour in
|
||||
// a two-hue palette is the one thing that breaks it.
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
colorization: 1.0
|
||||
colorizationColor: row.hovered ? Theme.ink : Theme.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.left: leading.right
|
||||
anchors.leftMargin: leading.width > 0 ? Theme.padS : Theme.padM
|
||||
anchors.right: arrow.left
|
||||
anchors.rightMargin: Theme.padS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
text: root.clean(row.entry.text)
|
||||
elide: Text.ElideRight
|
||||
color: !row.entry.enabled
|
||||
? Theme.muted
|
||||
: (row.hovered ? Theme.ink : Theme.text)
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Theme.weightBody
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
id: arrow
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: row.entry.hasChildren ? 18 : 0
|
||||
visible: row.entry.hasChildren
|
||||
text: row.expanded ? "" : ""
|
||||
color: row.hovered ? Theme.ink : Theme.subtext
|
||||
font.family: Theme.fontIcon
|
||||
font.pixelSize: Theme.fsMicro
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: rowHover
|
||||
enabled: row.interactive
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: row.interactive
|
||||
onTapped: {
|
||||
if (row.entry.hasChildren) {
|
||||
row.expanded = !row.expanded;
|
||||
} else {
|
||||
row.entry.triggered();
|
||||
root.activated();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- submenu
|
||||
Item {
|
||||
id: sub
|
||||
anchors.top: line.bottom
|
||||
width: parent.width
|
||||
height: subLoader.item ? subLoader.item.implicitHeight : 0
|
||||
clip: true
|
||||
visible: height > 0
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutExpo }
|
||||
}
|
||||
|
||||
// Loaded by URL rather than as an inline component: QML rejects
|
||||
// a type that instantiates itself, even lazily, and a menu tree
|
||||
// is the one place recursion is the natural shape.
|
||||
Loader {
|
||||
id: subLoader
|
||||
x: Theme.padM
|
||||
width: parent.width - Theme.padM
|
||||
|
||||
onLoaded: item.activated.connect(root.activated)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: row
|
||||
|
||||
function onExpandedChanged() {
|
||||
if (row.expanded)
|
||||
subLoader.setSource(Qt.resolvedUrl("TrayMenuList.qml"),
|
||||
{ menuHandle: row.entry });
|
||||
else
|
||||
subLoader.source = "";
|
||||
}
|
||||
}
|
||||
|
||||
Skew {
|
||||
x: Theme.padS
|
||||
width: 1
|
||||
height: parent.height
|
||||
visible: row.expanded
|
||||
color: Theme.alpha(Theme.accent, 0.8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user