dankbar: trace goth wings and adjacent bar seams

fixes #3496
This commit is contained in:
bbedward
2026-09-21 21:32:47 -04:00
parent 75b8cfff4f
commit 8538b303e3
4 changed files with 354 additions and 15 deletions
+32
View File
@@ -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);
}
+257
View File
@@ -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);
}
+63 -15
View File
@@ -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
}
}
}
}
}
@@ -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
});