diff --git a/quickshell/Common/ShellLayout.qml b/quickshell/Common/ShellLayout.qml index 02df95079..75e9a1304 100644 --- a/quickshell/Common/ShellLayout.qml +++ b/quickshell/Common/ShellLayout.qml @@ -115,6 +115,38 @@ Singleton { return (layout?.instances ?? []).filter(instance => instance.edge === side).reduce((sum, instance) => sum + instance.reservation, 0); } + // non-manual layouts let the compositor stack exclusive zones, so the bar that was mapped later is the tucked one + function adjacentCover(screen, side, config, length) { + const layout = forScreen(screen); + const neighbour = adjacentBar(screen, side, config); + if (!layout || !neighbour) + return null; + const opposite = { + top: "bottom", + bottom: "top", + left: "right", + right: "left" + }[side]; + const near = layout.edges[side].reservation; + const far = adjacentBar(screen, opposite, config) ? layout.edges[opposite].reservation : 0; + const deficit = (side === "top" || side === "bottom" ? layout.screen.height : layout.screen.width) - length; + const order = id => layout.instances.find(instance => instance.barId === id)?.configOrder ?? 0; + let tucked; + if (deficit >= near + far - 1) + tucked = true; + else if (deficit < near - 1) + tucked = false; + else if (Math.abs(near - far) > 1) + tucked = Math.abs(deficit - near) <= 1; + else + tucked = order(neighbour.config.id) < order(config?.id); + return { + reach: dockAdjacentThickness(screen, side), + wing: neighbour.wingSize ?? 0, + tucked + }; + } + function adjacentInfo(screen, config) { return Resolver.adjacentInfo(forScreen(screen), config, primaryBar); } diff --git a/quickshell/Modules/DankBar/BarOutline.js b/quickshell/Modules/DankBar/BarOutline.js new file mode 100644 index 000000000..4b465a45c --- /dev/null +++ b/quickshell/Modules/DankBar/BarOutline.js @@ -0,0 +1,257 @@ +.pragma library + +function borderPath(g) { + const f = canonical(g); + if (!f) + return ""; + return serialize(g.alongWings ? alongOps(f) : crossOps(f), g.position, g.width, g.height); +} + +function canonical(g) { + const body = g.body; + const c = g.corners; + switch (g.position) { + case "top": + return frame(g, body.x, body.x + body.width, body.height, c.topLeft, c.topRight, c.bottomLeft, c.bottomRight); + case "bottom": + return frame(g, body.x, body.x + body.width, body.height, c.bottomLeft, c.bottomRight, c.topLeft, c.topRight); + case "left": + return frame(g, body.y, body.y + body.height, body.width, c.topLeft, c.bottomLeft, c.topRight, c.bottomRight); + case "right": + return frame(g, body.y, body.y + body.height, body.width, c.topRight, c.bottomRight, c.topLeft, c.bottomLeft); + default: + return null; + } +} + +function frame(g, start, end, thickness, attachedStart, attachedEnd, farStart, farEnd) { + if (end - start <= 0 || thickness <= 0) + return null; + const inset = g.inset; + const shrink = radius => Math.max(0, radius - inset); + const sides = g.alongWings || !g.open; + const cover = reach => { + if (g.alongWings) + return 0; + return Math.max(0, reach ?? 0); + }; + return { + start, + end, + thickness, + inset, + wing: Math.max(0, g.wing), + attachedStart: shrink(attachedStart), + attachedEnd: shrink(attachedEnd), + farStart: shrink(farStart), + farEnd: shrink(farEnd), + attachedVisible: !g.open, + startSide: sides && !(g.seamStart ?? false), + endSide: sides && !(g.seamEnd ?? false), + coverStart: cover(g.coverStart), + coverEnd: cover(g.coverEnd), + coverStartWing: Math.max(0, g.coverStartWing ?? 0), + coverEndWing: Math.max(0, g.coverEndWing ?? 0) + }; +} + +function crossOps(f) { + const p = pen(); + const i = f.inset; + const r = f.wing; + const T = f.thickness; + // the offset of a concave arc meets the offset side line 2*sqrt(r*i) short of the wing tip + const tail = T + r - 2 * Math.sqrt(r * i); + const farEdge = T - i; + const endSideBottom = endSideStop(f, tail); + const farEdgeStart = farEdgeStop(f); + if (f.attachedVisible) { + if (f.startSide) + p.at(f.start + i + f.attachedStart, i); + else + p.at(f.start + i, i + f.attachedStart).corner(f.attachedStart, f.start + i + f.attachedStart, i); + p.at(f.end - i - f.attachedEnd, i).corner(f.attachedEnd, f.end - i, i + f.attachedEnd); + if (f.endSide) + p.at(f.end - i, endSideBottom); + else + p.lift(); + } + if (f.coverEnd > 0) { + // carry the stroke onto the tucked neighbour's wing circle so both windows paint one band + p.lift(); + if (f.coverEndWing > 0) + p.at(f.end - f.coverEnd + f.coverEndWing + i, T + f.coverEndWing).arc(f.coverEndWing + i, 0, f.end - f.coverEnd, T - i); + else + p.at(f.end - f.coverEnd, T - i); + } else if (r > 0) { + if (!f.endSide) + p.at(f.end + i, T + r); + p.arc(r + i, 0, f.end - r, T - i); + } else { + if (!f.endSide) + p.at(f.end - i, T - i - f.farEnd); + p.corner(f.farEnd, f.end - i - f.farEnd, T - i); + } + p.at(farEdgeStart, farEdge); + if (f.coverStart > 0) { + if (f.coverStartWing > 0) + p.arc(f.coverStartWing + i, 0, f.start + f.coverStart - f.coverStartWing - i, T + f.coverStartWing); + p.lift(); + if (f.startSide) + p.at(f.start + i, T - i); + } else if (r > 0) { + if (f.startSide) + p.arc(r + i, 0, f.start + i, tail); + else + p.arc(r + i, 0, f.start - i, T + r).lift(); + } else { + p.corner(f.farStart, f.start + i, T - i - f.farStart); + if (!f.startSide) + p.lift(); + } + if (!f.startSide || !f.attachedVisible) + return p.ops; + p.at(f.start + i, i + f.attachedStart).corner(f.attachedStart, f.start + i + f.attachedStart, i).seal(); + return p.ops; +} + +function endSideStop(f, tail) { + const farEdge = f.thickness - f.inset; + if (f.coverEnd > 0) + return farEdge; + if (f.wing > 0) + return tail; + return farEdge - f.farEnd; +} + +function farEdgeStop(f) { + if (f.coverStart > 0) + return f.start + f.coverStart; + if (f.wing > 0) + return f.start + f.wing; + return f.start + f.inset + f.farStart; +} + +function alongOps(f) { + const p = pen(); + const i = f.inset; + const r = f.wing; + const T = f.thickness; + // the offset of a wing arc meets the offset attached edge 2*sqrt(r*i) short of the wing tip + const reach = 2 * Math.sqrt(r * i); + const startTip = f.start - r; + const endTip = f.end + r; + if (r > 0) { + if (f.attachedVisible) + p.at(startTip + reach, i).at(endTip - reach, i); + else + p.at(endTip, -i); + p.arc(r + i, 0, f.end - i, r); + } else { + if (f.attachedVisible) + p.at(f.start + i + f.attachedStart, i); + p.at(f.end - i - f.attachedEnd, i).corner(f.attachedEnd, f.end - i, i + f.attachedEnd); + } + p.at(f.end - i, T - i - f.farEnd).corner(f.farEnd, f.end - i - f.farEnd, T - i); + p.at(f.start + i + f.farStart, T - i).corner(f.farStart, f.start + i, T - i - f.farStart); + if (r > 0) { + p.at(f.start + i, r); + if (f.attachedVisible) + p.arc(r + i, 0, startTip + reach, i); + else + p.arc(r + i, 0, startTip, -i); + } else { + p.at(f.start + i, i + f.attachedStart).corner(f.attachedStart, f.start + i + f.attachedStart, i); + } + if (f.attachedVisible) + p.seal(); + return p.ops; +} + +function pen() { + const self = { + ops: [], + down: false, + lifted: false, + at(a, c) { + self.ops.push({ + op: self.down ? "L" : "M", + a, + c + }); + self.down = true; + return self; + }, + arc(radius, sweep, a, c) { + self.ops.push({ + op: "A", + radius, + sweep, + a, + c + }); + return self; + }, + corner(radius, a, c) { + return radius > 0 ? self.arc(radius, 1, a, c) : self; + }, + lift() { + self.down = false; + self.lifted = true; + return self; + }, + seal() { + if (!self.lifted) + self.ops.push({ + op: "Z" + }); + self.down = false; + return self; + } + }; + return self; +} + +function serialize(ops, position, width, height) { + const map = mapper(position, width, height); + const flipSweep = position === "bottom" || position === "left"; + return ops.map(op => { + if (op.op === "Z") + return "Z"; + const point = map(op.a, op.c); + const target = `${num(point.x)} ${num(point.y)}`; + if (op.op !== "A") + return `${op.op} ${target}`; + const sweep = flipSweep ? 1 - op.sweep : op.sweep; + return `A ${num(op.radius)} ${num(op.radius)} 0 0 ${sweep} ${target}`; + }).join(" "); +} + +function mapper(position, width, height) { + switch (position) { + case "bottom": + return (a, c) => ({ + x: a, + y: height - c + }); + case "left": + return (a, c) => ({ + x: c, + y: a + }); + case "right": + return (a, c) => ({ + x: width - c, + y: a + }); + default: + return (a, c) => ({ + x: a, + y: c + }); + } +} + +function num(value) { + return String(Math.round(value * 100) / 100); +} diff --git a/quickshell/Modules/DankBar/BarSurface.qml b/quickshell/Modules/DankBar/BarSurface.qml index 8126c96eb..a3dcf66a3 100644 --- a/quickshell/Modules/DankBar/BarSurface.qml +++ b/quickshell/Modules/DankBar/BarSurface.qml @@ -1,7 +1,9 @@ import QtQuick +import QtQuick.Shapes import qs.Common import qs.Services import qs.Modules.SurfaceWidgets +import "BarOutline.js" as BarOutline Item { id: root @@ -21,6 +23,14 @@ Item { readonly property bool edgeAttached: (barConfig?.attachToScreenEdge ?? false) && !frameShapesBar readonly property bool alongWings: !(barWindow.spansEdge ?? true) readonly property real wing: gothEnabled ? barWindow._wingR : 0 + readonly property real windowLength: axis.isVertical ? barWindow.height : barWindow.width + readonly property string startSide: axis.isVertical ? "top" : "left" + readonly property string endSide: axis.isVertical ? "bottom" : "right" + readonly property var startNeighbour: alongWings ? null : ShellLayout.adjacentCover(barWindow.screen, startSide, barConfig, windowLength) + readonly property var endNeighbour: alongWings ? null : ShellLayout.adjacentCover(barWindow.screen, endSide, barConfig, windowLength) + readonly property real alongOrigin: axis.isVertical ? 0 : barWindow.effectiveSpacing + readonly property real startCover: coveredBy(startNeighbour) + readonly property real endCover: coveredBy(endNeighbour) readonly property real alongWing: alongWings ? wing : 0 readonly property real crossWing: alongWings ? 0 : wing readonly property real rt: { @@ -112,6 +122,12 @@ Item { anchors.topMargin: -(axis.isVertical ? alongWing : (isBottom ? crossWing : 0)) anchors.bottomMargin: -(axis.isVertical ? alongWing : (isTop ? crossWing : 0)) + function coveredBy(neighbour) { + if (!neighbour || neighbour.tucked) + return 0; + return Math.max(0, neighbour.reach - alongOrigin) + neighbour.wing; + } + function alongWingCorner(leading) { switch (barPos) { case SettingsData.Position.Bottom: @@ -202,7 +218,7 @@ Item { id: leadingWing radius: Math.max(0, motion.currentOffsetAlong) color: root.barWindow._bgColor - visible: root.gothEnabled && radius > 0 + visible: root.gothEnabled && radius > 0 && root.startCover <= 0 x: root.alongWings ? (root.axis.isVertical && root.isRight ? root.width - radius : 0) : (root.isLeft ? body.width : 0) y: root.alongWings ? (!root.axis.isVertical && root.isBottom ? root.height - radius : 0) : (root.isTop ? body.height : 0) corner: root.alongWings ? root.alongWingCorner(true) : root.isTop ? "bottomRight" : root.isBottom ? "topRight" : root.isLeft ? "bottomRight" : "bottomLeft" @@ -212,30 +228,62 @@ Item { id: trailingWing radius: Math.max(0, motion.currentOffsetAlong) color: root.barWindow._bgColor - visible: root.gothEnabled && radius > 0 + visible: root.gothEnabled && radius > 0 && root.endCover <= 0 x: root.axis.isVertical ? (root.alongWings ? (root.isRight ? root.width - radius : 0) : (root.isLeft ? body.width : 0)) : root.width - radius y: root.axis.isVertical ? root.height - radius : (root.alongWings ? (root.isBottom ? root.height - radius : 0) : (root.isTop ? body.height : 0)) corner: root.alongWings ? root.alongWingCorner(false) : root.isTop ? "bottomLeft" : root.isBottom ? "topLeft" : root.isLeft ? "topRight" : "topLeft" } - Rectangle { + Loader { id: border readonly property real thickness: Theme.snap(Math.max(Theme.outlineWidth, barConfig?.borderThickness ?? Theme.outlineWidth), CompositorService.getScreenScale(root.barWindow.screen)) readonly property string colorKey: barConfig?.borderColor || "surfaceText" readonly property color baseColor: colorKey === "surfaceText" ? Theme.surfaceText : colorKey === "primary" ? Theme.primary : Theme.secondary readonly property bool showFullBorder: (barConfig?.spacing ?? 4) > 0 + readonly property string path: BarOutline.borderPath({ + position: root.axis.edge, + width: root.width, + height: root.height, + body: { + x: body.x, + y: body.y, + width: body.width, + height: body.height + }, + corners: { + topLeft: body.topLeftRadius, + topRight: body.topRightRadius, + bottomLeft: body.bottomLeftRadius, + bottomRight: body.bottomRightRadius + }, + wing: root.gothEnabled ? leadingWing.radius : 0, + alongWings: root.alongWings, + inset: thickness / 2, + open: !showFullBorder, + coverStart: root.startCover, + coverStartWing: root.startNeighbour?.wing ?? 0, + coverEnd: root.endCover, + coverEndWing: root.endNeighbour?.wing ?? 0, + seamStart: root.startNeighbour?.tucked ?? false, + seamEnd: root.endNeighbour?.tucked ?? false + }) z: 100 - visible: barConfig?.borderEnabled ?? false - x: body.x - (!showFullBorder && root.isRight ? thickness : 0) - y: body.y - (!showFullBorder && root.isBottom ? thickness : 0) - width: body.width + (!showFullBorder && (root.isLeft || root.isRight) ? thickness : 0) - height: body.height + (!showFullBorder && (root.isTop || root.isBottom) ? thickness : 0) - topLeftRadius: body.topLeftRadius - topRightRadius: body.topRightRadius - bottomLeftRadius: body.bottomLeftRadius - bottomRightRadius: body.bottomRightRadius - color: "transparent" - border.width: thickness - border.color: Theme.withAlpha(baseColor, barConfig?.borderOpacity ?? 1.0) + anchors.fill: parent + active: barConfig?.borderEnabled ?? false + sourceComponent: Shape { + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: "transparent" + strokeColor: Theme.withAlpha(border.baseColor, barConfig?.borderOpacity ?? 1.0) + strokeWidth: border.thickness + joinStyle: ShapePath.RoundJoin + capStyle: ShapePath.FlatCap + + PathSvg { + path: border.path + } + } + } } } diff --git a/quickshell/Modules/DankBar/DankBarBody.qml b/quickshell/Modules/DankBar/DankBarBody.qml index 7537f5d28..d6a9cee55 100644 --- a/quickshell/Modules/DankBar/DankBarBody.qml +++ b/quickshell/Modules/DankBar/DankBarBody.qml @@ -321,6 +321,8 @@ Item { if (hasBar && barBackground.gothEnabled && barWindow._wingR > 0) { for (const wingItem of [barBackground.leadingWing, barBackground.trailingWing]) { + if (!wingItem.visible) + continue; const wing = blurWingRegionComp.createObject(region, { wing: wingItem });