mirror of
https://github.com/prdlk/omarchy-beeper.git
synced 2026-11-02 13:12:48 +00:00
feat: omarchy-beeper bar widget for unread Beeper chats
This commit is contained in:
commit
7fc78f1075
17 files changed
+3483
No files matched your search
+124
@@ -0,0 +1,124 @@
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import qs.Commons
|
||||
|
||||
// Chat bubble: rounded outline with a tail, and three dots that fade in when
|
||||
// something is unread. Stroke, not a filled blob, so the silhouette keeps
|
||||
// contrast on a transparent bar over light, dark, or mixed wallpapers.
|
||||
// Every painted extent stays inside the optical canvas at every dot pose.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real iconSize: Style.bar.iconCanvas
|
||||
property color color: Color.foreground
|
||||
property color dotColor: color
|
||||
property bool hasMail: false
|
||||
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
implicitWidth: iconSize
|
||||
implicitHeight: iconSize
|
||||
|
||||
readonly property real dpr: {
|
||||
var win = Window.window
|
||||
return (win && win.devicePixelRatio > 0) ? win.devicePixelRatio : 1
|
||||
}
|
||||
|
||||
function snap(v) {
|
||||
return Math.round(v * root.dpr) / root.dpr
|
||||
}
|
||||
|
||||
function snapStroke(v) {
|
||||
return Math.max(1, Math.round(v * root.dpr)) / root.dpr
|
||||
}
|
||||
|
||||
readonly property real stroke: snapStroke(Math.max(1.5, iconSize * 0.11))
|
||||
readonly property real pad: snap(Math.max(stroke / 2, 0.5))
|
||||
|
||||
readonly property real tailH: snap(Math.max(stroke * 1.2, iconSize * 0.16))
|
||||
readonly property real bodyX: snap(pad)
|
||||
readonly property real bodyY: snap(pad)
|
||||
readonly property real bodyW: snap(iconSize - pad * 2)
|
||||
readonly property real bodyH: snap(Math.max(stroke * 3, iconSize - pad * 2 - tailH))
|
||||
readonly property real radius: snap(Math.min(bodyW, bodyH) * 0.3)
|
||||
|
||||
// The tail hangs off the bottom edge, left of centre, and never reaches
|
||||
// the corner arc: keep both feet between the arcs.
|
||||
readonly property real tailLeft: snap(bodyX + Math.max(radius, bodyW * 0.2))
|
||||
readonly property real tailRight: snap(tailLeft + Math.max(stroke, bodyW * 0.2))
|
||||
readonly property real tailTipX: snap(tailLeft + Math.max(stroke / 2, bodyW * 0.04))
|
||||
|
||||
readonly property real dotR: snap(Math.max(1, Math.min(iconSize * 0.075, bodyH * 0.16)))
|
||||
readonly property real dotGap: snap(Math.max(dotR * 2.2, bodyW * 0.24))
|
||||
readonly property real dotY: snap(bodyY + bodyH / 2)
|
||||
|
||||
property real dotAmount: hasMail ? 1 : 0
|
||||
Behavior on dotAmount {
|
||||
NumberAnimation { duration: 180; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: canvas
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.lineCap = "round"
|
||||
ctx.lineJoin = "round"
|
||||
ctx.strokeStyle = root.color
|
||||
ctx.lineWidth = root.stroke
|
||||
|
||||
var x = root.bodyX
|
||||
var y = root.bodyY
|
||||
var w = root.bodyW
|
||||
var h = root.bodyH
|
||||
var r = Math.min(root.radius, w / 2, h / 2)
|
||||
|
||||
// One closed path: bubble outline with the tail cut into the bottom
|
||||
// edge, so there is no seam where a second stroke would overlap.
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + r, y)
|
||||
ctx.lineTo(x + w - r, y)
|
||||
ctx.arcTo(x + w, y, x + w, y + r, r)
|
||||
ctx.lineTo(x + w, y + h - r)
|
||||
ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
|
||||
ctx.lineTo(root.tailRight, y + h)
|
||||
ctx.lineTo(root.tailTipX, y + h + root.tailH)
|
||||
ctx.lineTo(root.tailLeft, y + h)
|
||||
ctx.lineTo(x + r, y + h)
|
||||
ctx.arcTo(x, y + h, x, y + h - r, r)
|
||||
ctx.lineTo(x, y + r)
|
||||
ctx.arcTo(x, y, x + r, y, r)
|
||||
ctx.closePath()
|
||||
ctx.stroke()
|
||||
|
||||
if (root.dotAmount <= 0.01)
|
||||
return
|
||||
|
||||
ctx.fillStyle = root.dotColor
|
||||
ctx.globalAlpha = root.dotAmount
|
||||
var cx = x + w / 2
|
||||
var scaled = root.dotR * (0.6 + 0.4 * root.dotAmount)
|
||||
for (var i = -1; i <= 1; i++) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx + i * root.dotGap, root.dotY, scaled, 0, Math.PI * 2, false)
|
||||
ctx.fill()
|
||||
}
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
onColorChanged: canvas.requestPaint()
|
||||
onDotColorChanged: canvas.requestPaint()
|
||||
onDotAmountChanged: canvas.requestPaint()
|
||||
onIconSizeChanged: canvas.requestPaint()
|
||||
onBodyWChanged: canvas.requestPaint()
|
||||
onDotRChanged: canvas.requestPaint()
|
||||
onDprChanged: canvas.requestPaint()
|
||||
Component.onCompleted: canvas.requestPaint()
|
||||
}
|
||||
Reference in new issue
Block a user