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>
304 lines
11 KiB
QML
304 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
|
|
// 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.
|
|
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]
|
|
|
|
// 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
|
|
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)
|
|
|
|
// 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))
|
|
: (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
|
|
|
|
PathSvg {
|
|
path: root.outline
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
|
|
PathSvg {
|
|
path: root.outline
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
PathSvg {
|
|
path: root.outline
|
|
}
|
|
}
|
|
|
|
// 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
|
|
|
|
PathSvg {
|
|
path: root.outline
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|