Promote bar/common to qs.Ui kit module

This commit is contained in:
Ryan Hughes
2026-05-17 22:33:08 -04:00
parent 408dada45d
commit 6b915926b2
25 changed files with 110 additions and 98 deletions
@@ -10,7 +10,7 @@ import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Commons
import "common" as BarCommon
import qs.Ui
Item {
id: root
@@ -1716,7 +1716,7 @@ Item {
}
}
BarCommon.PopupCard {
PopupCard {
id: managePopup
anchorItem: trayRoot
owner: trayRoot
@@ -1801,7 +1801,7 @@ Item {
elide: Text.ElideRight
}
BarCommon.PillButton {
PillButton {
id: rowPinBtn
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
@@ -1815,7 +1815,7 @@ Item {
onClicked: trayRoot.togglePin(rowRoot.itemId)
}
BarCommon.PillButton {
PillButton {
id: rowHideBtn
anchors.verticalCenter: parent.verticalCenter
anchors.right: rowPinBtn.left
@@ -1,25 +0,0 @@
import QtQuick
// Shared visual chrome for keyboard-and-mouse-navigable items inside a panel.
// Contract: items must NOT read `containsMouse` for color/border. Mouse
// hover updates the panel's cursor state at the root; visuals derive from
// `hasCursor` / `current`. That's what guarantees a single highlight on
// screen at any time across both keyboard and mouse interaction.
Rectangle {
id: root
property bool hasCursor: false
property bool current: false
property color foreground: "#cacccc"
property color fill: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18)
radius: 4
color: (hasCursor || current) ? fill : "transparent"
border.width: hasCursor ? 1 : 0
border.color: foreground
Behavior on color {
ColorAnimation { duration: 60 }
}
}
@@ -1,212 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
// Layer-shell popup attached to a bar widget icon, designed for
// click-driven AND keyboard-driven panels (e.g. SUPER+CTRL+W summon).
//
// Built on PanelWindow with WlrKeyboardFocus.Exclusive rather than
// PopupWindow (xdg-popup). Layer-shell surfaces declared Exclusive get
// keyboard focus from Hyprland *at map time*, which is the protocol-level
// equivalent of focus-on-launch for xdg-toplevels. xdg-popups don't get
// that — they only receive keys after a click/hover routes focus through
// their parent surface — so keyboard-summoned popups fell flat without it.
//
// API is a subset of Common.PopupCard: anchorItem, owner, bar, open,
// padding, margin, contentWidth/Height, default contentItem. Missing on
// purpose (for now): centerOnBar, triggerMode ("hover"), containsMouse.
// Hover-mode popups (system-stats, weather-flyout) and centered popups
// (calendar week-view) need extra plumbing before migrating; converting
// them is a follow-up.
//
// Positioning: full-screen layer-shell with the card placed inside at
// `cardOrigin`. We use the bar window's height/width for the perpendicular
// axis (away-from-bar) because mapToItem on the anchor returns
// bar-content-relative coords with internal layout offsets baked in
// (e.g. ~13px from the bar's vertical centering of its widget row). The
// parallel axis (along-the-bar) uses the anchor's content x/y since the
// bar spans full screen on that axis.
//
// Outside-click dismissal: an overlay MouseArea catches clicks, with the
// QsWindow.mask subtracting the bar strip so clicks on the bar still
// reach the bar widgets (activePopout coordinator hands off to another
// popup if the user clicks a different bar icon).
PanelWindow {
id: root
required property Item anchorItem
required property QtObject bar
property var owner: null
property int margin: 10
property int padding: 14
property int contentWidth: 280
property int contentHeight: 200
property bool open: false
property int gap: 10 // distance between bar edge and panel
default property alias contentItem: contentHolder.children
readonly property var coordinatorKey: owner || root
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
readonly property string barPos: bar ? bar.position : "top"
function closePopout() {
if (owner && "closePopout" in owner) owner.closePopout()
else root.open = false
}
// --- screen + lifetime ---------------------------------------------------
screen: anchorWindow ? anchorWindow.screen : null
visible: open || card.opacity > 0
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "omarchy-keyboard-panel"
WlrLayershell.layer: WlrLayer.Overlay
// Keyboard focus follows `open` (NOT `visible`). The window remains
// mapped during the fade-out so the opacity animation has something to
// animate, but keyboard/click ownership must release the moment the
// logical close fires — otherwise the user is locked out for 140ms.
WlrLayershell.keyboardFocus: open ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
// Full-screen layer-shell. The visible card is positioned inside via
// `cardOrigin`. The `mask` below makes the bar area click-through (so
// the user can click another bar icon while the panel is open and the
// activePopout coordinator swaps to that popup); everywhere else, the
// overlay catches the click and dismisses via the MouseArea below.
anchors {
top: true
bottom: true
left: true
right: true
}
// Clickable region = whole screen MINUS the bar's strip. Clicks on the
// bar pass through to the bar layer; clicks anywhere else are caught
// by us and either land on the card (no-op) or trigger dismissal.
readonly property real _barStripSize: bar ? bar.barSize : 0
mask: Region {
width: root.screenW
height: root.screenH
Region {
x: root.barPos === "right" ? root.screenW - root._barStripSize : 0
y: root.barPos === "bottom" ? root.screenH - root._barStripSize : 0
width: (root.barPos === "top" || root.barPos === "bottom") ? root.screenW : root._barStripSize
height: (root.barPos === "top" || root.barPos === "bottom") ? root._barStripSize : root.screenH
intersection: Intersection.Subtract
}
}
// Track every layout change between the bar's contentItem and the
// anchor item. `transform` updates whenever any item in that chain
// moves/resizes, which is what makes the position binding below
// actually reactive — mapToItem on its own is a one-shot.
TransformWatcher {
id: anchorWatcher
a: anchorWindow ? anchorWindow.contentItem : null
b: anchorItem
}
// Anchor item's position within the bar's content surface. For a
// full-width top bar, the content x maps directly to screen x; the y
// returned here has the bar's internal padding baked in (e.g. ~13px
// from vertical centering of the widget row), which is why `cardOrigin`
// below uses `barH` for the perpendicular axis instead of this y.
readonly property point anchorScreenPos: {
anchorWatcher.transform // reactive dependency
if (!anchorItem || !anchorWindow) return Qt.point(0, 0)
return anchorItem.mapToItem(anchorWindow.contentItem, 0, 0)
}
readonly property real anchorW: anchorItem ? anchorItem.width : 0
readonly property real anchorH: anchorItem ? anchorItem.height : 0
readonly property real screenW: screen ? screen.width : 0
readonly property real screenH: screen ? screen.height : 0
// Desired top-left of the card in screen coordinates. For the
// perpendicular axis (away-from-bar) we anchor to the bar window's edge
// directly — not the anchor item's y/x — because mapToItem(barContent)
// returns coordinates in the bar's content space, which can be offset
// from the bar surface's screen-anchored corner by internal layout
// (centering wrappers, padding). The bar's surface IS aligned to its
// anchored screen edge, so using `barW`/`barH` gives the right edge
// regardless of how the bar's internal widgets are positioned. For the
// parallel axis (along the bar) the anchor item's reported position is
// still consistent with the bar content origin, so it's accurate for
// centering the card under the icon.
readonly property real barW: anchorWindow ? anchorWindow.width : screenW
readonly property real barH: anchorWindow ? anchorWindow.height : 0
readonly property point cardOrigin: {
if (!anchorItem || !bar) return Qt.point(margin, margin)
var x = 0, y = 0
if (barPos === "bottom") {
x = anchorScreenPos.x + anchorW / 2 - contentWidth / 2
y = screenH - barH - contentHeight - gap
} else if (barPos === "left") {
x = barW + gap
y = anchorScreenPos.y + anchorH / 2 - contentHeight / 2
} else if (barPos === "right") {
x = screenW - barW - contentWidth - gap
y = anchorScreenPos.y + anchorH / 2 - contentHeight / 2
} else { // "top" (default)
x = anchorScreenPos.x + anchorW / 2 - contentWidth / 2
y = barH + gap
}
x = Math.max(margin, Math.min(x, screenW - contentWidth - margin))
y = Math.max(margin, Math.min(y, screenH - contentHeight - margin))
return Qt.point(Math.round(x), Math.round(y))
}
// --- popout coordination (same-bar single-popout model) -----------------
// Coordinate on `open`, not `visible`. `visible` lags into the fade-out
// animation, which made ownership transfer to a sibling popup race.
onOpenChanged: {
if (!bar) return
if (open) bar.requestPopout(coordinatorKey)
else if (bar.activePopout === coordinatorKey) bar.releasePopout(coordinatorKey)
}
// --- outside-click dismissal --------------------------------------------
// Catches clicks anywhere in the clickable region (i.e. everywhere on
// screen except the bar strip, which is masked out). The card has its
// own MouseArea below so clicks on it don't bubble up here. Disabled
// during the fade-out so the dying overlay doesn't swallow clicks that
// were meant for the apps behind it.
MouseArea {
anchors.fill: parent
enabled: root.open
onClicked: root.closePopout()
}
// --- card ----------------------------------------------------------------
Rectangle {
id: card
x: root.cardOrigin.x
y: root.cardOrigin.y
width: root.contentWidth
height: root.contentHeight
color: Color.popups.background
border.color: Color.popups.border
border.width: 2
radius: 0
opacity: root.open ? 1.0 : 0
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
// Swallow clicks on the card so they don't bubble to the dismissal
// MouseArea behind us.
MouseArea { anchors.fill: parent }
Item {
id: contentHolder
anchors.fill: parent
anchors.margins: root.padding
}
}
}
@@ -1,64 +0,0 @@
import QtQuick
// Small (22×22 by default) icon button used at the right edge of panel rows
// for inline actions — forget network, confirm passphrase, unpair device,
// etc. Two visual modes are supported via `hoverColor`:
// - default: hoverColor === foreground → subtle foreground-tint hover
// - urgent: hoverColor === bar.urgent → red-tint hover for destructive
// actions like forget/unpair
//
// `enabled` gates clicks and dims the icon. The component owns its own
// hover state visuals; mouse hover does NOT update any panel cursor state
// here because action buttons are not cursor targets — the row they live
// in is.
Rectangle {
id: root
property string iconText: ""
property string tooltipText: ""
property color foreground: "#cacccc"
property color hoverColor: foreground
property color panelBackground: "#101315"
property string fontFamily: "JetBrainsMono Nerd Font"
property real fontSize: 14
property real size: 22
signal clicked()
implicitWidth: size
implicitHeight: size
radius: 4
color: mouse.containsMouse && root.enabled
? Qt.rgba(hoverColor.r, hoverColor.g, hoverColor.b, 0.20)
: "transparent"
Behavior on color { ColorAnimation { duration: 60 } }
Text {
anchors.centerIn: parent
text: root.iconText
color: root.enabled
? (mouse.containsMouse ? root.hoverColor : Qt.darker(root.foreground, 1.3))
: Qt.darker(root.foreground, 2.0)
font.family: root.fontFamily
font.pixelSize: root.fontSize
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: root.enabled
onClicked: root.clicked()
}
PanelToolTip {
visible: root.tooltipText !== "" && mouse.containsMouse
text: root.tooltipText
panelForeground: root.foreground
panelBackground: root.panelBackground
fontFamily: root.fontFamily
}
}
@@ -1,17 +0,0 @@
import QtQuick
// Small-caps-style label that introduces a panel section ("DNS provider",
// "Wi-Fi networks", "Output device", "Paired devices"). Sits between a
// PanelSeparator and the content rows.
Text {
id: root
property color foreground: "#cacccc"
property string fontFamily: "JetBrainsMono Nerd Font"
property real fontSize: 10
color: Qt.darker(foreground, 1.4)
font.family: fontFamily
font.pixelSize: fontSize
font.bold: true
}
@@ -1,17 +0,0 @@
import QtQuick
// 1px horizontal divider for panel sections. The alpha-on-foreground tint
// keeps the rule legible against the panel background without competing
// with text or borders.
Rectangle {
id: root
property color foreground: "#cacccc"
property real strength: 0.12
width: parent ? parent.width : implicitWidth
implicitWidth: 100
implicitHeight: 1
height: 1
color: Qt.rgba(foreground.r, foreground.g, foreground.b, strength)
}
@@ -1,45 +0,0 @@
import QtQuick
import QtQuick.Controls
// Styled wrapper around Qt Quick Controls ToolTip. Drop-in: declare inside
// the hovered item and bind `visible` to the hover state, e.g.
// Common.PanelToolTip {
// visible: mouse.containsMouse
// text: "Forget network"
// panelForeground: bar.foreground
// panelBackground: bar.background
// fontFamily: bar.fontFamily
// }
//
// Property names are prefixed `panel*` to avoid clashing with ToolTip's
// built-in `background`/`font` properties.
ToolTip {
id: root
property color panelForeground: "#cacccc"
property color panelBackground: "#101315"
property string fontFamily: "JetBrainsMono Nerd Font"
property real fontSize: 11
delay: 400
padding: 0
background: Rectangle {
color: root.panelBackground
border.color: root.panelForeground
border.width: 1
radius: 0
opacity: 0.97
}
contentItem: Text {
text: root.text
color: root.panelForeground
font.family: root.fontFamily
font.pixelSize: root.fontSize
leftPadding: 10
rightPadding: 10
topPadding: 6
bottomPadding: 6
}
}
@@ -1,116 +0,0 @@
import QtQuick
import QtQuick.Controls
Rectangle {
id: root
property string text: ""
property string iconText: ""
property string tooltipText: ""
property color foreground: "#cacccc"
property color background: "transparent"
property color hoverBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.12)
property color pressedBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.22)
property color tooltipBackground: "#101315"
property color tooltipForeground: foreground
property string fontFamily: "JetBrainsMono Nerd Font"
property real fontSize: 12
property real iconSize: 14
property real horizontalPadding: 10
property real verticalPadding: 6
property bool active: false
property bool leftAlign: false
property color activeBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18)
// Keyboard cursor flag. When true, the pill renders the same fill + border
// as a mouse hover — so j/k navigation and pointer hover are visually
// indistinguishable. Default false; bind from a panel's cursor state.
property bool hasCursor: false
ToolTip {
visible: root.tooltipText !== "" && mouseArea.containsMouse
text: root.tooltipText
delay: 400
padding: 0
background: Rectangle {
color: root.tooltipBackground
border.color: root.tooltipForeground
border.width: 1
radius: 0
opacity: 0.97
}
contentItem: Text {
text: root.tooltipText
color: root.tooltipForeground
font.family: root.fontFamily
font.pixelSize: 11
leftPadding: 10
rightPadding: 10
topPadding: 6
bottomPadding: 6
}
}
signal clicked()
signal rightClicked()
implicitWidth: row.implicitWidth + horizontalPadding * 2
implicitHeight: row.implicitHeight + verticalPadding * 2
radius: 4
// Hot = mouse hover OR keyboard cursor. Pressed wins over both; otherwise
// hot beats `active` (so a cursor on an already-active pill still reads
// as hovered, matching CursorSurface's behaviour for navigable rows).
readonly property bool hot: mouseArea.containsMouse || hasCursor
color: mouseArea.pressed ? pressedBackground
: hot ? hoverBackground
: (active ? activeBackground : background)
border.width: hot ? 1 : 0
border.color: foreground
Behavior on color {
ColorAnimation { duration: 120 }
}
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
anchors.left: root.leftAlign ? parent.left : undefined
anchors.leftMargin: root.leftAlign ? root.horizontalPadding : 0
anchors.horizontalCenter: root.leftAlign ? undefined : parent.horizontalCenter
spacing: 8
Text {
visible: root.iconText !== ""
text: root.iconText
color: root.foreground
font.family: root.fontFamily
font.pixelSize: root.iconSize
anchors.verticalCenter: parent.verticalCenter
}
Text {
visible: root.text !== ""
text: root.text
color: root.foreground
font.family: root.fontFamily
font.pixelSize: root.fontSize
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) root.rightClicked()
else root.clicked()
}
}
}
@@ -1,139 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import qs.Commons
PopupWindow {
id: root
required property Item anchorItem
required property QtObject bar
property var owner: null
property int margin: 10
property int padding: 14
property int contentWidth: 280
property int contentHeight: 200
property bool open: false
property bool centerOnBar: false
// "click" — uses HyprlandFocusGrab so clicking outside dismisses the popup.
// "hover" — passive overlay; the owning widget controls open via hover.
property string triggerMode: "click"
readonly property var coordinatorKey: owner || root
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
readonly property bool containsMouse: cardHover.hovered
function closePopout() {
if (owner && "closePopout" in owner) owner.closePopout()
else root.open = false
}
default property alias contentItem: contentHolder.children
visible: open || card.opacity > 0
color: "transparent"
implicitWidth: contentWidth
implicitHeight: contentHeight
onOpenChanged: {
if (!bar) return
if (open) bar.requestPopout(coordinatorKey)
else if (bar.activePopout === coordinatorKey) bar.releasePopout(coordinatorKey)
}
// Outside-click dismissal via Hyprland's focus grab. While `active`, input
// is routed only to the listed windows; clicking anywhere else clears the
// grab and we close the popup. Skipped for hover-mode popups so the cursor
// can move freely between the trigger and the popup.
HyprlandFocusGrab {
active: root.open && root.triggerMode === "click"
windows: root.anchorWindow ? [root, root.anchorWindow] : [root]
onCleared: root.closePopout()
}
anchor {
id: popupAnchor
window: anchorItem ? anchorItem.QsWindow.window : null
adjustment: PopupAdjustment.Slide
edges: Edges.Top | Edges.Left
gravity: Edges.Bottom | Edges.Right
rect.width: 1
rect.height: 1
onAnchoring: {
if (!root.anchorItem || !root.bar) return
var target = root.anchorItem
var popupWidth = root.implicitWidth
var popupHeight = root.implicitHeight
var localX = target.width / 2 - popupWidth / 2
var localY = target.height + root.margin
if (root.bar.position === "bottom") {
localY = -popupHeight - root.margin
} else if (root.bar.position === "left") {
localX = target.width + root.margin
localY = target.height / 2 - popupHeight / 2
} else if (root.bar.position === "right") {
localX = -popupWidth - root.margin
localY = target.height / 2 - popupHeight / 2
}
var window = target.QsWindow.window
if (!window) return
if (root.centerOnBar) {
var cx = 0;
var cy = 0;
if (root.bar.position === "top" || root.bar.position === "bottom") {
cx = window.width / 2 - popupWidth / 2
cy = root.bar.position === "bottom" ? -popupHeight - root.margin : window.height + root.margin
cx = Math.max(root.margin, Math.min(cx, window.width - popupWidth - root.margin))
} else {
cx = root.bar.position === "left" ? window.width + root.margin : -popupWidth - root.margin
cy = window.height / 2 - popupHeight / 2
cy = Math.max(root.margin, Math.min(cy, window.height - popupHeight - root.margin))
}
popupAnchor.rect.x = Math.round(cx)
popupAnchor.rect.y = Math.round(cy)
return
}
var point = window.contentItem.mapFromItem(target, localX, localY)
if (root.bar.position === "top" || root.bar.position === "bottom") {
point.x = Math.max(root.margin, Math.min(point.x, window.width - popupWidth - root.margin))
} else {
point.y = Math.max(root.margin, Math.min(point.y, window.height - popupHeight - root.margin))
}
popupAnchor.rect.x = Math.round(point.x)
popupAnchor.rect.y = Math.round(point.y)
}
}
Rectangle {
id: card
anchors.fill: parent
color: Color.popups.background
border.color: Color.popups.border
border.width: 2
radius: 0
opacity: root.open ? 1.0 : 0
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
Item {
id: contentHolder
anchors.fill: parent
anchors.margins: root.padding
}
HoverHandler {
id: cardHover
}
}
}
@@ -1,117 +0,0 @@
import QtQuick
Item {
id: root
property QtObject bar: null
property real value: 0
property real minimum: 0
property real maximum: 1
property real step: 0.05
property bool integer: false
property color trackColor: bar ? Qt.rgba(bar.foreground.r, bar.foreground.g, bar.foreground.b, 0.18) : "#333"
property color fillColor: bar ? bar.foreground : "#cacccc"
property color knobColor: bar ? bar.foreground : "#cacccc"
property bool dragging: false
property real trackHeight: 4
property real liveValue: value
onValueChanged: if (!dragging) liveValue = value
signal moved(real value)
signal released(real value)
implicitWidth: 200
implicitHeight: 22
readonly property real range: Math.max(0.0001, maximum - minimum)
readonly property real progress: Math.max(0, Math.min(1, (liveValue - minimum) / range))
Rectangle {
id: track
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
height: root.trackHeight
radius: height / 2
color: root.trackColor
}
Rectangle {
id: fill
anchors.verticalCenter: track.verticalCenter
anchors.left: track.left
height: track.height
radius: track.radius
color: root.fillColor
width: track.width * root.progress
Behavior on width {
enabled: !root.dragging
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
}
Rectangle {
id: knob
width: 14
height: 14
radius: 7
color: root.knobColor
border.color: root.bar ? root.bar.background : "#101315"
border.width: 2
anchors.verticalCenter: track.verticalCenter
x: Math.max(0, Math.min(track.width - width, track.width * root.progress - width / 2))
scale: mouseArea.containsMouse || root.dragging ? 1.15 : 1.0
Behavior on x {
enabled: !root.dragging
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
Behavior on scale {
NumberAnimation { duration: 110; easing.type: Easing.OutCubic }
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
function valueFromX(x) {
var clamped = Math.max(0, Math.min(track.width, x))
var raw = root.minimum + (clamped / track.width) * root.range
if (root.integer) raw = Math.round(raw)
return Math.max(root.minimum, Math.min(root.maximum, raw))
}
onPressed: function(mouse) {
root.dragging = true
var next = valueFromX(mouse.x)
root.liveValue = next
root.moved(next)
}
onPositionChanged: function(mouse) {
if (!root.dragging) return
var next = valueFromX(mouse.x)
root.liveValue = next
root.moved(next)
}
onReleased: function(mouse) {
root.dragging = false
root.released(root.liveValue)
root.liveValue = root.value
}
onWheel: function(wheel) {
var delta = wheel.angleDelta.y > 0 ? root.step : -root.step
var next = Math.max(root.minimum, Math.min(root.maximum, root.liveValue + delta))
if (root.integer) next = Math.round(next)
root.liveValue = next
root.moved(next)
root.released(next)
}
}
}
@@ -1,69 +0,0 @@
import QtQuick
Item {
id: root
property var bar: null
property string text: ""
property string fontFamily: bar ? bar.fontFamily : "JetBrainsMono Nerd Font"
property real fontSize: 12
property color foreground: bar ? bar.foreground : "#cacccc"
property color activeColor: bar ? bar.urgent : "#a55555"
property bool active: false
property real horizontalMargin: 8.5
property real rightExtraMargin: 0
property real verticalPadding: 6
property real fixedWidth: -1
property real fixedHeight: -1
property real textRotation: 0
property bool keepSpace: false
property string tooltipText: ""
signal pressed(int button)
signal wheelMoved(int delta)
readonly property bool vertical: bar ? bar.vertical : false
readonly property int barSize: bar ? bar.barSize : 26
visible: text !== "" || keepSpace
opacity: text === "" ? 0 : 1
implicitWidth: fixedWidth > 0 ? fixedWidth : (vertical ? barSize : Math.max(12, label.implicitWidth + horizontalMargin * 2 + rightExtraMargin))
implicitHeight: fixedHeight > 0 ? fixedHeight : (vertical ? Math.max(12, label.implicitHeight + verticalPadding * 2) : barSize)
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
Text {
id: label
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.vertical ? 0 : -root.rightExtraMargin / 2
text: root.text
color: root.active ? root.activeColor : root.foreground
font.family: root.fontFamily
font.pointSize: root.fontSize * 0.75
renderType: Text.NativeRendering
rotation: root.textRotation
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Behavior on color {
ColorAnimation { duration: 160 }
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: if (root.bar) root.bar.showTooltip(root, root.tooltipText)
onExited: if (root.bar) root.bar.hideTooltip(root)
onClicked: function(mouse) {
if (root.bar) root.bar.hideTooltip(root)
root.pressed(mouse.button)
}
onWheel: function(wheel) { root.wheelMoved(wheel.angleDelta.y) }
}
}
@@ -3,7 +3,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -90,7 +90,7 @@ Item {
// selectedIndex semantics within a section:
// -1 → on the slider row (h/l adjusts volume, m/Enter mute)
// 0..N-1 → on the Nth device/stream row
// Visuals derive from hasCursor/current via Common.CursorSurface, never
// Visuals derive from hasCursor/current via CursorSurface, never
// from containsMouse — that's what keeps the highlight unique across
// keyboard + mouse like wifi does.
property string focusSection: "output"
@@ -348,7 +348,7 @@ Item {
function hide(): void { root.closePopout() }
}
Common.WidgetButton {
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
@@ -366,7 +366,7 @@ Item {
}
}
Common.KeyboardPanel {
KeyboardPanel {
id: panel
anchorItem: button
owner: root
@@ -440,7 +440,7 @@ Item {
width: parent.width
spacing: 8
Common.PanelSectionHeader {
PanelSectionHeader {
text: "Output"
foreground: root.bar.foreground
fontFamily: root.bar.fontFamily
@@ -462,7 +462,7 @@ Item {
// Output slider row — itself a cursor target (selectedIndex === -1
// when focusSection === "output"). h/l adjust the value via
// root.adjustVolume; m / Enter toggle mute.
Common.CursorSurface {
CursorSurface {
id: outputSliderRow
width: parent.width
height: outputSliderInner.implicitHeight + 8
@@ -496,7 +496,7 @@ Item {
}
}
Common.Slider {
PanelSlider {
id: outputSlider
bar: root.bar
width: parent.width - outputIconText.width - outputPercent.width - 16
@@ -559,7 +559,7 @@ Item {
width: parent.width
spacing: 8
Common.PanelSectionHeader {
PanelSectionHeader {
text: "Input"
foreground: root.bar.foreground
fontFamily: root.bar.fontFamily
@@ -578,7 +578,7 @@ Item {
}
}
Common.CursorSurface {
CursorSurface {
id: inputSliderRow
visible: !!root.source
width: parent.width
@@ -613,7 +613,7 @@ Item {
}
}
Common.Slider {
PanelSlider {
id: inputSlider
bar: root.bar
width: parent.width - inputIconText.width - inputPercent.width - 16
@@ -672,7 +672,7 @@ Item {
spacing: 6
visible: root.audioStreams.length > 0
Common.PanelSectionHeader {
PanelSectionHeader {
text: "Playing"
foreground: root.bar.foreground
fontFamily: root.bar.fontFamily
@@ -701,7 +701,7 @@ Item {
// Output device row — cursor target inside the "output" section. Mouse
// hover updates the panel cursor at the root; visuals come entirely
// from hasCursor/current via CursorSurface, never from containsMouse.
component SinkRow: Common.CursorSurface {
component SinkRow: CursorSurface {
id: sinkRow
required property var node
required property int rowIndex
@@ -767,7 +767,7 @@ Item {
}
// Input device row — sibling of SinkRow for the "input" section.
component SourceRow: Common.CursorSurface {
component SourceRow: CursorSurface {
id: sourceRow
required property var node
required property int rowIndex
@@ -836,7 +836,7 @@ Item {
// The stream has its own slider inline, so h/l from the keyboard
// adjusts THIS stream's volume (not the global output) when the cursor
// sits on this row. Enter/Space mutes the stream.
component StreamRow: Common.CursorSurface {
component StreamRow: CursorSurface {
id: streamRow
required property var node
required property int rowIndex
@@ -906,7 +906,7 @@ Item {
}
}
Common.Slider {
PanelSlider {
bar: root.bar
width: parent.width
minimum: 0
@@ -3,7 +3,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Bluetooth
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -71,7 +71,7 @@ Item {
// them, Enter activates.
// "known" — paired/known device rows; Enter toggles connect.
// "discovered" — unpaired devices visible while scanning; Enter pairs.
// Visuals always come from Common.CursorSurface (hasCursor / current),
// Visuals always come from CursorSurface (hasCursor / current),
// never from containsMouse. Mouse hover updates root cursor state too,
// guaranteeing one highlight on screen.
property string focusSection: "header"
@@ -310,7 +310,7 @@ Item {
function hide(): void { root.closePopout() }
}
Common.WidgetButton {
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
@@ -322,7 +322,7 @@ Item {
}
}
Common.KeyboardPanel {
KeyboardPanel {
id: panel
anchorItem: button
owner: root
@@ -445,7 +445,7 @@ Item {
}
// Discovered (unpaired) devices, only shown while scanning.
Common.PanelSectionHeader {
PanelSectionHeader {
visible: root.adapter && root.adapter.discovering && root.discoveredDevices.length > 0
text: "Discovered"
foreground: root.bar.foreground
@@ -483,11 +483,11 @@ Item {
}
}
// Header pill — wraps Common.PillButton with cursor-state binding so it
// Header pill — wraps PillButton with cursor-state binding so it
// participates in the same single-cursor model as device rows. We don't
// use Common.CursorSurface here because the pill already provides its
// use CursorSurface here because the pill already provides its
// own active/hover visuals and we want them to layer correctly.
component HeaderPill: Common.PillButton {
component HeaderPill: PillButton {
id: pill
required property int pillIndex
property bool pillEnabled: true
@@ -528,7 +528,7 @@ Item {
// connect that drops back to Disconnected within 10s surfaces as "Failed".
// Now a cursor target: hasCursor binds to root state, mouse hover updates
// root state. The X button on the right is a PanelActionButton.
component DeviceRow: Common.CursorSurface {
component DeviceRow: CursorSurface {
id: row
required property var dev
required property int rowIndex
@@ -660,7 +660,7 @@ Item {
// Explicit close button on any known device. Action depends on state:
// connected -> disconnect, otherwise -> forget the pairing entirely.
Common.PanelActionButton {
PanelActionButton {
id: disconnectBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -1,7 +1,7 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -57,7 +57,7 @@ Item {
onDateChanged: root.now = clockTimer.date
}
Common.WidgetButton {
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
@@ -76,7 +76,7 @@ Item {
}
}
Common.PopupCard {
PopupCard {
id: popup
anchorItem: button
bar: root.bar
@@ -94,7 +94,7 @@ Item {
width: parent.width
implicitHeight: 28
Common.PillButton {
PillButton {
id: prevButton
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
@@ -114,7 +114,7 @@ Item {
font.bold: true
}
Common.PillButton {
PillButton {
id: nextButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -1,7 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -53,7 +53,7 @@ Item {
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
Common.WidgetButton {
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
@@ -2,7 +2,7 @@ import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -71,7 +71,7 @@ Item {
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
Common.WidgetButton {
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
@@ -1,7 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -113,7 +113,7 @@ Item {
onExited: if (root.bar) root.bar.hideTooltip(root)
}
Common.PopupCard {
PopupCard {
id: popup
anchorItem: root
bar: root.bar
@@ -198,7 +198,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 6
Common.PillButton {
PillButton {
iconText: "󰒮"
foreground: root.bar.foreground
horizontalPadding: 10
@@ -208,7 +208,7 @@ Item {
onClicked: if (root.activePlayer) root.activePlayer.previous()
}
Common.PillButton {
PillButton {
iconText: root.activePlayer && root.activePlayer.isPlaying ? "󰏤" : "󰐊"
foreground: root.bar.foreground
horizontalPadding: 14
@@ -219,7 +219,7 @@ Item {
onClicked: if (root.activePlayer) root.activePlayer.togglePlaying()
}
Common.PillButton {
PillButton {
iconText: "󰒭"
foreground: root.bar.foreground
horizontalPadding: 10
@@ -1,7 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -36,7 +36,7 @@ Item {
PwObjectTracker { objects: root.source ? [root.source] : [] }
Common.WidgetButton {
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -174,7 +174,7 @@ Item {
onRunningChanged: if (!running) root.refresh()
}
Common.WidgetButton {
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
@@ -186,7 +186,7 @@ Item {
}
}
Common.PopupCard {
PopupCard {
anchorItem: button
owner: root
bar: root.bar
@@ -222,7 +222,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
}
Common.Slider {
PanelSlider {
id: brightnessSlider
bar: root.bar
width: parent.width - 22 - brightnessPercent.width - 16
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -66,7 +66,7 @@ Item {
// Single cursor model: exactly one highlighted spot across the whole
// panel, located via `focusSection` + (`selectedIndex` | `dnsIndex`).
// Mouse hover and keyboard nav both mutate this state at the root; items
// never read containsMouse for visuals. See Common.CursorSurface for the
// never read containsMouse for visuals. See CursorSurface for the
// shared chrome (fill / border) shared by NetworkRow and DnsProviderPill.
readonly property color activeFill: bar ? Qt.rgba(bar.foreground.r, bar.foreground.g, bar.foreground.b, 0.18) : "transparent"
@@ -518,7 +518,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
}
}
Common.WidgetButton {
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
@@ -535,12 +535,12 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
}
// Keyboard-driven popup anchored to the bar widget icon. The shared
// Common.KeyboardPanel handles the layer-shell PanelWindow scaffolding
// KeyboardPanel handles the layer-shell PanelWindow scaffolding
// (Exclusive focus on map, screen binding, anchored-to-icon positioning,
// outside-click via an overlay MouseArea + Region mask that lets the bar
// remain clickable, fade animation, popout coordination). What stays
// here is the wifi-specific UI inside.
Common.KeyboardPanel {
KeyboardPanel {
id: panel
anchorItem: button
owner: root
@@ -664,7 +664,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
}
}
Common.PillButton {
PillButton {
id: refreshBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -681,7 +681,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
}
}
Common.PanelSeparator {
PanelSeparator {
visible: !!root.info.iface
foreground: root.bar.foreground
}
@@ -776,7 +776,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
}
// DNS provider selection.
Common.PanelSeparator {
PanelSeparator {
foreground: root.bar.foreground
}
@@ -784,7 +784,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
width: parent.width
spacing: 8
Common.PanelSectionHeader {
PanelSectionHeader {
text: "DNS provider"
foreground: root.bar.foreground
fontFamily: root.bar.fontFamily
@@ -825,12 +825,12 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
}
// Wi-Fi networks (only if a Wi-Fi station is available).
Common.PanelSeparator {
PanelSeparator {
visible: root.wifiStationAvailable
foreground: root.bar.foreground
}
Common.PanelSectionHeader {
PanelSectionHeader {
visible: root.wifiStationAvailable
text: root.scanning ? "Scanning Wi-Fi…" : "Wi-Fi networks"
foreground: root.bar.foreground
@@ -882,7 +882,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
// One DNS provider pill. The cursor + current visuals come entirely from
// CursorSurface; this component just binds them to the panel's cursor
// state and renders the label/tooltip/click target.
component DnsProviderPill: Common.CursorSurface {
component DnsProviderPill: CursorSurface {
id: pill
required property string provider
required property int index
@@ -921,7 +921,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
onClicked: pill.clicked()
}
Common.PanelToolTip {
PanelToolTip {
visible: pill.tooltipText !== "" && pillMouse.containsMouse
text: pill.tooltipText
panelForeground: root.bar.foreground
@@ -934,7 +934,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
// expands inline to a passphrase prompt when the user picks a protected
// network we don't have credentials for. Clicking a connected row
// disconnects; the X button on a connected row forgets the network.
component NetworkRow: Common.CursorSurface {
component NetworkRow: CursorSurface {
id: row
required property var net
required property int index
@@ -1042,7 +1042,7 @@ iwctl known-networks list 2>/dev/null \\
anchors.verticalCenter: parent.verticalCenter
}
Common.PanelActionButton {
PanelActionButton {
id: forgetBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -1167,7 +1167,7 @@ iwctl known-networks list 2>/dev/null \\
// 22×22 right-anchored to line up with forgetBtn and lockIndicator
// above. Esc closes the prompt (handled by pwField.Keys.onEscapePressed)
// so there's no separate cancel button.
Common.PanelActionButton {
PanelActionButton {
id: connectPwBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -72,7 +72,7 @@ Item {
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
Common.WidgetButton {
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
@@ -103,7 +103,7 @@ Item {
}
}
Common.PopupCard {
PopupCard {
id: popup
anchorItem: button
bar: root.bar
@@ -1,7 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -147,7 +147,7 @@ Item {
onButtonHoveredChanged: buttonHovered ? showPopup() : scheduleHide()
onPopupHoveredChanged: popupHovered ? hideTimer.stop() : scheduleHide()
Common.WidgetButton {
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
@@ -169,7 +169,7 @@ Item {
onHoveredChanged: root.buttonHovered = hovered
}
Common.PopupCard {
PopupCard {
id: popup
anchorItem: button
owner: root
@@ -1,7 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "../common" as Common
import qs.Ui
Item {
id: root
@@ -279,7 +279,7 @@ Item {
onTriggered: root.refresh()
}
Common.WidgetButton {
WidgetButton {
id: button
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
@@ -305,7 +305,7 @@ Item {
}
}
Common.PopupCard {
PopupCard {
id: popup
anchorItem: button
owner: root
@@ -1,7 +1,7 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import "../bar/common" as Common
import qs.Ui
import qs.Commons
// Visual reference + live playground for omarchy-shell's common UI
@@ -124,7 +124,7 @@ Item {
font.bold: true
}
Text {
text: "Live previews of every component in plugins/bar/common/. Use this as the visual reference when porting panels or extracting new primitives. Press h/l to walk the cursor demo, Esc to close."
text: "Live previews of every type exported from qs.Ui. Use this as the visual reference when porting panels or building plugins. Press h/l to walk the cursor demo, Esc to close."
color: Qt.darker(root.foreground, 1.4)
font.family: root.fontFamily
font.pixelSize: 11
@@ -133,7 +133,7 @@ Item {
}
}
Common.PanelSeparator { foreground: root.foreground }
PanelSeparator { foreground: root.foreground }
// ---- PanelSectionHeader ------------------------------------------
Column {
@@ -171,17 +171,17 @@ Item {
anchors.rightMargin: 14
spacing: 6
Common.PanelSectionHeader {
PanelSectionHeader {
text: "DNS provider"
foreground: root.foreground
fontFamily: root.fontFamily
}
Common.PanelSectionHeader {
PanelSectionHeader {
text: "Wi-Fi networks"
foreground: root.foreground
fontFamily: root.fontFamily
}
Common.PanelSectionHeader {
PanelSectionHeader {
text: "Playing"
foreground: root.foreground
fontFamily: root.fontFamily
@@ -227,9 +227,9 @@ Item {
anchors.rightMargin: 14
spacing: 12
Common.PanelSeparator { foreground: root.foreground }
Common.PanelSeparator { foreground: root.foreground; strength: 0.25 }
Common.PanelSeparator { foreground: root.foreground; strength: 0.45 }
PanelSeparator { foreground: root.foreground }
PanelSeparator { foreground: root.foreground; strength: 0.25 }
PanelSeparator { foreground: root.foreground; strength: 0.45 }
}
}
}
@@ -279,7 +279,7 @@ Item {
{ "label": "Forget / scan / disabled-ish row" }
]
Common.CursorSurface {
CursorSurface {
required property var modelData
required property int index
width: parent.width
@@ -351,7 +351,7 @@ Item {
anchors.leftMargin: 14
spacing: 6
Common.PillButton {
PillButton {
text: "DHCP"
tooltipText: "Use DNS from DHCP"
tooltipBackground: root.background
@@ -360,7 +360,7 @@ Item {
fontFamily: root.fontFamily
}
Common.PillButton {
PillButton {
text: "Cloudflare"
tooltipText: "Set DNS to Cloudflare"
tooltipBackground: root.background
@@ -370,7 +370,7 @@ Item {
active: true
}
Common.PillButton {
PillButton {
iconText: "󰑐"
tooltipText: "Refresh"
tooltipBackground: root.background
@@ -381,7 +381,7 @@ Item {
verticalPadding: 4
}
Common.PillButton {
PillButton {
iconText: "󰂯"
text: "On"
tooltipText: "Turn Bluetooth off"
@@ -431,7 +431,7 @@ Item {
anchors.leftMargin: 14
spacing: 18
Common.PanelActionButton {
PanelActionButton {
iconText: "󰄬"
tooltipText: "Confirm (default flavor)"
foreground: root.foreground
@@ -439,7 +439,7 @@ Item {
fontFamily: root.fontFamily
}
Common.PanelActionButton {
PanelActionButton {
iconText: "󰅙"
tooltipText: "Forget network (urgent flavor)"
foreground: root.foreground
@@ -448,7 +448,7 @@ Item {
fontFamily: root.fontFamily
}
Common.PanelActionButton {
PanelActionButton {
iconText: "󰄬"
tooltipText: "Disabled — type a passphrase first"
foreground: root.foreground
@@ -504,7 +504,7 @@ Item {
cursorShape: Qt.PointingHandCursor
}
Common.PanelToolTip {
PanelToolTip {
visible: tipMouse.containsMouse
text: "Styled tooltip — drop into any panel"
panelForeground: root.foreground
@@ -563,7 +563,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
}
Common.Slider {
PanelSlider {
id: demoSlider
bar: root.fakeBar
width: parent.width - 70
@@ -623,13 +623,13 @@ Item {
anchors.rightMargin: 14
spacing: 6
Common.PanelSectionHeader {
PanelSectionHeader {
text: "Wi-Fi networks"
foreground: root.foreground
fontFamily: root.fontFamily
}
Common.CursorSurface {
CursorSurface {
width: parent.width
implicitHeight: composedRow.implicitHeight + 12
current: true
@@ -655,7 +655,7 @@ Item {
font.pixelSize: 14
}
Common.PanelActionButton {
PanelActionButton {
id: composedForget
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -695,9 +695,9 @@ Item {
}
}
Common.PanelSeparator { foreground: root.foreground }
PanelSeparator { foreground: root.foreground }
Common.CursorSurface {
CursorSurface {
width: parent.width
implicitHeight: idleRow.implicitHeight + 12
foreground: root.foreground