Build the Panama Hyprland desktop
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user