quickshell: add a pixel worm and middle-click-to-knock-cards-off

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-08-04 08:30:06 +01:00
parent e39be67887
commit 4626e72219

View file

@ -374,6 +374,7 @@ in
Bar 1.0 Bar.qml
Lock 1.0 Lock.qml
Cheatsheet 1.0 Cheatsheet.qml
Weird 1.0 Weird.qml
'';
};
@ -827,6 +828,164 @@ in
'';
};
"quickshell/Weird.qml" = {
onChange = qsRestart;
text = ''
import QtQuick
// Decorative only. A pixel worm wanders the screen, and
// middle-clicking any Card tips it out of its panel to land in a
// pile at the bottom. Nothing here is load-bearing: delete the
// file, its qmldir line, the objectName on Bar's Card component
// and the one instantiation at the end of Bar.qml.
Item {
id: weird
anchors.fill: parent
// Pixel grid. Every worm segment snaps to it, which IS the
// pixel-art look there are no sprites involved.
readonly property int cell: 6
readonly property int segments: 10
readonly property int lag: 3 // ticks of trail per segment
// Worm
QtObject {
id: worm
property real hx: weird.width / 2
property real hy: weird.height / 2
property real dir: Math.random() * 2 * Math.PI
property var trail: []
}
Timer {
interval: 33; running: true; repeat: true
onTriggered: {
worm.dir += (Math.random() - 0.5) * 0.45;
let m = weird.cell * 3;
let nx = worm.hx + Math.cos(worm.dir) * 2.4;
let ny = worm.hy + Math.sin(worm.dir) * 2.4;
// Reflect the heading at the edges. Clamping alone
// leaves it grinding along the border forever.
if (nx < m || nx > weird.width - m) { worm.dir = Math.PI - worm.dir; nx = worm.hx; }
if (ny < m || ny > weird.height - m) { worm.dir = -worm.dir; ny = worm.hy; }
worm.hx = nx; worm.hy = ny;
let t = worm.trail;
t.unshift(Qt.point(nx, ny));
if (t.length > weird.segments * weird.lag) t.pop();
worm.trail = t; // reassign: arrays don't notify on mutation
}
}
Repeater {
model: weird.segments
Rectangle {
id: seg
required property int index
readonly property var p: worm.trail[index * weird.lag]
readonly property int size: weird.cell * (index < weird.segments - 3 ? 2 : 1)
visible: p !== undefined
width: size; height: size
// Snap to the grid sub-pixel motion would sand the
// pixels back off.
x: p ? Math.round((p.x - size / 2) / weird.cell) * weird.cell : 0
y: p ? Math.round((p.y - size / 2) / weird.cell) * weird.cell : 0
color: Theme.base0D
opacity: 1 - index * 0.05
antialiasing: false
Row { // eyes, head segment only
visible: seg.index === 0
anchors.centerIn: parent
spacing: 4
Repeater { model: 2; Rectangle { width: 2; height: 2; color: Theme.base00 } }
}
}
}
// Knocked-off cards
// hideSource pulls the panel's own copy out of the render
// pass without touching a single one of its bindings, so
// destroying the chunk puts the card straight back.
property var pile: []
property var resetOn: null // bind to whatever sweeps the pile
onResetOnChanged: sweep()
function knock(card) {
for (let d of pile) if (d.card === card) return;
let p = card.mapToItem(weird, 0, 0);
let c = chunk.createObject(weird, { card: card, x: p.x, y: p.y });
pile.push(c);
c.drop();
}
function sweep() {
for (let d of pile) d.destroy();
pile = [];
}
Component {
id: chunk
ShaderEffectSource {
id: bit
property Item card: null
sourceItem: card
hideSource: true
width: card ? card.width : 0
height: card ? card.height : 0
// Deliberately under-resolved so the card lands as
// chunky pixels like the worm. Raise the divisor for
// a cleaner-looking chunk.
textureSize: Qt.size(width / 3, height / 3)
smooth: false
transformOrigin: Item.Center
function drop() {
fallX.to = Math.max(Theme.barWidth, Math.min(weird.width - width, x + (Math.random() - 0.5) * 240));
fallY.to = weird.height - height - Theme.frameWidth;
spin.to = (Math.random() - 0.5) * 60;
fall.start();
}
ParallelAnimation {
id: fall
NumberAnimation { id: fallY; target: bit; property: "y"; duration: 1100; easing.type: Easing.OutBounce }
NumberAnimation { id: fallX; target: bit; property: "x"; duration: 1100; easing.type: Easing.OutQuad }
NumberAnimation { id: spin; target: bit; property: "rotation"; duration: 1100; easing.type: Easing.OutQuad }
}
}
}
// Sits above every panel, so it sees the press first. A press
// that is not on a card is handed straight back the volume
// widget's own middle-click still mutes.
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.MiddleButton
onPressed: (mouse) => {
let c = weird.cardAt(weird.parent, mouse.x, mouse.y);
if (c) weird.knock(c); else mouse.accepted = false;
}
}
// Deepest item tagged objectName "card" under the point.
// childAt() is no use: this overlay is the topmost child of
// the window, so it would only ever find itself.
function cardAt(item, x, y) {
for (let i = item.children.length - 1; i >= 0; i--) {
let c = item.children[i];
if (c === weird || !c.visible) continue;
let p = item.mapToItem(c, x, y);
if (p.x < 0 || p.y < 0 || p.x > c.width || p.y > c.height) continue;
let hit = cardAt(c, p.x, p.y);
if (hit) return hit;
if (c.objectName === "card") return c;
}
return null;
}
}
'';
};
"quickshell/Commands.qml" = {
onChange = qsRestart;
text = ''
@ -1262,6 +1421,8 @@ in
component Card: Rectangle {
default property alias cardData: _cardCol.data
property alias cardSpacing: _cardCol.spacing
// Weird.qml hit-tests for this tag on middle-click.
objectName: "card"
radius: Theme.radiusCard
color: Theme.cardBg
implicitHeight: _cardCol.height + 2 * Theme.cardPad
@ -6467,6 +6628,10 @@ in
}
}
}
// Last child = on top of every panel, which is what the
// card hit-test needs. Purely decorative, see Weird.qml.
Weird { resetOn: bar.activeDropdown }
}
'';
};