theme: soften the YoRHa shapes; SUPER+D opens the dashboard

The square markers, block meters and hard-cornered fills read as boxy
next to the porthole. The desktop is now soft throughout: the porthole
is the shape everything is built around and every smaller surface echoes
it.

- Theme gains radiusS / radiusM / radiusL. radiusL is frameRadius - padS,
  so anything inset padS inside one of the frame's curves is concentric
  with it.
- P5Panel and Skew draw rounded rectangles (Geom.roundedRect) whenever
  they neither lean nor are chopped, which is always now; the polygon
  path is kept for shapes brought back on purpose.
- Markers are dots; workspace pips, toggles, transport and media chips,
  notification actions and today's date are pills or discs; the slider
  handle is round. Meters are one continuous rounded line that glides to
  its level instead of a row of segments.
- Album art is rounded with ClippingRectangle in the bar, the media
  popout and the dashboard.
- Free-standing panels (launcher, ask) drop the corner brackets and take
  the frame radius.
- Fix: a hovered notification's wash was a full-bleed square, so in the
  drawer's bottom corner it ran past the drawer's curve. It is inset by
  padS/2 and rounded by radiusL, which keeps it inside the curve.

Hyprland: the discord special workspace and its window rule are gone,
SUPER+S stays the only scratchpad, and SUPER+D now opens the dashboard
(it was on SUPER+I).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nvrlandClaude Opus 5.5 committed 2026-10-04 20:56:43 +02:00
1 parent d6de04eaaf
commit a8c19961e2
24 files changed
+188 -110

No files matched your search

+1
View File
@@ -18,4 +18,5 @@ P5Panel {
slashColor: tone
slashWidth: 2
padding: Theme.padS
radius: Theme.radiusM
}
+23 -3
View File
@@ -3,9 +3,10 @@ pragma Singleton
import QtQuick
import Quickshell
// Polygon maths for panel shapes. Under the YoRHa theme nothing leans and
// nothing is chopped (Theme.skew and Theme.cut are 0), so these collapse to
// plain rectangles; they are kept so a shape can be brought back on purpose.
// Shape maths for panels. Under the YoRHa theme nothing leans and nothing is
// chopped (Theme.skew and Theme.cut are 0); surfaces are rounded rectangles
// (roundedRect). The polygon helpers are kept so a shape can be brought back
// on purpose.
// Originally the Persona 5 panel language: everything leans, and corners
// get chopped rather than rounded.
Singleton {
@@ -89,6 +90,25 @@ Singleton {
return root.closed(out);
}
// A polygon as an SVG path string, for PathSvg.
function svg(pts: var): string {
if (pts.length === 0) return "";
let d = `M ${pts[0].x} ${pts[0].y}`;
for (let i = 1; i < pts.length; i++) d += ` L ${pts[i].x} ${pts[i].y}`;
return d + " Z";
}
// A w x h rectangle with every corner rounded by `r`, clamped so a short
// piece becomes a pill rather than folding over itself.
function roundedRect(w: real, h: real, r: real): string {
const k = Math.max(0, Math.min(r, w / 2, h / 2));
if (k <= 0) return root.svg([Qt.point(0, 0), Qt.point(w, 0), Qt.point(w, h), Qt.point(0, h)]);
return `M ${k} 0 H ${w - k} A ${k} ${k} 0 0 1 ${w} ${k}`
+ ` V ${h - k} A ${k} ${k} 0 0 1 ${w - k} ${h}`
+ ` H ${k} A ${k} ${k} 0 0 1 0 ${h - k}`
+ ` V ${k} A ${k} ${k} 0 0 1 ${k} 0 Z`;
}
function closed(pts: var): var {
if (pts.length === 0) return pts;
const out = pts.slice();
+3 -1
View File
@@ -7,7 +7,7 @@ import "root:/config"
// came out as a faint milky film. This is the one row they share now.
//
// Rest is bare. Hover lifts the row onto the surface tone. The current item —
// connected, in use — carries the NieR square marker at its leading edge and
// connected, in use — carries a dot at its leading edge and
// an accent glyph, and its label goes to full weight.
Item {
id: root
@@ -30,6 +30,7 @@ Item {
Rectangle {
anchors.fill: parent
radius: Theme.radiusM
color: root.hovered ? Theme.treatmentHoverFill : "transparent"
Behavior on color {
@@ -43,6 +44,7 @@ Item {
anchors.leftMargin: 2
width: root.current ? Theme.marker : 0
height: width
radius: width / 2
color: Theme.accent
Behavior on width {
+26 -31
View File
@@ -2,8 +2,12 @@ import QtQuick
import "root:/config"
import "root:/components"
// A segmented load meter — upright ticks that light up left to right. Reads as
// a gauge at a glance and keeps the panel language consistent.
// A load meter: a soft rounded track with the level filling it from the left.
// It used to be a row of separate segments; one continuous line reads calmer,
// and a level that glides reads as a quantity rather than as a count.
//
// `segments`, `segmentWidth` and `spacing` are accepted and ignored, so the
// call sites that still size a segmented meter do not have to be chased down.
Item {
id: root
@@ -12,42 +16,33 @@ Item {
property real segmentWidth: 4
property real spacing: 3
property color activeColor: Theme.heat(value)
property color idleColor: Theme.alpha(Theme.outline, 0.55)
property color idleColor: Theme.alpha(Theme.outline, 0.35)
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
implicitHeight: 4
Row {
readonly property real fraction: Math.max(0, Math.min(100, root.value)) / 100
Rectangle {
anchors.fill: parent
spacing: root.spacing
radius: height / 2
color: root.idleColor
}
Repeater {
model: root.segments
Rectangle {
height: parent.height
// Never narrower than its own height, so a near-zero level is a dot
// rather than a sliver.
width: root.fraction > 0 ? Math.max(height, parent.width * root.fraction) : 0
radius: height / 2
color: root.activeColor
Item {
required property int index
width: root.segmentWidth
height: root.height
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.
opacity: parent.index < root.litCount
? (parent.index >= root.segments - 2 ? 1.0 : 0.9)
: 1.0
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
Behavior on width {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutCubic }
}
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
+21 -11
View File
@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import "root:/config"
import "root:/components"
@@ -7,14 +8,14 @@ import "root:/services"
// One notification, used both in the history list and in the toast drawer.
//
// Laid out like an entry in NieR's message log rather than as a card: a square
// marker and the sender's name running into a hairline, the summary, the body,
// and hairline buttons for any actions. There is no panel of its own — both
// places it appears are already hull surfaces — so rest is bare, hover lifts
// the row onto the surface tone, and entries are divided by the hairline in
// their own header.
// Laid out like an entry in NieR's message log rather than as a card: a dot
// and the sender's name running into a hairline, the summary, the body, and
// pill buttons for any actions. There is no panel of its own — both places it
// appears are already hull surfaces — so rest is bare, hover lifts the entry
// onto a soft rounded wash, and entries are divided by the hairline in their
// own header.
//
// Urgency is the marker: the light for normal, grey for low, red for critical.
// Urgency is the dot: the light for normal, grey for low, red for critical.
// A critical entry also takes the red into its sender line, and nothing else.
Item {
id: root
@@ -38,8 +39,14 @@ Item {
implicitHeight: layout.implicitHeight + Theme.padM * 2
// Hover wash. Inset by padS and rounded by radiusL, which is the frame's
// radius less that inset: an entry in the bottom corner of the drawer gets
// a corner concentric with the drawer's own, instead of a square one that
// ran out past the drawer's curve.
Rectangle {
anchors.fill: parent
anchors.margins: Theme.padS / 2
radius: Theme.radiusL
color: root.hovered ? Theme.treatmentHoverFill : "transparent"
Behavior on color {
@@ -66,6 +73,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
width: Theme.marker
height: Theme.marker
radius: width / 2
color: root.tone
}
@@ -162,12 +170,13 @@ Item {
renderType: Text.NativeRendering
}
// Inline image, when the sender supplied one, in a hairline frame.
Rectangle {
// Inline image, when the sender supplied one, rounded in a hairline frame.
ClippingRectangle {
x: Theme.marker + Theme.padS
visible: inline.source != "" && inline.status === Image.Ready
width: inline.width + 2
height: inline.height + 2
radius: Theme.radiusM
color: "transparent"
border.width: 1
border.color: Theme.overlay
@@ -192,8 +201,7 @@ Item {
Repeater {
model: root.notif?.actions ?? []
// A hairline button with a marker, the P5Button pattern at a
// smaller size. Hover lifts and lights it; it never inverts.
// A hairline pill. Hover lifts and lights it; it never inverts.
Item {
id: actionChip
required property var modelData
@@ -203,6 +211,7 @@ Item {
Rectangle {
anchors.fill: parent
radius: height / 2
color: actionHover.hovered ? Theme.treatmentHoverFill : "transparent"
border.width: 1
border.color: actionHover.hovered ? Theme.treatmentHoverMark : Theme.overlay
@@ -221,6 +230,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
width: 4
height: 4
radius: 2
color: actionLabel.color
opacity: actionHover.hovered ? 1 : 0.4
}
+1
View File
@@ -90,6 +90,7 @@ FocusScope {
anchors.verticalCenter: parent.verticalCenter
width: Theme.marker
height: Theme.marker
radius: width / 2
color: label.color
opacity: root.highlighted ? 1.0 : 0.35
+19 -11
View File
@@ -5,9 +5,8 @@ import "root:/config"
import "root:/components"
// The base surface for everything in this shell. Under the YoRHa theme it is a
// plain rectangle with a hairline border, an optional dot wash, an optional
// accent bar down its leading edge, and optional corner ticks — the brackets
// NieR frames its panels with.
// rounded rectangle with a hairline border, an optional dot wash, an optional
// accent bar down its leading edge, and optional corner ticks.
//
// The lean and chop knobs still exist and still work, so a caller can bring a
// shape back on purpose, but Theme.skew and Theme.cut are both zero.
@@ -34,6 +33,10 @@ Item {
property real chop: Theme.cut
property var chopCorners: [1, 3]
// Corner radius. Used whenever the panel neither leans nor is chopped,
// which under the YoRHa theme is always.
property real radius: Theme.radiusM
// Accent bar down the leading edge.
property bool slash: false
property color slashColor: Theme.accent
@@ -67,6 +70,11 @@ Item {
readonly property real leanInset:
(vertical ? width : height) * Math.abs(lean)
// The outline every layer below is drawn from.
readonly property string outline: root.lean === 0 && root.chop <= 0
? Geom.roundedRect(root.width, root.height, root.radius)
: Geom.svg(Geom.chamfer(root.polygon, root.chop, root.chopCorners).slice(0, -1))
readonly property var polygon: vertical
? (leanLeft ? Geom.parallelogramVL(width, height, lean)
: Geom.parallelogramV(width, height, lean))
@@ -99,8 +107,8 @@ Item {
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
PathSvg {
path: root.outline
}
}
}
@@ -123,8 +131,8 @@ Item {
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
PathSvg {
path: root.outline
}
}
}
@@ -148,8 +156,8 @@ Item {
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
PathSvg {
path: root.outline
}
}
@@ -161,8 +169,8 @@ Item {
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
PathSvg {
path: root.outline
}
}
}
+2 -2
View File
@@ -2,8 +2,7 @@ import QtQuick
import "root:/config"
import "root:/components"
// Hairline slider with a square handle, the NieR settings-menu slider. Drag or
// click anywhere on the rail.
// Hairline slider with a round handle. Drag or click anywhere on the rail.
Item {
id: root
@@ -68,6 +67,7 @@ Item {
Skew {
id: handle
width: height
radius: height / 2
height: root.activeFocus || hover.hovered || drag.active ? 12 : 10
color: root.enabled ? Theme.text : Theme.treatmentDisabledText
anchors.verticalCenter: parent.verticalCenter
+14 -6
View File
@@ -3,7 +3,8 @@ import QtQuick.Shapes
import "root:/config"
import "root:/components"
// A leaning block — the small-scale counterpart to P5Panel.
// A small block — the small-scale counterpart to P5Panel. Upright and rounded
// under the YoRHa theme; it still leans if given a lean.
//
// 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
@@ -31,6 +32,11 @@ Shape {
// rail slab.
property bool vertical: false
// Corner radius for an upright piece, clamped to a pill. Small by default,
// so a meter segment or a hover wash reads soft rather than as a block;
// a hairline stays a hairline.
property real radius: Theme.radiusS
// NOTE ON SIZING. The lean shifts one edge by `lean * height` (or by
// `lean * width` when vertical), and the piece that actually gets painted is
// what is left: a Skew declared 1px wide and 20px tall does not draw a 1px
@@ -55,14 +61,16 @@ Shape {
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
PathSvg {
path: {
const w = root.width, h = root.height, l = root.lean;
if (l === 0)
return Geom.roundedRect(w, h, root.radius);
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));
return Geom.svg(root.leanLeft ? Geom.parallelogramVL(w, h, l)
: Geom.parallelogramV(w, h, l));
return Geom.svg(root.leanLeft ? Geom.parallelogramL(w, h, l)
: Geom.parallelogram(w, h, l));
}
}
}