pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Wayland PanelWindow { id: root required property Theme theme required property Niri niriService required property Time timeService required property Resources resourceService required property Notifications notificationService required property Tray trayService required property Media mediaService required property Clipboard clipboardService required property Vault vaultService required property Launcher launcherService required property Audio audioService required property Brightness brightnessService required property Network networkService required property Bluetooth bluetoothService required property Power powerService required property Privilege privilegeService required property Idle idleService required property Lock lockService required property Noko nokoService required property SurfaceManager surfaces required property Usage usageService required property CalDav caldavService required property Format formatService required property string orientation readonly property bool vertical: orientation === "vertical" function chipLabel(name) { switch (String(name)) { case "clock": return root.timeService ? root.timeService.compact : "--:--"; case "launcher": return "launch"; case "media": return "media"; case "noko": return root.nokoService ? root.nokoService.label : "noko"; case "system": return "system"; case "clipboard": return "clip"; case "vault": return "vault"; case "audio": return root.audioService ? root.audioService.privacyLabel().toLowerCase() : "audio"; case "brightness": return "bright"; case "connectivity": return "network"; case "power": return "power"; case "notifications": return "notify"; case "tray": return root.trayService && root.trayService.attentionCount > 0 ? `tray ${root.trayService.attentionCount}` : "tray"; case "idle": return "idle"; } return String(name); } // "something active inside" — drives a super subtle active dot on the chip function chipActive(name) { switch (String(name)) { case "media": return !!(root.mediaService && root.mediaService.playing); case "noko": return !!(root.nokoService && root.nokoService.active); case "notifications": return !!(root.notificationService && root.notificationService.unreadCount > 0); case "tray": return !!(root.trayService && root.trayService.attentionCount > 0); case "idle": return !!(root.idleService && root.idleService.inhibited); case "audio": return !!(root.audioService && root.audioService.privacyActive()); case "vault": return !!(root.vaultService && root.vaultService.unlocked); } return false; } // idle (keep-awake) is important enough to pop louder than a subtle dot function chipActiveStrong(name) { const key = String(name); if (key === "noko") return !!(root.nokoService && root.nokoService.strong); return key === "idle" && root.chipActive(name); } function chipRole(name) { if (String(name) === "noko" && root.nokoService) return root.nokoService.role; return "ui.accent"; } function itemRectInBar(item) { let x = 0; let y = 0; let cursor = item; while (cursor && cursor !== root.contentItem) { x += cursor.x; y += cursor.y; cursor = cursor.parent; } return { "x": x, "y": y, "width": item.width, "height": item.height }; } function registerAnchor(name, item) { // Skip hidden items: the horizontal clock chip and the vertical clock // stack share the "clock" key, so only the visible one must register. if (!root.surfaces || !item || !item.visible || item.width <= 0 || item.height <= 0) return; const rect = itemRectInBar(item); root.surfaces.registerAnchor(name, `${Math.round(rect.x)},${Math.round(rect.y)},${Math.round(rect.width)},${Math.round(rect.height)}`); } function activatePopout(name) { if (!root.surfaces) return; const target = String(name); if (root.surfaces.popoutOpen && root.surfaces.popoutName === target) { root.surfaces.closePopout("bar-toggle"); } else { if (root.usageService) root.usageService.record(target); if (target === "vault" && root.vaultService) root.vaultService.prepareOpen(root.niriService ? root.niriService.focusedAppId : ""); root.surfaces.openRegisteredPopout(target); } } function refreshSurfaceGeometry() { if (root.surfaces) { const thickness = Math.round(root.vertical ? root.width : root.height); root.surfaces.setPopoutScreen(Math.round(root.screen ? root.screen.width : root.width), Math.round(root.screen ? root.screen.height : root.height), thickness, root.orientation); } } // horizontal: width spans (left+right anchored), implicitHeight sets bar // height. vertical: height spans (top+bottom anchored), implicitWidth sets // bar thickness from the widest chip content (never a magic constant). implicitHeight: 34 implicitWidth: root.vertical ? (Math.max(34, widgetsRow.implicitWidth, clockStack.width, workspaceStrip.implicitWidth) + 12) : root.width color: "transparent" WlrLayershell.namespace: "nuguland-bar" WlrLayershell.layer: WlrLayer.Top WlrLayershell.keyboardFocus: WlrKeyboardFocus.None Component.onCompleted: Qt.callLater(root.refreshSurfaceGeometry) onSurfacesChanged: Qt.callLater(root.refreshSurfaceGeometry) onWidthChanged: root.refreshSurfaceGeometry() onHeightChanged: root.refreshSurfaceGeometry() anchors { top: true left: true right: !root.vertical bottom: root.vertical } Rectangle { id: barContent anchors.fill: parent color: "transparent" // accent edge: bottom hairline (horizontal) / right hairline (vertical) Rectangle { x: root.vertical ? parent.width - 1 : 0 y: root.vertical ? 0 : parent.height - 1 width: root.vertical ? 1 : parent.width height: root.vertical ? parent.height : 1 color: root.theme ? root.theme.withAlpha(root.theme.ui.accent, 0.55) : "transparent" } // Horizontal clock stays at the right edge; widgets sit immediately left. // The vertical clock retains its existing centred placement. NuguClock { id: clockChip stacked: false visible: !root.vertical anchors.right: parent.right anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter } NuguClock { id: clockStack stacked: true visible: root.vertical anchors.horizontalCenter: parent.horizontalCenter y: Math.min(parent.height / 2 - height / 2, widgetsRow.y - height - 12) } // Workspaces fill the space before widgets horizontally, or above the // clock vertically. Each output keeps its own faint tray. GridLayout { id: workspaceStrip // horizontal base: single row from the left edge up to the widgets, // centred vertically. Vertical state swaps to a single column from // the top down to the clock, centred horizontally. columns: root.vertical ? 1 : 99 rowSpacing: 8 columnSpacing: 8 clip: true anchors.left: parent.left anchors.leftMargin: root.vertical ? 0 : 10 anchors.right: widgetsRow.left anchors.rightMargin: root.vertical ? 0 : 12 anchors.verticalCenter: parent.verticalCenter anchors.topMargin: root.vertical ? 10 : 0 anchors.bottomMargin: root.vertical ? 10 : 0 states: State { when: root.vertical AnchorChanges { target: workspaceStrip anchors.left: undefined anchors.right: undefined anchors.verticalCenter: undefined anchors.top: parent.top anchors.bottom: clockStack.top anchors.horizontalCenter: parent.horizontalCenter } } Repeater { model: root.niriService ? root.niriService.workspaceGroups() : [] // Wrap the group in a plain Item: Qt6 won't inject an array // model's modelData into a required prop on a delegate root that // re-declares it for a nested Repeater. The wrapper takes the // injection and hands it to the tray/pills explicitly. delegate: Item { id: groupItem required property var modelData implicitWidth: tray.width implicitHeight: tray.height Layout.preferredWidth: tray.width Layout.preferredHeight: tray.height // pinned to the strip's start edge: left in the horizontal // row, top in the vertical column (no centred-float gap). Layout.alignment: root.vertical ? (Qt.AlignHCenter | Qt.AlignTop) : (Qt.AlignLeft | Qt.AlignVCenter) Rectangle { id: tray anchors.centerIn: parent width: pills.implicitWidth + 12 height: pills.implicitHeight + 12 radius: 11 color: root.theme ? root.theme.withAlpha(root.theme.ui.minor, 0.07) : "transparent" // pills: one output's workspaces. Flow follows the bar — // a horizontal row, or a vertical stack in vertical mode. GridLayout { id: pills anchors.centerIn: parent columns: root.vertical ? 1 : 99 rowSpacing: 4 columnSpacing: 4 Repeater { model: groupItem.modelData.workspaces delegate: Item { id: pillWrap required property var modelData implicitWidth: workspaceChip.width implicitHeight: workspaceChip.height Layout.preferredWidth: workspaceChip.width Layout.preferredHeight: workspaceChip.height Layout.alignment: Qt.AlignCenter Rectangle { id: workspaceChip readonly property bool active: !!pillWrap.modelData.is_focused readonly property string idxStr: String(pillWrap.modelData.shortcut || pillWrap.modelData.idx || pillWrap.modelData.id || "?") readonly property string nameStr: pillWrap.modelData.name ? String(pillWrap.modelData.name).toLowerCase() : "" readonly property string abbrName: root.formatService && workspaceChip.nameStr ? root.formatService.abbreviate(workspaceChip.nameStr) : "" anchors.centerIn: parent width: Math.max(30, workspaceContent.implicitWidth + (root.vertical ? 12 : 16)) height: root.vertical ? (workspaceContent.implicitHeight + 8) : 22 radius: 8 color: root.theme ? (active ? root.theme.withAlpha(root.theme.ui.focus, 0.28) : root.theme.withAlpha(root.theme.ui.minor, 0.1)) : "transparent" border.width: 0 // idx (minor) leads the name (normal): inline in horizontal, stacked in vertical. GridLayout { id: workspaceContent anchors.centerIn: parent columns: root.vertical ? 1 : 2 rowSpacing: 0 columnSpacing: 4 NuguText { Layout.alignment: Qt.AlignHCenter theme: root.theme variant: "meta" text: workspaceChip.idxStr color: !root.theme ? "transparent" : (workspaceChip.active ? root.theme.ui.accent : root.theme.ui.minor) } NuguText { Layout.alignment: Qt.AlignHCenter visible: text.length > 0 theme: root.theme variant: "meta" font.pixelSize: root.vertical ? 10 : 13 text: root.vertical ? workspaceChip.abbrName : workspaceChip.nameStr color: !root.theme ? "transparent" : (workspaceChip.active ? root.theme.ui.accent : root.theme.ui.normal) } } MouseArea { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { if (root.niriService) root.niriService.focusWorkspace(pillWrap.modelData.name || pillWrap.modelData.idx || pillWrap.modelData.id); } } } } } } } } } } // Widgets keep their natural width before the clock horizontally, // or at the bottom edge vertically. GridLayout { id: widgetsRow // horizontal base: single row pinned before the clock, centred // vertically. The vertical state swaps to a single column pinned to // the bottom edge, centred horizontally. AnchorChanges is required // because clearing an anchor with `undefined` does not reset it. columns: root.vertical ? 1 : 99 rowSpacing: 4 columnSpacing: 4 anchors.right: clockChip.left anchors.rightMargin: root.vertical ? 0 : 12 anchors.verticalCenter: parent.verticalCenter anchors.bottomMargin: root.vertical ? 10 : 0 states: State { when: root.vertical AnchorChanges { target: widgetsRow anchors.right: undefined anchors.verticalCenter: undefined anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter } } Repeater { model: root.usageService ? root.usageService.layoutOrder : [] delegate: BarChip { required property var modelData popoutName: modelData label: root.vertical ? root.formatService.abbreviate(root.chipLabel(modelData)) : root.chipLabel(modelData) } } } } PopoutHost { theme: root.theme surfaces: root.surfaces niriService: root.niriService timeService: root.timeService caldavService: root.caldavService resourceService: root.resourceService notificationService: root.notificationService trayService: root.trayService mediaService: root.mediaService clipboardService: root.clipboardService vaultService: root.vaultService launcherService: root.launcherService audioService: root.audioService brightnessService: root.brightnessService networkService: root.networkService bluetoothService: root.bluetoothService powerService: root.powerService privilegeService: root.privilegeService lockService: root.lockService nokoService: root.nokoService barWindow: root } component NuguText: Text { required property Theme theme property string variant: "body" property bool selected: false color: !theme ? "transparent" : (selected ? theme.content.backdrop : (variant === "title" ? theme.ui.important_global : (variant === "meta" ? theme.ui.normal : theme.content.normal))) font.family: variant === "mono" ? "IBM Plex Mono" : "IBM Plex Sans" font.pixelSize: variant === "title" ? 15 : (variant === "meta" ? 13 : 14) font.bold: variant === "title" elide: Text.ElideRight } component OdometerText: Item { id: odometer required property Theme theme property string text: "--:--" property bool selected: false property int digitWidth: 10 property int digitHeight: 18 property int separatorWidth: 5 property string fontFamily: "IBM Plex Mono" property int pixelSize: 13 readonly property string source: "Nuguland Clock.qml ClockNumber.qml" readonly property string motion: "vertical-digit-roll" readonly property bool noGlow: true readonly property bool settingsBacked: false readonly property var hardSeparators: [":"] readonly property var cells: makeCells(text) function makeCells(value) { const result = []; const chars = String(value).split(""); for (let i = 0; i < chars.length; i++) { const ch = chars[i]; const digit = ch >= "0" && ch <= "9"; result.push(digit ? { "kind": "digit", "value": Number(ch) } : { "kind": "separator", "value": ch }); } return result; } function colorForCell(cell) { if (!theme) return "white"; if (odometer.selected) return theme.content.backdrop; return cell.kind === "separator" ? theme.ui.accent : theme.content.normal; } implicitWidth: row.implicitWidth implicitHeight: digitHeight clip: true Row { id: row anchors.centerIn: parent spacing: 0 Repeater { model: odometer.cells delegate: Item { id: cellRoot required property var modelData width: modelData.kind === "digit" ? odometer.digitWidth : odometer.separatorWidth height: odometer.digitHeight clip: true Column { id: digitColumn visible: cellRoot.modelData.kind === "digit" y: cellRoot.modelData.kind === "digit" ? -cellRoot.modelData.value * odometer.digitHeight : 0 Repeater { model: 10 delegate: Text { required property int index width: odometer.digitWidth height: odometer.digitHeight horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: String(index) font.family: odometer.fontFamily font.pixelSize: odometer.pixelSize color: odometer.colorForCell(cellRoot.modelData) } } Behavior on y { NumberAnimation { duration: odometer.theme && odometer.theme.lowPowerMode ? 0 : 220 easing.type: Easing.OutCubic } } } Text { visible: cellRoot.modelData.kind === "separator" anchors.centerIn: parent text: String(cellRoot.modelData.value) font.family: odometer.fontFamily font.pixelSize: odometer.pixelSize color: odometer.colorForCell(cellRoot.modelData) } } } } } component StateLayer: MouseArea { id: layer property Theme theme property bool checked: false property bool disabled: false property bool shapeMorph: true property real hoverOpacity: 0.08 property real pressOpacity: 0.14 property real disabledOpacity: 0.38 property point pressOrigin: Qt.point(width / 2, height / 2) property real rippleRadius: 0 property real cornerRadius: 0 property real endRadius: Math.sqrt(width * width + height * height) * (shapeMorph ? 1.25 : 1) property string sourceMechanic: "Nuguland StateLayer.qml ButtonBase.qml" signal activated function stateLayerContract() { return { "clickablePrimitive": "NuguStateLayer", "source": sourceMechanic, "hoverOpacity": hoverOpacity, "pressOpacity": pressOpacity, "disabledOpacity": disabledOpacity, "rippleRadius": "radial-press-feedback", "pressOrigin": "pointer-local-origin", "shapeMorph": shapeMorph, "keyboardActivation": ["Return", "Space"], "roles": { "checked": "ui.focus", "hover": "ui.accent", "disabled": "ui.minor" } }; } function beginPress(x, y) { pressOrigin = Qt.point(x, y); rippleRadius = 0; rippleAnimation.restart(); } function keyboardActivate() { beginPress(width / 2, height / 2); activated(); } anchors.fill: parent clip: true enabled: !disabled hoverEnabled: true acceptedButtons: Qt.LeftButton cursorShape: disabled ? Qt.ArrowCursor : Qt.PointingHandCursor activeFocusOnTab: !disabled onPressed: event => { return beginPress(event.x, event.y); } onClicked: activated() Keys.onReturnPressed: event => { event.accepted = true; keyboardActivate(); } Keys.onSpacePressed: event => { event.accepted = true; keyboardActivate(); } Rectangle { anchors.fill: parent radius: layer.cornerRadius color: layer.theme ? layer.theme.ui.accent : "white" opacity: layer.disabled ? layer.disabledOpacity : layer.pressed ? layer.pressOpacity : layer.containsMouse || layer.checked ? layer.hoverOpacity : 0 Behavior on opacity { NumberAnimation { duration: layer.theme && layer.theme.lowPowerMode ? 0 : 120 easing.type: Easing.OutCubic } } } Rectangle { width: layer.rippleRadius * 2 height: width x: layer.pressOrigin.x - width / 2 y: layer.pressOrigin.y - height / 2 radius: width / 2 color: layer.theme ? layer.theme.ui.accent : "white" opacity: layer.pressed ? layer.pressOpacity : 0 Behavior on opacity { NumberAnimation { duration: layer.theme && layer.theme.lowPowerMode ? 0 : 160 easing.type: Easing.OutCubic } } } NumberAnimation { id: rippleAnimation target: layer property: "rippleRadius" to: layer.endRadius duration: layer.theme && layer.theme.lowPowerMode ? 0 : 220 easing.type: Easing.OutCubic } } // Shared clock renderer. `stacked` lays HH / rule / MM in a column (vertical // bar); otherwise HH | rule | MM in a row (horizontal bar). The accent rule // is a Rectangle, so it never depends on a font glyph. component NuguClock: Rectangle { id: clk property bool stacked: false readonly property bool selected: root.surfaces && root.surfaces.popoutName === "clock" readonly property var clockParts: String(root.chipLabel("clock")).split(":") readonly property string hh: clk.clockParts[0] || "--" readonly property string mm: clk.clockParts[1] || "--" implicitWidth: clockContent.implicitWidth + (stacked ? 12 : 16) implicitHeight: clockContent.implicitHeight + (stacked ? 12 : 8) width: implicitWidth height: implicitHeight radius: 9 color: !root.theme ? "transparent" : (selected ? root.theme.ui.focus : root.theme.withAlpha(root.theme.ui.minor, 0.1)) clip: true Component.onCompleted: Qt.callLater(() => root.registerAnchor("clock", clk)) onXChanged: root.registerAnchor("clock", clk) onYChanged: root.registerAnchor("clock", clk) onWidthChanged: root.registerAnchor("clock", clk) onHeightChanged: root.registerAnchor("clock", clk) GridLayout { id: clockContent anchors.centerIn: parent columns: clk.stacked ? 1 : 3 rowSpacing: clk.stacked ? 3 : 0 columnSpacing: 6 NuguText { Layout.alignment: Qt.AlignCenter theme: root.theme variant: "meta" text: clk.hh color: !root.theme ? "transparent" : (clk.selected ? root.theme.content.backdrop : root.theme.content.normal) } Rectangle { Layout.alignment: Qt.AlignCenter Layout.preferredWidth: clk.stacked ? 16 : 2 Layout.preferredHeight: clk.stacked ? 2 : 14 radius: 1 color: root.theme ? root.theme.ui.accent : "transparent" } NuguText { Layout.alignment: Qt.AlignCenter theme: root.theme variant: "meta" text: clk.mm color: !root.theme ? "transparent" : (clk.selected ? root.theme.content.backdrop : root.theme.content.normal) } } StateLayer { theme: root.theme cornerRadius: clk.radius checked: clk.selected onActivated: { if (root.surfaces) root.activatePopout("clock"); } } } component BarChip: Rectangle { id: chip required property string popoutName required property string label property int preferredWidth: Math.max(52, labelText.implicitWidth + 18) readonly property bool selected: root.surfaces && root.surfaces.popoutName === popoutName readonly property bool active: root.chipActive(popoutName) readonly property bool activeStrong: root.chipActiveStrong(popoutName) readonly property bool nokoCapture: popoutName === "noko" && root.nokoService && (root.nokoService.listening || root.nokoService.transcribing) readonly property color roleColor: !root.theme ? "transparent" : (root.chipRole(popoutName) === "warning" ? root.theme.warning : root.theme.ui.accent) Layout.preferredWidth: preferredWidth Layout.preferredHeight: 26 Layout.alignment: Qt.AlignVCenter radius: 9 color: !root.theme ? "transparent" : (selected ? root.theme.ui.focus : (activeStrong && !selected ? root.theme.withAlpha(chip.roleColor, 0.3) : root.theme.withAlpha(root.theme.ui.minor, 0.1))) clip: true border.width: chip.activeStrong && !chip.selected ? 1 : 0 border.color: root.theme && chip.activeStrong ? chip.roleColor : "transparent" Component.onCompleted: Qt.callLater(() => { return root.registerAnchor(chip.popoutName, chip); }) onXChanged: root.registerAnchor(chip.popoutName, chip) onYChanged: root.registerAnchor(chip.popoutName, chip) onWidthChanged: root.registerAnchor(chip.popoutName, chip) onHeightChanged: root.registerAnchor(chip.popoutName, chip) NuguText { id: labelText anchors.centerIn: parent width: Math.max(0, parent.width - 14) theme: root.theme variant: "meta" selected: chip.selected text: chip.label font.bold: chip.activeStrong && !chip.selected horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight color: !root.theme ? "transparent" : (chip.selected ? root.theme.content.backdrop : (chip.activeStrong ? chip.roleColor : root.theme.content.normal)) z: 2 } Rectangle { id: activeDot visible: chip.active && !chip.activeStrong && !chip.selected && !chip.nokoCapture width: 5 height: 5 radius: 2.5 anchors.top: parent.top anchors.right: parent.right anchors.topMargin: 4 anchors.rightMargin: 5 color: root.theme ? chip.roleColor : "white" opacity: 0.9 z: 4 Behavior on opacity { NumberAnimation { duration: root.theme && root.theme.lowPowerMode ? 0 : 160 easing.type: Easing.OutCubic } } } Rectangle { visible: chip.nokoCapture anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 3 width: Math.max(16, parent.width * 0.42) height: 2 radius: 1 color: chip.roleColor opacity: 0.65 z: 4 SequentialAnimation on opacity { running: chip.nokoCapture && !(root.theme && root.theme.lowPowerMode) loops: Animation.Infinite NumberAnimation { to: 0.25 duration: 520 easing.type: Easing.InOutSine } NumberAnimation { to: 0.78 duration: 520 easing.type: Easing.InOutSine } } } StateLayer { theme: root.theme cornerRadius: chip.radius checked: chip.selected z: 3 onActivated: { if (chip.popoutName === "idle") { if (root.usageService) root.usageService.record("idle"); if (root.idleService) root.idleService.toggle(); return; } if (root.surfaces) root.activatePopout(chip.popoutName); } } } }