Build the Panama Hyprland desktop

This commit is contained in:
Gabriel Brown
2026-08-17 10:32:55 -04:00
parent 67033f2a31
commit 5248883e4b
190 changed files with 18554 additions and 34 deletions
+73
View File
@@ -0,0 +1,73 @@
// A bar module container: rounded, hover-highlighted, optionally clickable.
// Every widget that sits in the top bar wraps itself in one of these so
// spacing, hover feedback and press feedback stay identical across the bar.
import QtQuick
import qs.config
Rectangle {
id: root
// Fired on left click; if nothing is connected the pill is inert and
// shows no hover affordance.
signal activated
signal secondaryActivated
signal scrolled(int delta)
property alias hovered: mouse.containsMouse
property bool interactive: true
property int horizontalPadding: 10
default property alias content: layout.data
implicitWidth: layout.implicitWidth + horizontalPadding * 2
implicitHeight: Theme.barHeight - 8
radius: Theme.pillRadius
// border.width: 0 works around QTBUG-137166, where transparent rounded
// rectangles can render with holes at the corners.
border.width: 0
color: {
if (!interactive)
return "transparent";
if (mouse.pressed)
return Theme.alpha(Theme.accent, Theme.activeAlpha);
if (mouse.containsMouse)
return Theme.alpha(Theme.fg, Theme.hoverAlpha);
return "transparent";
}
Behavior on color {
ColorAnimation {
duration: Theme.durFast
easing.type: Easing.OutQuad
}
}
Row {
id: layout
anchors.centerIn: parent
spacing: 6
}
MouseArea {
id: mouse
anchors.fill: parent
enabled: root.interactive
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: root.interactive ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: event => {
if (event.button === Qt.RightButton)
root.secondaryActivated();
else
root.activated();
}
onWheel: event => {
root.scrolled(event.angleDelta.y > 0 ? 1 : -1);
}
}
}
+90
View File
@@ -0,0 +1,90 @@
// A themed popup panel anchored under a bar item, in the style of GNOME's
// shell menus: near-opaque, heavily rounded, blurred behind (the blur comes
// from the `qs-popover` layer rule in hypr/rules.lua), closing on outside click.
//
// Usage:
// Popover {
// id: pop
// anchorItem: someBarPill
// Column { ... }
// }
// ...
// pop.visible = true
import Quickshell
import QtQuick
import qs.config
PopupWindow {
id: root
// The bar item this popover hangs from.
property Item anchorItem: null
property int contentPadding: Theme.popoverPadding
default property alias content: container.data
anchor.item: anchorItem
// Attach to the bottom-left of the anchor and grow down-right.
anchor.edges: Edges.Bottom | Edges.Left
anchor.gravity: Edges.Bottom | Edges.Right
anchor.margins.top: 8
implicitWidth: Math.max(container.implicitWidth + contentPadding * 2, 240)
implicitHeight: container.implicitHeight + contentPadding * 2
color: "transparent"
visible: false
// Clicking anywhere outside dismisses the popover and clears `visible`.
grabFocus: true
Rectangle {
id: surface
anchors.fill: parent
radius: Theme.popoverRadius
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.08)
// The prism edge — see widgets/PrismEdge.qml. Sits just inside the
// 1px border so the two don't fight for the same row of pixels.
PrismEdge {
anchors.top: parent.top
anchors.topMargin: 1
anchors.left: parent.left
anchors.right: parent.right
inset: parent.radius
}
// Scale + fade in. Runs once on open, never while idle.
transform: Scale {
id: popScale
origin.x: 0
origin.y: 0
xScale: 1
yScale: 1
}
Item {
id: container
anchors.fill: parent
anchors.margins: root.contentPadding
}
}
onVisibleChanged: {
if (visible)
openAnim.restart();
}
NumberAnimation {
id: openAnim
target: popScale
property: "yScale"
from: 0.96
to: 1.0
duration: Theme.durNormal
easing.type: Easing.OutCubic
}
}
@@ -0,0 +1,57 @@
// The signature: a one-pixel hairline along the top edge of a glass surface,
// running blue on the left to orchid on the right and fading out before it
// reaches either corner.
//
// It reads as light catching the bevel of a pane of glass, which is why it sits
// at the very top edge and nowhere else — put it on all four sides and it stops
// being light and becomes a border.
//
// Every glass surface in the shell carries one, so the bar, dock, popovers and
// notification cards all read as the same material.
//
// Usage — anchor it to the top of the surface and inset it past the corner
// radius so it doesn't overhang the curve:
//
// PrismEdge {
// anchors { top: parent.top; left: parent.left; right: parent.right }
// inset: parent.radius
// }
import QtQuick
import qs.config
Rectangle {
id: root
// Horizontal inset, normally the parent's corner radius. Below that the
// line runs out onto the curve and looks like a rendering artefact.
property real inset: Theme.popoverRadius
anchors.leftMargin: root.inset
anchors.rightMargin: root.inset
implicitHeight: 1
height: 1
// The colour stops are the only place the two accents touch.
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0.0
color: Theme.alpha(Theme.accent, 0)
}
GradientStop {
position: Theme.prismStart
color: Theme.alpha(Theme.accent, Theme.prismEdgeAlpha)
}
GradientStop {
position: Theme.prismEnd
color: Theme.alpha(Theme.accentSecondary, Theme.prismEdgeAlpha)
}
GradientStop {
position: 1.0
color: Theme.alpha(Theme.accentSecondary, 0)
}
}
}
@@ -0,0 +1,50 @@
// A freedesktop symbolic icon, recoloured to a Theme token.
//
// Adwaita's symbolic SVGs carry a hardcoded near-black fill (#2e3436) and Qt
// does not do GTK's runtime recolouring, so drawn as-is they are invisible on
// a Tokyo Night background — verified on this machine. The fix is to paint a
// themed rectangle through the icon's alpha channel.
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.config
Item {
id: root
property string icon: ""
property string iconFallback: "dialog-information-symbolic"
property color tint: Theme.fg
property int size: 18
implicitWidth: root.size
implicitHeight: root.size
// Both inputs are invisible but layered, so they render into textures
// without ever being composited onto the panel themselves.
IconImage {
id: glyph
anchors.fill: parent
asynchronous: true
source: Quickshell.iconPath(root.icon, root.iconFallback)
visible: false
layer.enabled: true
}
Rectangle {
id: fill
anchors.fill: parent
color: root.tint
visible: false
layer.enabled: true
}
MultiEffect {
anchors.fill: parent
source: fill
maskEnabled: true
maskSource: glyph
}
}
+105
View File
@@ -0,0 +1,105 @@
// A GNOME-style quick-settings tile: big rounded button with an icon and a
// label, filled with the accent colour when active.
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.config
Rectangle {
id: root
property string icon: "" // freedesktop icon name
property string label: ""
property string sublabel: ""
property bool active: false
property bool enabled: true
signal toggled
// Emitted on right-click / long press — used to open the relevant detail
// panel (pick a wifi network, pick an output device).
signal expanded
implicitWidth: 168
implicitHeight: 56
radius: Theme.cardRadius
border.width: 0
opacity: enabled ? 1.0 : 0.45
color: {
if (root.active)
return mouse.containsMouse ? Qt.lighter(Theme.accent, 1.12) : Theme.accent;
if (mouse.pressed)
return Theme.alpha(Theme.fg, Theme.activeAlpha);
if (mouse.containsMouse)
return Theme.alpha(Theme.fg, Theme.hoverAlpha);
return Theme.alpha(Theme.fg, 0.07);
}
Behavior on color {
ColorAnimation {
duration: Theme.durFast
}
}
readonly property color contentColor: root.active ? Theme.bgDark : Theme.fg
Row {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 10
// ThemedIcon, not a bare IconImage: Adwaita's symbolic SVGs carry a
// hardcoded #2e3436 fill and Qt does not recolour them the way GTK
// does, so drawn directly they are all but invisible on this palette.
ThemedIcon {
anchors.verticalCenter: parent.verticalCenter
size: 22
icon: root.icon
tint: root.contentColor
visible: root.icon !== ""
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 1
Text {
text: root.label
color: root.contentColor
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.Medium
elide: Text.ElideRight
width: Math.min(implicitWidth, 108)
}
Text {
text: root.sublabel
visible: root.sublabel !== ""
color: root.active ? Theme.alpha(Theme.bgDark, 0.7) : Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
width: Math.min(implicitWidth, 108)
}
}
}
MouseArea {
id: mouse
anchors.fill: parent
enabled: root.enabled
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: event => {
if (event.button === Qt.RightButton)
root.expanded();
else
root.toggled();
}
}
}
@@ -0,0 +1,121 @@
// A GNOME-style slider: a thick rounded track with an inline leading icon,
// used for volume and brightness in the quick settings panel.
//
// Deliberately not QtQuick.Controls.Slider — that pulls in a style plugin and
// fights the theme. This is ~50 lines and does exactly what's needed.
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.config
Item {
id: root
property real value: 0 // 0.0 .. 1.0
property string icon: ""
property bool enabled: true
// Emitted continuously while dragging.
signal moved(real value)
implicitWidth: 240
implicitHeight: 32
opacity: enabled ? 1.0 : 0.45
// ThemedIcon, not a bare IconImage — see the note in Toggle.qml: Adwaita
// symbolic icons have a hardcoded near-black fill that Qt will not recolour.
ThemedIcon {
id: leadingIcon
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
size: 18
icon: root.icon
tint: Theme.fg
visible: root.icon !== ""
}
Rectangle {
id: track
anchors.left: leadingIcon.visible ? leadingIcon.right : parent.left
anchors.leftMargin: leadingIcon.visible ? 10 : 0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 10
radius: height / 2
color: Theme.alpha(Theme.fg, 0.12)
border.width: 0
Rectangle {
id: fill
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * Math.max(0, Math.min(1, root.value))
radius: parent.radius
border.width: 0
// The gradient is anchored to the full track, not to the fill, so
// the colour at a given position never moves as the value changes —
// dragging reveals the prism rather than squashing it.
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0.0
color: Theme.accent
}
GradientStop {
position: 1.0
color: Theme.mix(Theme.accent, Theme.accentSecondary, Math.max(0.001, Math.min(1, root.value)))
}
}
// Only animates when the value changes from outside (e.g. a media
// key); dragging sets it directly and reads as instant.
Behavior on width {
enabled: !drag.pressed
NumberAnimation {
duration: Theme.durFast
easing.type: Easing.OutQuad
}
}
}
Rectangle {
id: knob
width: 16
height: 16
radius: 8
border.width: 0
color: Theme.fg
anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(parent.width - width, fill.width - width / 2))
visible: drag.containsMouse || drag.pressed
}
MouseArea {
id: drag
anchors.fill: parent
anchors.margins: -8 // easier to grab than a 10px track
enabled: root.enabled
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function apply(mouseX) {
const ratio = Math.max(0, Math.min(1, (mouseX + 8) / track.width));
root.moved(ratio);
}
onPressed: event => apply(event.x)
onPositionChanged: event => {
if (pressed)
apply(event.x);
}
onWheel: event => {
const step = event.angleDelta.y > 0 ? 0.05 : -0.05;
root.moved(Math.max(0, Math.min(1, root.value + step)));
}
}
}
}