diff --git a/settings/quickshell.nix b/settings/quickshell.nix index a4582c8..9cacbf7 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -91,6 +91,8 @@ in readonly property string fontFamily: "${monoFont}" // Matches hyprland general.border_size (col.inactive_border = base03) 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 } + // ── 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 // panel takes priority, then the toast. Tracking the animated // 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. Rectangle { id: barBorderLeft - x: 0; y: 30 - Theme.borderWidth / 2 - width: bar.hasGap ? bar.gapLeft : bar.width + x: Theme.frameWidth + 8 + y: 30 - Theme.borderWidth / 2 + width: Math.max(0, (bar.hasGap ? bar.gapLeft : bar.width - Theme.frameWidth - 8) - x) height: Theme.borderWidth color: Theme.base03 } @@ -487,7 +559,7 @@ in visible: bar.hasGap x: bar.gapRight y: 30 - Theme.borderWidth / 2 - width: bar.width - x + width: Math.max(0, bar.width - Theme.frameWidth - 8 - x) height: Theme.borderWidth color: Theme.base03 } @@ -998,7 +1070,9 @@ in property real fullWidth: 200 property real fullHeight: 200 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 default property alias content: dropdownContent.data @@ -1024,9 +1098,11 @@ in _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 - width: fullWidth + 16 + width: fullWidth + (alignRight ? 8 : 16) height: fullHeight + 4 visible: false @@ -1065,7 +1141,8 @@ in // reveals/hides with the morph; fades on open/close. Item { id: _dropdownRect - anchors.horizontalCenter: parent.horizontalCenter + anchors.left: parent.left + anchors.leftMargin: 8 anchors.top: parent.top width: dropdown.fullWidth 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 // geometry with animation (the caelestia-style morph), snaps // instantly when opening from closed. @@ -1132,6 +1244,7 @@ in property real tX: 0 property real tW: 200 property real tH: 0 + property bool flushRight: false property real openH: bar.activeDropdown ? tH : 0 x: tX @@ -1158,6 +1271,12 @@ in when: bar.activeDropdown !== null 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 { enabled: chrome.height > 0.5 @@ -1172,6 +1291,12 @@ in } PanelShape { + visible: !chrome.flushRight + width: chrome.width + height: chrome.height + } + PanelShapeFlush { + visible: chrome.flushRight width: chrome.width height: chrome.height }