initial commit

This commit is contained in:
2026-07-26 21:16:55 +02:00
commit 30fdd5cb28
30 changed files with 3373 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import qs.config
// Renders a desktop-entry icon, which may be either an icon-theme name or an
// absolute path. Falls back to a lettered tile so an entry with a broken or
// missing icon still gets something clickable.
Item {
id: root
property string icon: ""
property string fallbackLabel: ""
// When set, shown instead of the lettered tile if the icon cannot resolve.
property string fallbackGlyph: ""
property color glyphColor: Theme.text
property int size: Theme.iconSize
implicitWidth: size
implicitHeight: size
readonly property string resolved: root.resolveIcon(root.icon)
function resolveIcon(name: string): string {
if (!name)
return "";
if (name.startsWith("file://") || name.startsWith("image://"))
return name;
if (name.startsWith("/"))
return "file://" + name;
// iconPath with checkExists returns "" when the theme has no such icon,
// so these can be chained as fallbacks. The last segment of a
// reverse-DNS app id ("org.qutebrowser.qutebrowser") is very often the
// real icon name, so try that before giving up.
const lower = name.toLowerCase();
const tail = lower.split(".").pop();
return Quickshell.iconPath(name, true) || Quickshell.iconPath(lower, true) || (tail !== lower ? Quickshell.iconPath(tail, true) : "");
}
IconImage {
id: image
anchors.fill: parent
source: root.resolved
visible: root.resolved !== "" && image.status !== Image.Error
}
Text {
anchors.centerIn: parent
visible: !image.visible && root.fallbackGlyph !== ""
text: root.fallbackGlyph
color: root.glyphColor
font.family: Theme.fontFamily
font.pixelSize: Math.max(10, root.size * 0.8)
}
Rectangle {
anchors.fill: parent
visible: !image.visible && root.fallbackGlyph === ""
radius: Math.max(2, root.size / 5)
color: Theme.surfaceActive
Text {
anchors.centerIn: parent
text: (root.fallbackLabel || "?").charAt(0).toUpperCase()
color: Theme.textDim
font.family: Theme.fontFamily
font.pixelSize: Math.max(8, root.size * 0.55)
font.bold: true
}
}
}
+87
View File
@@ -0,0 +1,87 @@
import QtQuick
import QtQuick.Shapes
import qs.config
// The bluetooth rune, drawn rather than themed: icon themes ship it only as a
// symbolic (black-on-transparent) SVG, which Qt renders as-is and which would
// be invisible on the dark panel. Drawing it also makes it recolourable to
// signal adapter state.
Item {
id: root
property color color: Theme.text
property int size: Theme.iconSize
// Struck through with a diagonal, for "off".
property bool struck: false
implicitWidth: root.size
implicitHeight: root.size
readonly property real glyphWidth: root.size * 0.44
readonly property real glyphHeight: root.size * 0.82
readonly property real stroke: Math.max(1.5, root.size * 0.09)
Shape {
anchors.centerIn: parent
width: root.glyphWidth
height: root.glyphHeight
preferredRendererType: Shape.CurveRenderer
// One continuous stroke: up the left diagonal, over the top apex, down
// the spine, out to the right and back down the other diagonal.
ShapePath {
strokeColor: root.color
strokeWidth: root.stroke
fillColor: "transparent"
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
startX: 0
startY: root.glyphHeight * 0.25
PathLine {
x: root.glyphWidth
y: root.glyphHeight * 0.75
}
PathLine {
x: root.glyphWidth * 0.5
y: root.glyphHeight
}
PathLine {
x: root.glyphWidth * 0.5
y: 0
}
PathLine {
x: root.glyphWidth
y: root.glyphHeight * 0.25
}
PathLine {
x: 0
y: root.glyphHeight * 0.75
}
}
}
Shape {
anchors.centerIn: parent
width: root.size
height: root.size
visible: root.struck
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeColor: root.color
strokeWidth: root.stroke
fillColor: "transparent"
capStyle: ShapePath.RoundCap
startX: root.size * 0.12
startY: root.size * 0.88
PathLine {
x: root.size * 0.88
y: root.size * 0.12
}
}
}
}
+63
View File
@@ -0,0 +1,63 @@
import QtQuick
import qs.config
// Shared hover/press surface used by every clickable thing on the panel.
//
// Content is placed at its natural size inside `holder`, which then drives the
// button's implicit size. Content must not anchor to the holder or the size
// binding becomes circular.
Rectangle {
id: root
property bool active: false
property int minContentWidth: 0
property alias acceptedButtons: mouse.acceptedButtons
property alias containsMouse: mouse.containsMouse
property color activeColor: Theme.surfaceActive
default property alias content: holder.data
// Decorations that should stretch across the whole button (underlines,
// badges) go here instead of `content`, so they stay out of the size
// calculation above.
property alias overlay: overlayHolder.data
signal clicked(var event)
implicitWidth: Math.max(root.minContentWidth, holder.childrenRect.width) + Theme.padding * 2
implicitHeight: holder.childrenRect.height + Theme.padding
radius: Theme.radius
color: root.active ? root.activeColor : mouse.containsMouse ? Theme.surfaceHover : "transparent"
Behavior on color {
ColorAnimation {
duration: Theme.animDuration
easing.type: Theme.animEasing
}
}
Item {
id: holder
anchors.centerIn: parent
width: childrenRect.width
height: childrenRect.height
}
Item {
id: overlayHolder
anchors.fill: parent
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onClicked: event => root.clicked(event)
}
}
+64
View File
@@ -0,0 +1,64 @@
import QtQuick
import qs.config
// A themed on/off switch. Reports intent through `toggled` rather than driving
// `checked` itself, so the caller can keep it bound to the real state and let
// the switch follow whatever actually happened.
Rectangle {
id: root
property bool checked: false
// Shown mid-flight, while the backing state is still catching up.
property bool busy: false
signal toggled(bool checked)
implicitWidth: 38
implicitHeight: 20
radius: height / 2
color: root.checked ? Theme.accent : Theme.background
border.width: Theme.borderWidth
border.color: root.checked ? Theme.accent : Theme.border
opacity: root.busy ? 0.6 : 1
Behavior on color {
ColorAnimation {
duration: Theme.animDuration
easing.type: Theme.animEasing
}
}
Rectangle {
id: knob
y: (parent.height - height) / 2
x: root.checked ? root.width - width - 3 : 3
width: root.height - 6
height: width
radius: height / 2
color: root.checked ? Theme.background : mouse.containsMouse ? Theme.text : Theme.textDim
Behavior on x {
NumberAnimation {
duration: Theme.animDuration
easing.type: Theme.animEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.animDuration
easing.type: Theme.animEasing
}
}
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggled(!root.checked)
}
}
+88
View File
@@ -0,0 +1,88 @@
import QtQuick
import QtQuick.Shapes
import qs.config
// The wifi fan, drawn for the same reason as the bluetooth rune: themes ship it
// symbolic-only, which Qt renders black-on-transparent. Drawing it also lets
// each arc be lit separately, which is how signal strength is shown.
Item {
id: root
property color color: Theme.text
// Unlit arcs stay drawn, faintly, so the glyph keeps one silhouette at
// every strength instead of changing shape as the signal moves.
property color dimColor: Theme.textDim
property int size: Theme.iconSize
// Lit arcs, 0-3.
property int level: 3
// Struck through with a diagonal, for "off".
property bool struck: false
implicitWidth: root.size
implicitHeight: root.size
readonly property real cx: root.size / 2
readonly property real cy: root.size * 0.74
readonly property real stroke: Math.max(1.5, root.size * 0.085)
Rectangle {
x: root.cx - width / 2
y: root.cy - height / 2
width: Math.max(2, root.size * 0.14)
height: width
radius: width / 2
color: root.level > 0 ? root.color : root.dimColor
}
Repeater {
model: 3
Shape {
id: arc
required property int index
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeColor: root.level > arc.index ? root.color : root.dimColor
strokeWidth: root.stroke
fillColor: "transparent"
capStyle: ShapePath.RoundCap
// Centred on the dot, opening upwards: 225° to 315°, measured
// clockwise from three o'clock.
PathAngleArc {
centerX: root.cx
centerY: root.cy
radiusX: root.size * (0.2 + arc.index * 0.115)
radiusY: root.size * (0.2 + arc.index * 0.115)
startAngle: 225
sweepAngle: 90
}
}
}
}
Shape {
anchors.fill: parent
visible: root.struck
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeColor: root.color
strokeWidth: root.stroke
fillColor: "transparent"
capStyle: ShapePath.RoundCap
startX: root.size * 0.12
startY: root.size * 0.88
PathLine {
x: root.size * 0.88
y: root.size * 0.12
}
}
}
}