quickshell: screen frame with rounded cutout; flush-right dropdowns merge into it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-06-11 20:10:37 +01:00
parent 0878cba10d
commit a5feef766d

View file

@ -91,6 +91,8 @@ in
readonly property string fontFamily: "${monoFont}" readonly property string fontFamily: "${monoFont}"
// Matches hyprland general.border_size (col.inactive_border = base03) // Matches hyprland general.border_size (col.inactive_border = base03)
readonly property int borderWidth: 2 readonly property int borderWidth: 2
// Screen frame band; sits inside hyprland's gaps_out (12)
readonly property int frameWidth: 6
} }
''; '';
}; };
@ -457,6 +459,75 @@ in
color: Theme.barBg color: Theme.barBg
} }
// Screen frame: a bar-coloured band around the monitor.
// Fill is one donut path (screen rect minus rounded inner
// cutout); the inner border is one continuous open path from
// the top-left corner the long way around to the bottom-right
// corner. The right column's border is separate so it can
// open up where a flush-right dropdown merges into it.
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.barBg
strokeWidth: -1
fillRule: ShapePath.OddEvenFill
startX: 0; startY: 30
PathLine { x: bar.width; y: 30 }
PathLine { x: bar.width; y: bar.height }
PathLine { x: 0; y: bar.height }
PathLine { x: 0; y: 30 }
PathMove { x: Theme.frameWidth + 8; y: 30 }
PathLine { x: bar.width - Theme.frameWidth - 8; y: 30 }
PathArc { x: bar.width - Theme.frameWidth; y: 38; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
PathLine { x: bar.width - Theme.frameWidth; y: bar.height - Theme.frameWidth - 8 }
PathArc { x: bar.width - Theme.frameWidth - 8; y: bar.height - Theme.frameWidth; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
PathLine { x: Theme.frameWidth + 8; y: bar.height - Theme.frameWidth }
PathArc { x: Theme.frameWidth; y: bar.height - Theme.frameWidth - 8; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
PathLine { x: Theme.frameWidth; y: 38 }
PathArc { x: Theme.frameWidth + 8; y: 30; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
}
ShapePath {
fillColor: "transparent"
strokeColor: Theme.base03
strokeWidth: Theme.borderWidth
capStyle: ShapePath.FlatCap
startX: Theme.frameWidth + 8; startY: 30
PathArc { x: Theme.frameWidth; y: 38; radiusX: 8; radiusY: 8; direction: PathArc.Counterclockwise }
PathLine { x: Theme.frameWidth; y: bar.height - Theme.frameWidth - 8 }
PathArc { x: Theme.frameWidth + 8; y: bar.height - Theme.frameWidth; radiusX: 8; radiusY: 8; direction: PathArc.Counterclockwise }
PathLine { x: bar.width - Theme.frameWidth - 8; y: bar.height - Theme.frameWidth }
PathArc { x: bar.width - Theme.frameWidth; y: bar.height - Theme.frameWidth - 8; radiusX: 8; radiusY: 8; direction: PathArc.Counterclockwise }
}
}
// Frame right-column inner border starts below a flush-right
// dropdown's bottom curve and follows the morph.
Rectangle {
x: bar.width - Theme.frameWidth - Theme.borderWidth / 2
y: chrome.visible && chrome.flushRight ? 30 + chrome.height + 8 : 38
width: Theme.borderWidth
height: Math.max(0, bar.height - Theme.frameWidth - 8 - y)
color: Theme.base03
}
// Frame top-right inner corner hidden while a flush-right
// dropdown is merged into the column there.
Shape {
visible: !(chrome.visible && chrome.flushRight)
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: Theme.base03
strokeWidth: Theme.borderWidth
capStyle: ShapePath.FlatCap
startX: bar.width - Theme.frameWidth - 8; startY: 30
PathArc { x: bar.width - Theme.frameWidth; y: 38; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
}
}
// The "gap source" for the bar border the morphing chrome // The "gap source" for the bar border the morphing chrome
// panel takes priority, then the toast. Tracking the animated // panel takes priority, then the toast. Tracking the animated
// chrome means the border gap follows the morph. // chrome means the border gap follows the morph.
@ -475,8 +546,9 @@ in
// y=30 so it runs into the panel's edge-centered border stroke. // y=30 so it runs into the panel's edge-centered border stroke.
Rectangle { Rectangle {
id: barBorderLeft id: barBorderLeft
x: 0; y: 30 - Theme.borderWidth / 2 x: Theme.frameWidth + 8
width: bar.hasGap ? bar.gapLeft : bar.width y: 30 - Theme.borderWidth / 2
width: Math.max(0, (bar.hasGap ? bar.gapLeft : bar.width - Theme.frameWidth - 8) - x)
height: Theme.borderWidth height: Theme.borderWidth
color: Theme.base03 color: Theme.base03
} }
@ -487,7 +559,7 @@ in
visible: bar.hasGap visible: bar.hasGap
x: bar.gapRight x: bar.gapRight
y: 30 - Theme.borderWidth / 2 y: 30 - Theme.borderWidth / 2
width: bar.width - x width: Math.max(0, bar.width - Theme.frameWidth - 8 - x)
height: Theme.borderWidth height: Theme.borderWidth
color: Theme.base03 color: Theme.base03
} }
@ -998,7 +1070,9 @@ in
property real fullWidth: 200 property real fullWidth: 200
property real fullHeight: 200 property real fullHeight: 200
property int autoCloseMs: 1500 property int autoCloseMs: 1500
property bool alignRight: false // legacy; all dropdowns now center on their widget // Flush-right dropdowns merge into the screen frame's
// right column instead of centering on their widget.
property bool alignRight: false
property real dropdownHeight: open ? fullHeight : 0 property real dropdownHeight: open ? fullHeight : 0
default property alias content: dropdownContent.data default property alias content: dropdownContent.data
@ -1024,9 +1098,11 @@ in
_autoClose.restart(); _autoClose.restart();
} }
x: Math.round(Math.min(bar.width - width, Math.max(0, dropdownX - width / 2))) x: alignRight
? bar.width - Theme.frameWidth - width
: Math.round(Math.min(bar.width - Theme.frameWidth - width, Math.max(Theme.frameWidth, dropdownX - width / 2)))
y: 30 y: 30
width: fullWidth + 16 width: fullWidth + (alignRight ? 8 : 16)
height: fullHeight + 4 height: fullHeight + 4
visible: false visible: false
@ -1065,7 +1141,8 @@ in
// reveals/hides with the morph; fades on open/close. // reveals/hides with the morph; fades on open/close.
Item { Item {
id: _dropdownRect id: _dropdownRect
anchors.horizontalCenter: parent.horizontalCenter anchors.left: parent.left
anchors.leftMargin: 8
anchors.top: parent.top anchors.top: parent.top
width: dropdown.fullWidth width: dropdown.fullWidth
height: Math.min(dropdown.fullHeight, chrome.height) height: Math.min(dropdown.fullHeight, chrome.height)
@ -1124,6 +1201,41 @@ in
} }
} }
// Flush-right variant: no right ear; the bottom-right curve
// merges the panel into the screen frame's right column.
component PanelShapeFlush: Shape {
id: pshapeF
readonly property real ear: 8
readonly property real rr: Math.min(8, Math.max(1, height / 2))
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.barBg
strokeWidth: -1
startX: 0; startY: 0
PathArc { x: pshapeF.ear; y: Math.min(pshapeF.ear, pshapeF.height); radiusX: pshapeF.ear; radiusY: pshapeF.ear; direction: PathArc.Clockwise }
PathLine { x: pshapeF.ear; y: pshapeF.height - pshapeF.rr }
PathArc { x: pshapeF.ear + pshapeF.rr; y: pshapeF.height; radiusX: pshapeF.rr; radiusY: pshapeF.rr; direction: PathArc.Counterclockwise }
PathLine { x: pshapeF.width - pshapeF.ear; y: pshapeF.height }
PathArc { x: pshapeF.width; y: pshapeF.height + pshapeF.ear; radiusX: pshapeF.ear; radiusY: pshapeF.ear; direction: PathArc.Clockwise }
PathLine { x: pshapeF.width; y: 0 }
PathLine { x: 0; y: 0 }
}
ShapePath {
fillColor: "transparent"
strokeColor: Theme.base03
strokeWidth: Theme.borderWidth
capStyle: ShapePath.FlatCap
startX: 0; startY: 0
PathArc { x: pshapeF.ear; y: Math.min(pshapeF.ear, pshapeF.height); radiusX: pshapeF.ear; radiusY: pshapeF.ear; direction: PathArc.Clockwise }
PathLine { x: pshapeF.ear; y: pshapeF.height - pshapeF.rr }
PathArc { x: pshapeF.ear + pshapeF.rr; y: pshapeF.height; radiusX: pshapeF.rr; radiusY: pshapeF.rr; direction: PathArc.Counterclockwise }
PathLine { x: pshapeF.width - pshapeF.ear; y: pshapeF.height }
PathArc { x: pshapeF.width; y: pshapeF.height + pshapeF.ear; radiusX: pshapeF.ear; radiusY: pshapeF.ear; direction: PathArc.Clockwise }
}
}
// The shared morphing panel: follows the active dropdown's // The shared morphing panel: follows the active dropdown's
// geometry with animation (the caelestia-style morph), snaps // geometry with animation (the caelestia-style morph), snaps
// instantly when opening from closed. // instantly when opening from closed.
@ -1132,6 +1244,7 @@ in
property real tX: 0 property real tX: 0
property real tW: 200 property real tW: 200
property real tH: 0 property real tH: 0
property bool flushRight: false
property real openH: bar.activeDropdown ? tH : 0 property real openH: bar.activeDropdown ? tH : 0
x: tX x: tX
@ -1158,6 +1271,12 @@ in
when: bar.activeDropdown !== null when: bar.activeDropdown !== null
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: chrome; property: "flushRight"
value: bar.activeDropdown ? bar.activeDropdown.alignRight : false
when: bar.activeDropdown !== null
restoreMode: Binding.RestoreNone
}
Behavior on tX { Behavior on tX {
enabled: chrome.height > 0.5 enabled: chrome.height > 0.5
@ -1172,6 +1291,12 @@ in
} }
PanelShape { PanelShape {
visible: !chrome.flushRight
width: chrome.width
height: chrome.height
}
PanelShapeFlush {
visible: chrome.flushRight
width: chrome.width width: chrome.width
height: chrome.height height: chrome.height
} }