feat(quickshell): give the prompt its own panel, ESC handling and placement
PolkitPanel replaces BarPanel as the dialog's chrome. Deliberately not a
subclass or a fork: most of BarPanel is density machinery — summary slot,
animated height, state pair, transitions — that a modal never uses, and
inheriting it would tie the dialog's look to a component whose real job is the
rail, so every restyle here would have to be justified against the panels up
there. It keeps the shell's silhouette (cut corners with detached accent caps,
the accent rules, the header strip) and drops the rail's tick decoration.
ESC closes the rail. The bar had no keyboard focus at all, so this adds it,
gated by the shell rather than left to the compositor to arbitrate between two
exclusive surfaces — that resolves by stacking and would invert silently the
day the layers change:
grabsKeyboard: shell.expanded && !polkit.prompting
so ESC dismisses the prompt while one is open and closes the rail afterwards.
Verified by instrumenting the handoff: expanded -> true, prompt open -> false,
prompt dismissed -> true, collapsed -> false. Note the rail now takes EXCLUSIVE
keyboard focus while expanded, which is the cost of answering a keypress the
user has not aimed at anything.
The dialog sits a third of the way down rather than centred, panel centre on
the third so it grows symmetrically as the message wraps, floored at a margin
so a tall prompt on a short output cannot be pushed off the top.
Also carries the backdrop tuning: dim 0.75 -> 0.65, gridOpacity 0.15 -> 0.10,
crossOpacity 0.45 -> 0.15, now that the scrim is used by the prompt as well as
the rail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GAq2kKCLazZmrKvkd3akud
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.HyprChrome.Theme
|
||||
|
||||
// Panel chrome for the authentication dialog.
|
||||
//
|
||||
// Deliberately NOT BarPanel. The rail's panel exists to carry two renderings of
|
||||
// the same data and cross-fade between them as the rail changes density, and
|
||||
// almost all of its size is that machinery: the summary slot, the animated
|
||||
// height, the state pair, the transitions. A modal has exactly one density and
|
||||
// never collapses, so inheriting all of that would mean carrying dead weight
|
||||
// and, worse, tying the dialog's look to a component whose real job is the rail
|
||||
// — every restyle here would have to be justified against the panels up there.
|
||||
//
|
||||
// What it does keep is the silhouette, because that is the shell's visual
|
||||
// signature rather than the rail's: two cut corners (top-right, bottom-left)
|
||||
// with detached accent caps outside them, an accent rule under the header slug,
|
||||
// its mirror at the lower right, and a header strip of slug / title / meta.
|
||||
//
|
||||
// This is the file to edit to restyle the prompt. Nothing else reads it.
|
||||
Item {
|
||||
id: panel
|
||||
|
||||
property string panelId: ""
|
||||
property string title: ""
|
||||
property string meta: ""
|
||||
|
||||
property int chamfer: 16
|
||||
property int padding: 14
|
||||
property int outlineWidth: 1
|
||||
property int accentLineThickness: 3
|
||||
|
||||
readonly property int headerHeight: 30
|
||||
|
||||
// Gap between the header rule and the body.
|
||||
property int headerGap: 10
|
||||
readonly property int headerPadding: 10
|
||||
|
||||
// Detached corner caps: the corner each chamfer removed, put back outside
|
||||
// the panel as an accent triangle whose hypotenuse faces the cut. capGap is
|
||||
// the perpendicular distance from the cut, so the per-axis shift is it over
|
||||
// root 2 — the cap moves along the cut's normal, not along an axis.
|
||||
property real capGap: 4
|
||||
readonly property real capOffset: panel.capGap / Math.SQRT2
|
||||
|
||||
// Never let the two cuts cross, which would turn the outline inside out on
|
||||
// a panel shorter than twice the chamfer.
|
||||
readonly property real activeChamfer: Math.max(2, Math.min(panel.chamfer, panel.height / 2 - 1))
|
||||
|
||||
// The accent rule under the slug is sized to the slug, not to the panel.
|
||||
readonly property real accentLineWidth: Math.min(panel.width, slugChip.width + panel.headerPadding * 2)
|
||||
|
||||
default property alias content: body.data
|
||||
|
||||
implicitHeight: Math.round(body.y + body.height + panel.padding)
|
||||
|
||||
Shape {
|
||||
id: panelShape
|
||||
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
// Outline: square except for the two cut corners.
|
||||
ShapePath {
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.hair
|
||||
strokeWidth: panel.outlineWidth
|
||||
|
||||
startX: 0; startY: 0
|
||||
PathLine { x: panelShape.width - panel.activeChamfer; y: 0 }
|
||||
PathLine { x: panelShape.width; y: panel.activeChamfer }
|
||||
PathLine { x: panelShape.width; y: panelShape.height }
|
||||
PathLine { x: panel.activeChamfer; y: panelShape.height }
|
||||
PathLine { x: 0; y: panelShape.height - panel.activeChamfer }
|
||||
PathLine { x: 0; y: 0 }
|
||||
}
|
||||
|
||||
// Cap on the top-right cut.
|
||||
ShapePath {
|
||||
fillColor: Theme.accent
|
||||
strokeWidth: 0
|
||||
|
||||
startX: panelShape.width - panel.activeChamfer + panel.capOffset
|
||||
startY: -panel.capOffset
|
||||
PathLine { x: panelShape.width + panel.capOffset; y: panel.activeChamfer - panel.capOffset }
|
||||
PathLine { x: panelShape.width + panel.capOffset; y: -panel.capOffset }
|
||||
PathLine { x: panelShape.width - panel.activeChamfer + panel.capOffset; y: -panel.capOffset }
|
||||
}
|
||||
|
||||
// Cap on the bottom-left cut, the same triangle mirrored.
|
||||
ShapePath {
|
||||
fillColor: Theme.accent
|
||||
strokeWidth: 0
|
||||
|
||||
startX: panel.activeChamfer - panel.capOffset
|
||||
startY: panelShape.height + panel.capOffset
|
||||
PathLine { x: -panel.capOffset; y: panelShape.height - panel.activeChamfer + panel.capOffset }
|
||||
PathLine { x: -panel.capOffset; y: panelShape.height + panel.capOffset }
|
||||
PathLine { x: panel.activeChamfer - panel.capOffset; y: panelShape.height + panel.capOffset }
|
||||
}
|
||||
|
||||
// Accent rule under the slug.
|
||||
ShapePath {
|
||||
fillColor: Theme.accent
|
||||
strokeWidth: 0
|
||||
|
||||
startX: 0; startY: 0
|
||||
PathLine { x: panel.accentLineWidth; y: 0 }
|
||||
PathLine { x: panel.accentLineWidth; y: panel.accentLineThickness }
|
||||
PathLine { x: 0; y: panel.accentLineThickness }
|
||||
PathLine { x: 0; y: 0 }
|
||||
}
|
||||
|
||||
// Its mirror at the lower right.
|
||||
ShapePath {
|
||||
fillColor: Theme.accent
|
||||
strokeWidth: 0
|
||||
|
||||
startX: panelShape.width; startY: panelShape.height
|
||||
PathLine { x: panelShape.width - panel.accentLineWidth; y: panelShape.height }
|
||||
PathLine { x: panelShape.width - panel.accentLineWidth; y: panelShape.height - panel.accentLineThickness }
|
||||
PathLine { x: panelShape.width; y: panelShape.height - panel.accentLineThickness }
|
||||
PathLine { x: panelShape.width; y: panelShape.height }
|
||||
}
|
||||
}
|
||||
|
||||
// Header: slug chip, title, and the meta text pinned right.
|
||||
Row {
|
||||
id: headerRow
|
||||
|
||||
x: panel.headerPadding
|
||||
y: Math.round((panel.headerHeight - height) / 2)
|
||||
spacing: panel.headerPadding + 6
|
||||
|
||||
Rectangle {
|
||||
id: slugChip
|
||||
|
||||
width: slugText.implicitWidth + 8
|
||||
height: slugText.implicitHeight + 4
|
||||
color: Theme.accent
|
||||
|
||||
Text {
|
||||
id: slugText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: panel.panelId
|
||||
color: Theme.surface
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: slugChip.verticalCenter
|
||||
text: panel.title
|
||||
color: Theme.text
|
||||
font.family: Theme.displayFont
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.1
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: panel.headerPadding + 2
|
||||
y: Math.round((panel.headerHeight - implicitHeight) / 2)
|
||||
visible: panel.meta.length > 0
|
||||
text: panel.meta
|
||||
color: Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 8
|
||||
font.letterSpacing: 0.7
|
||||
horizontalAlignment: Text.AlignRight
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Header rule.
|
||||
Rectangle {
|
||||
x: 1
|
||||
y: panel.headerHeight
|
||||
width: parent.width - 2
|
||||
height: 1
|
||||
color: Theme.text
|
||||
opacity: 0.12
|
||||
}
|
||||
|
||||
// Body. Measured by childrenRect, so a child must carry its own size and
|
||||
// must NOT anchor to this slot.
|
||||
Item {
|
||||
id: body
|
||||
|
||||
x: panel.padding
|
||||
y: panel.headerHeight + panel.headerGap
|
||||
width: Math.max(0, panel.width - panel.padding * 2)
|
||||
height: childrenRect.height
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user