Replaces the Persona x Mocha rice. NieR's menu language is kept whole — plain rectangles, hairline rules, letter-spaced upright capitals, a square marker on the current row, corner ticks round panels, and selection shown by inverting the row — but the colours come from ~/.wallpapers/space-1.jpg rather than from the game's beige: the grey-green void as the ground, the suit's sage grey as text, the planet's white as the one accent, and the hull's red indicator lights as the alarm. The Persona geometry is switched off rather than ripped out: Theme.skew and Theme.cut are 0, so P5Panel, Skew and Geom collapse to rectangles. Overshoot easing, italics and black weights are gone shell-wide. New, borrowed from caelestia's screen border: bar/Frame.qml draws the bar, the rail and a thin left/bottom band as one hull on the bottom layer, with the desktop as a rounded opening cut out of it. Frame also owns all four exclusive zones, so the bar and rail are placed from the screen edge. Hyprland moves to 1px borders lit from the planet white into grey, a soft wide shadow and gaps measured from the opening. Alacritty, fish and dunst get matching YoRHa palettes. Neovim, GTK/Qt and btop are still Mocha. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
296 lines
11 KiB
QML
296 lines
11 KiB
QML
import QtQuick
|
|
import QtQuick.Shapes
|
|
import QtQuick.Effects
|
|
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.
|
|
//
|
|
// 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.
|
|
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
|
|
|
|
// Tall panels (the right-hand rail) slant their horizontal edges instead,
|
|
// by a fraction of width, and inset their content top/bottom rather than
|
|
// left/right.
|
|
property bool vertical: 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]
|
|
|
|
// Accent bar down the leading edge.
|
|
property bool slash: false
|
|
property color slashColor: Theme.accent
|
|
property real slashWidth: 4
|
|
|
|
// Dot-grid wash.
|
|
property bool halftone: false
|
|
property color halftoneColor: Theme.primary
|
|
property real halftoneOpacity: 0.07
|
|
|
|
// Accepted and ignored. This used to paint a teal-to-transparent gradient
|
|
// down the top of every panel in the shell — the single biggest source of
|
|
// the murky blue cast. Fills are flat now. Kept as a no-op property so the
|
|
// call sites that still set it don't have to be chased down.
|
|
property bool sheen: false
|
|
|
|
// Optional print stack: an ink copy of the silhouette offset behind the
|
|
// fill, and an ink keyline outside the border. Both off by default — on a
|
|
// dark ground they only thicken the edge.
|
|
property bool drop: false
|
|
property real dropOffset: Theme.dropOffset
|
|
property color dropColor: Theme.ink
|
|
property bool keyline: false
|
|
property real keylineWidth: Theme.keylineWidth
|
|
|
|
// Corner ticks: a short L at each corner, just outside the border.
|
|
property bool ticks: false
|
|
property color tickColor: Theme.outline
|
|
property real tickLength: Theme.tick
|
|
|
|
readonly property real leanInset:
|
|
(vertical ? width : height) * Math.abs(lean)
|
|
|
|
readonly property var polygon: vertical
|
|
? (leanLeft ? Geom.parallelogramVL(width, height, lean)
|
|
: Geom.parallelogramV(width, height, lean))
|
|
: (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
|
|
|
|
// The panel outline as a mask texture. Anything that is painted from the
|
|
// raw polygon rather than from the chamfered one has to be clipped to this,
|
|
// or it escapes at the chopped corners.
|
|
Shape {
|
|
id: silhouette
|
|
anchors.fill: parent
|
|
visible: false
|
|
layer.enabled: true
|
|
layer.smooth: true
|
|
preferredRendererType: Shape.CurveRenderer
|
|
|
|
ShapePath {
|
|
fillColor: "white"
|
|
strokeColor: "transparent"
|
|
joinStyle: ShapePath.MiterJoin
|
|
|
|
PathPolyline {
|
|
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Offset black copy of the silhouette, behind everything.
|
|
Loader {
|
|
anchors.fill: parent
|
|
active: root.drop && root.dropOffset > 0
|
|
sourceComponent: Shape {
|
|
preferredRendererType: Shape.CurveRenderer
|
|
asynchronous: false
|
|
|
|
transform: Translate {
|
|
x: root.dropOffset * (root.leanLeft ? -1 : 1)
|
|
y: root.dropOffset
|
|
}
|
|
|
|
ShapePath {
|
|
fillColor: root.dropColor
|
|
strokeColor: "transparent"
|
|
joinStyle: ShapePath.MiterJoin
|
|
|
|
PathPolyline {
|
|
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Shape {
|
|
id: body
|
|
anchors.fill: parent
|
|
preferredRendererType: Shape.CurveRenderer
|
|
asynchronous: false
|
|
|
|
// Fill, carrying the black keyline. The stroke is centred on the path,
|
|
// so half of it sits outside the silhouette — which is exactly the
|
|
// heavy outer contour the style wants.
|
|
ShapePath {
|
|
fillColor: Theme.alpha(root.fill, root.fillOpacity)
|
|
strokeColor: root.keyline ? root.dropColor : "transparent"
|
|
strokeWidth: root.keyline
|
|
? root.borderWidth + root.keylineWidth * 2
|
|
: 0
|
|
joinStyle: ShapePath.MiterJoin
|
|
capStyle: ShapePath.FlatCap
|
|
|
|
PathPolyline {
|
|
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
|
|
}
|
|
}
|
|
|
|
// Coloured border, drawn on top of the keyline so it reads as an inlay.
|
|
ShapePath {
|
|
fillColor: "transparent"
|
|
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: Image {
|
|
anchors.fill: parent
|
|
source: Theme.texDots
|
|
fillMode: Image.Tile
|
|
opacity: root.halftoneOpacity
|
|
smooth: false
|
|
layer.enabled: true
|
|
layer.effect: MultiEffect {
|
|
colorization: 1.0
|
|
colorizationColor: root.halftoneColor
|
|
maskEnabled: true
|
|
maskSource: silhouette
|
|
}
|
|
}
|
|
}
|
|
|
|
// Leading-edge slash.
|
|
//
|
|
// Its polygon is built from the panel's raw corners, so on a panel with a
|
|
// chopped corner the slash used to run straight through the cut and out the
|
|
// far side — an accent wedge sitting a few pixels outside the silhouette,
|
|
// at an angle that matched nothing. It is masked to the outline now, so the
|
|
// slash ends exactly where the chamfer does.
|
|
Loader {
|
|
anchors.fill: parent
|
|
active: root.slash
|
|
sourceComponent: Item {
|
|
Shape {
|
|
id: slashShape
|
|
anchors.fill: parent
|
|
visible: false
|
|
layer.enabled: true
|
|
layer.smooth: true
|
|
preferredRendererType: Shape.CurveRenderer
|
|
|
|
ShapePath {
|
|
fillColor: root.slashColor
|
|
strokeColor: "transparent"
|
|
|
|
PathPolyline {
|
|
path: {
|
|
const t = root.slashWidth;
|
|
|
|
// On a rail slab the slash runs along the slanted
|
|
// top edge instead of down a side.
|
|
if (root.vertical) {
|
|
const voff = root.width * root.lean;
|
|
return root.leanLeft
|
|
? Geom.closed([Qt.point(0, 0), Qt.point(root.width, voff),
|
|
Qt.point(root.width, voff + t), Qt.point(0, t)])
|
|
: Geom.closed([Qt.point(0, voff), Qt.point(root.width, 0),
|
|
Qt.point(root.width, t), Qt.point(0, voff + t)]);
|
|
}
|
|
|
|
const off = root.height * root.lean;
|
|
return root.leanLeft
|
|
? Geom.closed([Qt.point(0, 0), Qt.point(t, 0),
|
|
Qt.point(off + t, root.height), Qt.point(off, root.height)])
|
|
: Geom.closed([Qt.point(off, 0), Qt.point(off + t, 0),
|
|
Qt.point(t, root.height), Qt.point(0, root.height)]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MultiEffect {
|
|
anchors.fill: parent
|
|
source: slashShape
|
|
maskEnabled: true
|
|
maskSource: silhouette
|
|
}
|
|
}
|
|
}
|
|
|
|
// Corner ticks. Two hairlines per corner, sitting 3px outside the panel so
|
|
// they read as a frame around it rather than as part of its border.
|
|
Loader {
|
|
anchors.fill: parent
|
|
anchors.margins: -3
|
|
active: root.ticks
|
|
sourceComponent: Item {
|
|
Repeater {
|
|
model: 4
|
|
|
|
Item {
|
|
required property int index
|
|
readonly property bool atRight: index === 1 || index === 2
|
|
readonly property bool atBottom: index >= 2
|
|
|
|
x: atRight ? parent.width - width : 0
|
|
y: atBottom ? parent.height - height : 0
|
|
width: root.tickLength
|
|
height: root.tickLength
|
|
|
|
Rectangle {
|
|
y: parent.atBottom ? parent.height - 1 : 0
|
|
width: parent.width
|
|
height: 1
|
|
color: root.tickColor
|
|
}
|
|
|
|
Rectangle {
|
|
x: parent.atRight ? parent.width - 1 : 0
|
|
width: 1
|
|
height: parent.height
|
|
color: root.tickColor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: contentHolder
|
|
anchors.fill: parent
|
|
anchors.leftMargin: root.vertical ? root.padding : root.contentPad
|
|
anchors.rightMargin: root.vertical ? root.padding : root.contentPad
|
|
anchors.topMargin: root.vertical ? root.contentPad : root.padding
|
|
anchors.bottomMargin: root.vertical ? root.contentPad : root.padding
|
|
}
|
|
}
|