added quickshell

This commit is contained in:
2026-08-10 23:57:26 +02:00
parent cbaf55cfc5
commit 47ff5233bd
69 changed files with 6945 additions and 80 deletions
+120
View File
@@ -0,0 +1,120 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
// Radial gauge used across the resource popout. Sweeps a 270° arc with a
// tick-marked rail behind it and the reading set in the middle.
Item {
id: root
property real value: 0 // 0..100
property string label: ""
property string readout: Math.round(value) + "%"
property string sub: ""
property color arcColor: Theme.heat(value)
property real thickness: 7
property real startAngle: 135
property real sweep: 270
property int ticks: 24
implicitWidth: 96
implicitHeight: 96
readonly property real _r: Math.min(width, height) / 2 - thickness / 2 - 4
readonly property real _cx: width / 2
readonly property real _cy: height / 2
// Rail
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: Theme.alpha(Theme.outline, 0.5)
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep
}
}
}
// Tick marks around the rail.
Repeater {
model: root.ticks
Rectangle {
required property int index
readonly property real a: (root.startAngle + root.sweep * (index / (root.ticks - 1))) * Math.PI / 180
readonly property real rr: root._r + root.thickness / 2 + 3
width: 2
height: index % 4 === 0 ? 6 : 3
color: Theme.alpha(Theme.primary, index % 4 === 0 ? 0.55 : 0.25)
x: root._cx + Math.cos(a) * rr - width / 2
y: root._cy + Math.sin(a) * rr - height / 2
rotation: (root.startAngle + root.sweep * (index / (root.ticks - 1))) + 90
}
}
// Value arc
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: root.arcColor
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
id: valueArc
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep * Math.max(0, Math.min(100, root.value)) / 100
Behavior on sweepAngle {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutExpo
}
}
}
}
}
Column {
anchors.centerIn: parent
spacing: -2
SplitText {
anchors.horizontalCenter: parent.horizontalCenter
text: root.readout
pixelSize: Theme.fsLarge
color: Theme.text
split: 1.2
splitOpacity: 0.6
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.label
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: root.sub !== ""
text: root.sub
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
+100
View File
@@ -0,0 +1,100 @@
pragma Singleton
import QtQuick
import Quickshell
// Polygon maths for the Persona 5 panel language: everything leans, and corners
// get chopped rather than rounded.
Singleton {
id: root
// A right-leaning parallelogram filling w x h. `lean` is the horizontal
// offset applied to the top edge, as a fraction of height.
function parallelogram(w: real, h: real, lean: real): var {
const off = h * lean;
return [
Qt.point(off, 0),
Qt.point(w, 0),
Qt.point(w - off, h),
Qt.point(0, h)
];
}
// Same, leaning the other way.
function parallelogramL(w: real, h: real, lean: real): var {
const off = h * lean;
return [
Qt.point(0, 0),
Qt.point(w - off, 0),
Qt.point(w, h),
Qt.point(off, h)
];
}
function rect(w: real, h: real): var {
return [Qt.point(0, 0), Qt.point(w, 0), Qt.point(w, h), Qt.point(0, h)];
}
// Move `dist` px from `p` toward `q`.
function toward(p: point, q: point, dist: real): point {
const dx = q.x - p.x;
const dy = q.y - p.y;
const len = Math.hypot(dx, dy);
if (len < 0.0001) return p;
const t = Math.min(dist, len / 2) / len;
return Qt.point(p.x + dx * t, p.y + dy * t);
}
// Chop the listed corners of a polygon. `corners` is an array of indices;
// pass null to chop every corner.
function chamfer(pts: var, size: real, corners: var): var {
if (size <= 0) return root.closed(pts);
const n = pts.length;
const out = [];
for (let i = 0; i < n; i++) {
const p = pts[i];
if (corners !== null && corners.indexOf(i) === -1) {
out.push(p);
continue;
}
out.push(root.toward(p, pts[(i - 1 + n) % n], size));
out.push(root.toward(p, pts[(i + 1) % n], size));
}
return root.closed(out);
}
function closed(pts: var): var {
if (pts.length === 0) return pts;
const out = pts.slice();
out.push(pts[0]);
return out;
}
// Shrink a polygon toward its centroid — used for inner strokes.
function inset(pts: var, amount: real): var {
let cx = 0, cy = 0;
for (const p of pts) { cx += p.x; cy += p.y; }
cx /= pts.length; cy /= pts.length;
return pts.map(p => {
const dx = cx - p.x, dy = cy - p.y;
const len = Math.hypot(dx, dy);
if (len < 0.0001) return p;
const t = amount / len;
return Qt.point(p.x + dx * t, p.y + dy * t);
});
}
// A jagged "torn paper" edge, the Persona 5 speech-bubble signature.
// Returns a polygon `w` x `h` whose bottom edge is serrated.
function torn(w: real, h: real, teeth: int, depth: real): var {
const out = [Qt.point(0, 0), Qt.point(w, 0)];
const step = w / teeth;
for (let i = 0; i < teeth; i++) {
const x1 = w - i * step - step / 2;
const x2 = w - (i + 1) * step;
out.push(Qt.point(x1, h));
out.push(Qt.point(x2, h - depth));
}
return out;
}
}
+45
View File
@@ -0,0 +1,45 @@
import QtQuick
import "root:/config"
// Nerd Font glyph with an optional pulse, used for every status indicator.
Text {
id: root
property bool pulsing: false
property real pulseScale: 1.18
font.family: Theme.fontIcon
font.pixelSize: Theme.fsLarge
color: Theme.primary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
SequentialAnimation on scale {
running: root.pulsing
loops: Animation.Infinite
alwaysRunToEnd: true
NumberAnimation { to: root.pulseScale; duration: 620; easing.type: Easing.OutExpo }
NumberAnimation { to: 1.0; duration: 620; easing.type: Easing.InOutQuad }
}
function bump() {
bumpAnim.restart();
}
SequentialAnimation {
id: bumpAnim
NumberAnimation {
target: root; property: "scale"
to: 1.35; duration: Theme.durFast; easing.type: Easing.OutExpo
}
NumberAnimation {
target: root; property: "scale"
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack
}
}
}
+62
View File
@@ -0,0 +1,62 @@
import QtQuick
import "root:/config"
// Horizontally scrolling text that only moves when it actually overflows, and
// pauses at each end so titles stay readable.
Item {
id: root
property string text: ""
property int pixelSize: Theme.fsSmall
property string family: Theme.fontDisplay
property int weight: Theme.weightBody
property bool italic: false
property color color: Theme.text
property real speed: 26 // px per second
property int pause: 1400 // ms held at each end
property bool running: true
readonly property bool overflowing: label.implicitWidth > width + 1
clip: true
implicitHeight: label.implicitHeight
implicitWidth: label.implicitWidth
Text {
id: label
y: 0
height: root.height
text: root.text
color: root.color
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
renderType: Text.NativeRendering
}
SequentialAnimation {
id: scroll
running: root.running && root.overflowing && root.visible
loops: Animation.Infinite
PauseAnimation { duration: root.pause }
NumberAnimation {
target: label; property: "x"
from: 0; to: Math.min(0, root.width - label.implicitWidth)
duration: Math.max(1, Math.abs(root.width - label.implicitWidth) / root.speed * 1000)
easing.type: Easing.Linear
}
PauseAnimation { duration: root.pause }
NumberAnimation {
target: label; property: "x"
to: 0
duration: Theme.durSlow
easing.type: Easing.OutExpo
}
}
onOverflowingChanged: if (!overflowing) label.x = 0
onTextChanged: { label.x = 0; scroll.restart(); }
}
+14
View File
@@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Effects
// Layer effect that clips whatever it is applied to into the silhouette of
// `maskItem` — used to chop album art into the shell's panel shape.
MultiEffect {
property Item maskItem
maskEnabled: maskItem !== null
maskSource: maskItem
// Hard edge — the chopped corners should read as cuts, not fades.
maskThresholdMin: 0.5
maskSpreadAtMin: 0.0
}
+59
View File
@@ -0,0 +1,59 @@
import QtQuick
import "root:/config"
// 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.
Item {
id: root
property real value: 0 // 0..100
property int segments: 14
property real segmentWidth: 4
property real spacing: 3
property color activeColor: Theme.heat(value)
property color idleColor: Theme.alpha(Theme.outline, 0.55)
property real lean: Theme.skew
readonly property int litCount: Math.round(Math.max(0, Math.min(100, value)) / 100 * segments)
implicitWidth: segments * segmentWidth + (segments - 1) * spacing
implicitHeight: 14
Row {
anchors.fill: parent
spacing: root.spacing
Repeater {
model: root.segments
Item {
required property int index
width: root.segmentWidth
height: root.height
Rectangle {
width: root.segmentWidth
height: root.height
color: parent.index < root.litCount ? root.activeColor : root.idleColor
// Peak segments glow, so a pegged CPU is visible peripherally.
opacity: parent.index < root.litCount
? (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 }
}
}
}
}
}
}
+191
View File
@@ -0,0 +1,191 @@
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "root:/config"
import "root:/services"
// One notification, used both in the history list and as a floating toast.
Item {
id: root
required property var notif
property bool showActions: true
readonly property color tone: root.notif
? Notifs.urgencyColor(root.notif.urgency)
: Theme.primary
signal dismissed()
implicitHeight: panel.height
P5Panel {
id: panel
width: parent.width
height: layout.implicitHeight + Theme.padM * 2
lean: 0.05
chop: 12
fill: Theme.surface
fillOpacity: 0.97
border: Theme.alpha(root.tone, hover.hovered ? 0.95 : 0.5)
slash: true
slashColor: root.tone
slashWidth: 4
halftone: root.notif?.urgency === NotificationUrgency.Critical
halftoneColor: Theme.accent
halftoneOpacity: 0.12
padding: Theme.padM
Column {
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 3
// App line
Row {
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Notifs.appIconFor(root.notif)
color: root.tone
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: (root.notif?.appName || "System").toUpperCase()
color: root.tone
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 2
renderType: Text.NativeRendering
}
}
Text {
width: parent.width
text: root.notif?.summary || ""
color: Theme.text
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.WordWrap
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Font.Black
font.italic: true
renderType: Text.NativeRendering
}
Text {
width: parent.width
visible: text !== ""
text: root.notif?.body || ""
color: Theme.subtext
elide: Text.ElideRight
maximumLineCount: 4
wrapMode: Text.WordWrap
textFormat: Text.StyledText
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
renderType: Text.NativeRendering
}
// Inline image, when the sender supplied one.
Image {
visible: source != "" && status === Image.Ready
source: root.notif?.image || ""
sourceSize.height: 90
fillMode: Image.PreserveAspectFit
asynchronous: true
}
Row {
visible: root.showActions && (root.notif?.actions?.length ?? 0) > 0
spacing: Theme.padS
topPadding: Theme.padS
Repeater {
model: root.notif?.actions ?? []
Item {
id: actionChip
required property var modelData
implicitWidth: actionLabel.implicitWidth + Theme.padM
implicitHeight: 22
Rectangle {
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)
}
}
Text {
id: actionLabel
anchors.centerIn: parent
text: actionChip.modelData.text
color: Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsMicro
font.italic: true
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
HoverHandler {
id: actionHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
actionChip.modelData.invoke();
root.dismissed();
}
}
}
}
}
}
// Close affordance.
Icon {
anchors.right: parent.right
anchors.top: parent.top
visible: hover.hovered
text: "󰅖"
color: closeHover.hovered ? Theme.accent : Theme.muted
font.pixelSize: Theme.fsBody
HoverHandler {
id: closeHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: root.dismissed()
}
}
}
HoverHandler {
id: hover
}
// Middle click dismisses, matching the old dunst muscle memory.
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.dismissed()
}
}
+106
View File
@@ -0,0 +1,106 @@
import QtQuick
import "root:/config"
// Chopped, leaning button. On hover it lunges toward the pointer and flips to
// crimson-on-white; on press it snaps flat.
Item {
id: root
property string text: ""
property string icon: ""
property color accent: Theme.primary
property color hoverFill: Theme.accent
property bool destructive: false
property real lean: Theme.skew
property real lunge: 6
property bool wide: false
signal clicked()
readonly property bool hovered: hover.hovered
readonly property bool pressed: tap.pressed
implicitWidth: wide ? 260 : (row.implicitWidth + panel.contentPad * 2)
implicitHeight: 44
P5Panel {
id: panel
anchors.fill: parent
lean: root.lean
padding: Theme.padS
fill: root.hovered ? (root.destructive ? root.hoverFill : Theme.text) : Theme.surface
fillOpacity: root.hovered ? 1.0 : 0.9
border: root.hovered ? "transparent" : Theme.alpha(root.accent, 0.55)
slash: !root.hovered
slashColor: root.destructive ? Theme.accent : root.accent
halftone: root.hovered
halftoneColor: root.destructive ? Theme.text : Theme.base
halftoneOpacity: 0.14
Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast }
}
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
Icon {
anchors.verticalCenter: parent.verticalCenter
visible: root.icon !== ""
text: root.icon
font.pixelSize: Theme.fsTitle
color: root.hovered
? (root.destructive ? Theme.text : Theme.base)
: root.accent
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.text
color: root.hovered
? (root.destructive ? Theme.text : Theme.base)
: Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 1.5
font.capitalization: Font.AllUppercase
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
}
}
transform: Translate {
x: root.pressed ? 0 : (root.hovered ? root.lunge : 0)
Behavior on x {
NumberAnimation {
duration: Theme.durBase
easing.type: Easing.OutBack
}
}
}
scale: root.pressed ? 0.97 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutExpo }
}
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
id: tap
onTapped: root.clicked()
}
}
+165
View File
@@ -0,0 +1,165 @@
import QtQuick
import QtQuick.Shapes
import QtQuick.Effects
import "root:/config"
import "root:/components"
// The base surface for everything in this shell: a leaning, corner-chopped slab
// with an optional halftone wash and a crimson slash along its leading edge.
Item {
id: root
property color fill: Theme.surface
property real fillOpacity: 0.94
property color border: Theme.outline
property real borderWidth: Theme.stroke
// Lean of the vertical edges, as a fraction of height. 0 = upright.
property real lean: Theme.skew
property bool leanLeft: false
// Corner chop size, and which corners get chopped (indices into the
// polygon, clockwise from the top-left). Default chops the two corners that
// read as "cut" against the lean.
property real chop: Theme.cut
property var chopCorners: [1, 3]
// Crimson slash down the leading edge — the Persona 5 tell.
property bool slash: false
property color slashColor: Theme.accent
property real slashWidth: 4
// Screen-printed dot wash.
property bool halftone: false
property color halftoneColor: Theme.primary
property real halftoneOpacity: 0.07
// Inner glow along the top edge, for panels that should feel backlit.
property bool sheen: false
readonly property real leanInset: height * Math.abs(lean)
readonly property var polygon: leanLeft
? Geom.parallelogramL(width, height, lean)
: Geom.parallelogram(width, height, lean)
// Child content lives inside `contentItem`, inset on both sides far enough
// to clear the slanted edges. Panels do not auto-size: callers set implicit
// sizes from their own layout (`row.implicitWidth + contentPad * 2`), which
// keeps the shape maths out of every binding loop.
default property alias content: contentHolder.data
readonly property alias contentItem: contentHolder
property real padding: Theme.padM
readonly property real contentPad: padding + leanInset
Shape {
id: body
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: false
ShapePath {
fillColor: Theme.alpha(root.fill, root.fillOpacity)
strokeColor: root.borderWidth > 0 ? root.border : "transparent"
strokeWidth: root.borderWidth
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
}
// Halftone wash, masked to the panel silhouette.
Loader {
anchors.fill: parent
active: root.halftone
asynchronous: true
sourceComponent: Item {
Shape {
id: maskShape
anchors.fill: parent
visible: false
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "white"
strokeColor: "transparent"
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
}
Image {
anchors.fill: parent
source: Theme.texHalftone
fillMode: Image.Tile
opacity: root.halftoneOpacity
smooth: false
layer.enabled: true
layer.effect: MultiEffect {
colorization: 1.0
colorizationColor: root.halftoneColor
maskEnabled: true
maskSource: maskShape
}
}
}
}
// Backlit top edge.
Loader {
anchors.fill: parent
active: root.sheen
sourceComponent: Shape {
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeColor: "transparent"
fillGradient: LinearGradient {
x1: 0; y1: 0; x2: 0; y2: root.height
GradientStop { position: 0.0; color: Theme.alpha(Theme.primary, 0.16) }
GradientStop { position: 0.55; color: "transparent" }
}
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
}
}
// Leading-edge slash.
Loader {
anchors.fill: parent
active: root.slash
sourceComponent: Shape {
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: root.slashColor
strokeColor: "transparent"
PathPolyline {
path: {
const off = root.height * root.lean;
const w = root.slashWidth;
return root.leanLeft
? Geom.closed([Qt.point(0, 0), Qt.point(w, 0),
Qt.point(off + w, root.height), Qt.point(off, root.height)])
: Geom.closed([Qt.point(off, 0), Qt.point(off + w, 0),
Qt.point(w, root.height), Qt.point(0, root.height)]);
}
}
}
}
}
Item {
id: contentHolder
anchors.fill: parent
anchors.leftMargin: root.contentPad
anchors.rightMargin: root.contentPad
anchors.topMargin: root.padding
anchors.bottomMargin: root.padding
}
}
+99
View File
@@ -0,0 +1,99 @@
import QtQuick
import "root:/config"
// Leaning slider with a chopped handle. Drag or click anywhere on the rail.
Item {
id: root
property real value: 0 // 0..1
property color accent: Theme.primary
property bool enabledControl: true
signal moved(real value)
implicitHeight: 18
implicitWidth: 200
readonly property real _lean: Theme.skew * height
// Rail
Rectangle {
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 {
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 }
}
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
// Handle
Rectangle {
id: handle
width: 6
height: hover.hovered || drag.active ? 18 : 14
color: root.enabledControl ? Theme.text : Theme.muted
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 }
}
Behavior on x {
enabled: !drag.active
NumberAnimation { duration: Theme.durFast }
}
}
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: event => root.moved(Math.max(0, Math.min(1, event.position.x / root.width)))
}
DragHandler {
id: drag
target: null
xAxis.enabled: true
yAxis.enabled: false
onCentroidChanged: {
if (!active) return;
root.moved(Math.max(0, Math.min(1, centroid.position.x / root.width)));
}
}
}
+135
View File
@@ -0,0 +1,135 @@
import QtQuick
import Quickshell
import "root:/config"
// Hover-driven panel that hangs off a bar item. Opens on a short dwell, stays
// open while the pointer is over either the trigger or the panel itself, and
// can be pinned with a click. Slides in from behind the bar with an overshoot.
PopupWindow {
id: root
required property Item anchorItem
property bool triggerHovered: false
property bool pinned: false
property int showDelay: 140
property int hideDelay: 200
property int fromEdge: Edges.Bottom
property real contentWidth: Theme.popoutWidth
property real contentHeight: 200
// Extra surface around the content so the entry overshoot and glow are not
// clipped by the window edge.
readonly property int bleed: 24
default property alias body: contentHolder.data
readonly property bool wantsOpen: triggerHovered || panelHover.hovered || pinned
signal dismissedFully()
anchor.item: anchorItem
anchor.edges: fromEdge
anchor.gravity: fromEdge
anchor.adjustment: PopupAdjustment.SlideX
anchor.margins.top: Theme.popoutGap
anchor.margins.bottom: Theme.popoutGap
implicitWidth: contentWidth + bleed * 2
implicitHeight: contentHeight + bleed * 2
color: "transparent"
visible: shown
property bool shown: false
onWantsOpenChanged: {
if (wantsOpen) {
hideTimer.stop();
showTimer.restart();
} else {
showTimer.stop();
hideTimer.restart();
}
}
Timer {
id: showTimer
interval: root.showDelay
onTriggered: root.open()
}
Timer {
id: hideTimer
interval: root.hideDelay
onTriggered: root.dismiss()
}
function open() {
exitAnim.stop();
shown = true;
enterAnim.restart();
}
function dismiss() {
if (!shown) return;
enterAnim.stop();
exitAnim.restart();
}
Item {
id: contentHolder
x: root.bleed
y: root.bleed
width: root.contentWidth
height: root.contentHeight
opacity: 0
transformOrigin: Item.Top
scale: 0.94
// The handler has to live on the content item, not the window: a
// HoverHandler parented to the PopupWindow itself latches on and the
// panel never learns the pointer left.
HoverHandler {
id: panelHover
}
}
ParallelAnimation {
id: enterAnim
NumberAnimation {
target: contentHolder; property: "opacity"
to: 1.0; duration: Theme.durBase; easing.type: Easing.OutExpo
}
NumberAnimation {
target: contentHolder; property: "scale"
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack
}
NumberAnimation {
target: contentHolder; property: "y"
from: root.bleed - 14; to: root.bleed
duration: Theme.durSlow; easing.type: Easing.OutBack
}
}
ParallelAnimation {
id: exitAnim
NumberAnimation {
target: contentHolder; property: "opacity"
to: 0.0; duration: Theme.durFast; easing.type: Easing.InQuad
}
NumberAnimation {
target: contentHolder; property: "scale"
to: 0.96; duration: Theme.durFast; easing.type: Easing.InQuad
}
NumberAnimation {
target: contentHolder; property: "y"
to: root.bleed - 8; duration: Theme.durFast; easing.type: Easing.InQuad
}
onFinished: {
root.shown = false;
root.pinned = false;
contentHolder.y = root.bleed;
root.dismissedFully();
}
}
}
+96
View File
@@ -0,0 +1,96 @@
import QtQuick
import "root:/config"
// Display text with the wallpaper's RGB split baked in: a crimson and a cyan
// ghost sitting a pixel off the white body. Drives most headings in the shell.
Item {
id: root
property string text: ""
property int pixelSize: Theme.fsLarge
property int weight: Theme.weightDisplay
property string family: Theme.fontDisplay
property bool italic: true
property color color: Theme.text
property real split: 1.6
property real splitOpacity: 0.85
property int elide: Text.ElideNone
property int horizontalAlignment: Text.AlignLeft
property real letterSpacing: 0.5
implicitWidth: body.implicitWidth
implicitHeight: body.implicitHeight
Text {
id: ghostRed
anchors.fill: parent
anchors.leftMargin: -root.split
anchors.topMargin: root.split * 0.4
text: root.text
color: Theme.splitRed
opacity: root.splitOpacity
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
}
Text {
id: ghostCyan
anchors.fill: parent
anchors.leftMargin: root.split
anchors.topMargin: -root.split * 0.4
text: root.text
color: Theme.splitCyan
opacity: root.splitOpacity
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
}
Text {
id: body
anchors.fill: parent
text: root.text
color: root.color
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
}
// Kick the ghosts outward briefly — used on track changes, mode flips, etc.
function glitch() {
glitchAnim.restart();
}
SequentialAnimation {
id: glitchAnim
NumberAnimation {
target: root; property: "split"
to: 5.5; duration: Theme.durFast
easing.type: Easing.OutExpo
}
NumberAnimation {
target: root; property: "split"
to: 1.6; duration: Theme.durSlow
easing.type: Easing.OutBack
}
}
}
+11
View File
@@ -0,0 +1,11 @@
import QtQuick
import QtQuick.Effects
// Layer effect that recolours whatever it is applied to — used to tint the
// white texture tiles without shipping a copy per colour.
MultiEffect {
property color tintColor: "white"
colorization: 1.0
colorizationColor: tintColor
}