From 36f6f5a157c8de35c705f6610b212f7f2b5e312d Mon Sep 17 00:00:00 2001 From: 28allday Date: Sun, 14 Jun 2026 10:36:33 +0100 Subject: [PATCH] =?UTF-8?q?feat(nosignal):=20interface=20redesign=20?= =?UTF-8?q?=E2=80=94=20slim=20glass=20bar=20+=20floating=20popouts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert the caelestia shell to the NoSignal "design_handoff_quickshell_shell" spec: a slim translucent top bar with floating glass popouts, fixed dark-glass chrome + Material You dynamic accent, square corners, JetBrains Mono. - Theme singleton (services/Theme.qml): design tokens; accent = m3primary (dynamic), fixed glass/text, square radii. - New slim bar as its own PanelWindow (modules/nsbar/NsBar.qml), additive in shell.qml; reuses caelestia services + components. Helpers: BarPill, NsWorkspaces (numbered, active=accent pill), NsOverlay (popout host, one-at-a- time via services/NsShell.qml, outside-click + Esc), components/NsPanel + components/NsSwitch. - Old caelestia frame removed: BarWrapper collapsed; ContentWindow border 0 + borderTop 0; Exclusions zones 0. Drawer engine kept for launcher/OSD/lock. - 9 popouts (modules/nsbar/panels/): Calendar, QuickSettings, Network, Bluetooth, Notifications, Audio, SystemTray, Overview, PowerMenu. - Clock restyled (date muted + time DemiBold). - nexus (Settings) reskinned: dark-glass window + cards, square corners (Nexus.qml, common/ConnectedRect.qml) — keeps all pages/functionality. Launcher kept as-is. Font (JetBrainsMono) + rounding.scale=0 + Super+Alt+Space nexus bind are caelestia/hypr CONFIG and ship in the OS, not this fork. Next: full Material Symbols -> Nerd Font icon remap. Co-Authored-By: Claude Opus 4.8 (1M context) --- components/NsPanel.qml | 43 +++ components/NsSwitch.qml | 46 +++ modules/bar/BarWrapper.qml | 4 +- modules/bar/components/Clock.qml | 121 ++----- modules/drawers/ContentWindow.qml | 6 +- modules/drawers/Exclusions.qml | 4 +- modules/nexus/Nexus.qml | 6 +- modules/nexus/common/ConnectedRect.qml | 11 +- modules/nsbar/BarPill.qml | 46 +++ modules/nsbar/NsBar.qml | 257 +++++++++++++++ modules/nsbar/NsOverlay.qml | 161 ++++++++++ modules/nsbar/NsWorkspaces.qml | 85 +++++ modules/nsbar/panels/NsAudio.qml | 162 ++++++++++ modules/nsbar/panels/NsBluetooth.qml | 150 +++++++++ modules/nsbar/panels/NsCalendar.qml | 175 +++++++++++ modules/nsbar/panels/NsNetwork.qml | 139 +++++++++ modules/nsbar/panels/NsNotifications.qml | 171 ++++++++++ modules/nsbar/panels/NsOverview.qml | 113 +++++++ modules/nsbar/panels/NsPowerMenu.qml | 143 +++++++++ modules/nsbar/panels/NsQuickSettings.qml | 382 +++++++++++++++++++++++ modules/nsbar/panels/NsSystemTray.qml | 100 ++++++ services/NsShell.qml | 35 +++ services/Theme.qml | 91 ++++++ shell.qml | 5 +- 24 files changed, 2341 insertions(+), 115 deletions(-) create mode 100644 components/NsPanel.qml create mode 100644 components/NsSwitch.qml create mode 100644 modules/nsbar/BarPill.qml create mode 100644 modules/nsbar/NsBar.qml create mode 100644 modules/nsbar/NsOverlay.qml create mode 100644 modules/nsbar/NsWorkspaces.qml create mode 100644 modules/nsbar/panels/NsAudio.qml create mode 100644 modules/nsbar/panels/NsBluetooth.qml create mode 100644 modules/nsbar/panels/NsCalendar.qml create mode 100644 modules/nsbar/panels/NsNetwork.qml create mode 100644 modules/nsbar/panels/NsNotifications.qml create mode 100644 modules/nsbar/panels/NsOverview.qml create mode 100644 modules/nsbar/panels/NsPowerMenu.qml create mode 100644 modules/nsbar/panels/NsQuickSettings.qml create mode 100644 modules/nsbar/panels/NsSystemTray.qml create mode 100644 services/NsShell.qml create mode 100644 services/Theme.qml diff --git a/components/NsPanel.qml b/components/NsPanel.qml new file mode 100644 index 00000000..23375e66 --- /dev/null +++ b/components/NsPanel.qml @@ -0,0 +1,43 @@ +pragma ComponentBehavior: Bound + +// NoSignal floating popout container: dark-glass card, 1px border, 4px radius. +// Content goes in a ColumnLayout (default children). Sizes to its content height; +// set `width` on the instance. A background MouseArea swallows clicks so clicking +// inside the panel doesn't fall through to the overlay's close-catcher. + +import QtQuick +import QtQuick.Layouts +import qs.services + +Rectangle { + id: root + + property int pad: Theme.size.panelPad + property alias spacing: holder.spacing + default property alias content: holder.data + + // anchoring hint read by NsOverlay: "center" | "right" | "trigger" + property string anchorMode: "right" + + implicitHeight: holder.implicitHeight + root.pad * 2 + color: Theme.panelBg + radius: Theme.radius.panel + border.width: 1 + border.color: Theme.panelBorder + + MouseArea { + anchors.fill: parent + hoverEnabled: true + // swallow clicks/hover so they don't reach the overlay close-catcher + } + + ColumnLayout { + id: holder + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: root.pad + spacing: Theme.size.gap + } +} diff --git a/components/NsSwitch.qml b/components/NsSwitch.qml new file mode 100644 index 00000000..b6d3d391 --- /dev/null +++ b/components/NsSwitch.qml @@ -0,0 +1,46 @@ +pragma ComponentBehavior: Bound + +// NoSignal toggle switch (animated 16px knob) used in panel headers. + +import QtQuick +import qs.services + +Rectangle { + id: root + + property bool on: false + signal toggled + + implicitWidth: 38 + implicitHeight: 22 + radius: 11 + color: on ? Theme.accent : Theme.fillSubtle + + Behavior on color { + ColorAnimation { + duration: Theme.hoverMs + } + } + + Rectangle { + x: root.on ? parent.width - width - 3 : 3 + anchors.verticalCenter: parent.verticalCenter + implicitWidth: 16 + implicitHeight: 16 + radius: 8 + color: root.on ? Theme.onAccent : Theme.text + + Behavior on x { + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.toggled() + } +} diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index f1216f57..0049704b 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -24,7 +24,9 @@ Item { readonly property int padding: Math.max(Tokens.padding.small, Config.border.thickness) readonly property int contentHeight: Tokens.sizes.bar.innerWidth + padding * 2 readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentHeight : Config.border.thickness - readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered) + // NoSignal redesign: the old caelestia bar is replaced by the slim NsBar + // PanelWindow. Collapse it (the drawer engine is kept for launcher/OSD/lock). + readonly property bool shouldBeVisible: false property bool isHovered function closeTray(): void { diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index d279a7f9..36879556 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -1,113 +1,34 @@ pragma ComponentBehavior: Bound +// NoSignal redesign: center clock module — date (muted) + time (DemiBold), +// JetBrains Mono. The hover/active pill is applied by the bar module wrapper; +// this is just the content. Opens the Calendar popout. + import QtQuick import QtQuick.Layouts -import Caelestia.Config import qs.components import qs.services -StyledRect { +RowLayout { id: root - readonly property color colour: Colours.palette.m3tertiary - readonly property int padding: Config.bar.clock.background ? Tokens.padding.medium : Tokens.padding.extraSmall - readonly property var font: Tokens.font.body.builders.small.scale(1.1) + spacing: 8 - implicitHeight: Tokens.sizes.bar.innerWidth - implicitWidth: layout.implicitWidth + root.padding * 2 + StyledText { + Layout.alignment: Qt.AlignVCenter + text: Time.format("ddd d MMM") + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bar + font.weight: Font.Normal + } - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.full - - RowLayout { - id: layout - - anchors.centerIn: parent - spacing: Tokens.spacing.small - - Loader { - Layout.alignment: Qt.AlignVCenter - asynchronous: true - active: Config.bar.clock.showIcon - visible: active - - sourceComponent: MaterialIcon { - text: "calendar_month" - color: root.colour - } - } - - StyledText { - Layout.alignment: Qt.AlignVCenter - visible: Config.bar.clock.showDate - - horizontalAlignment: StyledText.AlignHCenter - text: Time.format("ddd d") - font: Tokens.font.body.small - color: root.colour - } - - Rectangle { - Layout.fillHeight: true - Layout.topMargin: root.padding - Layout.bottomMargin: root.padding - visible: Config.bar.clock.showDate - implicitWidth: 1 - color: Colours.palette.m3outlineVariant - } - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: Time.hourStr - font: { - const scale = text === "11" ? 1.15 : Math.min(1.05, Math.max(hourMetrics.width, minMetrics.width) / hourMetrics.width); - return root.font.width(scale * 100).letterSpacing(scale).build(); - } - color: root.colour - - TextMetrics { - id: hourMetrics - - font: root.font.build() - text: Time.hourStr - } - } - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: ":" - font: root.font.build() - color: root.colour - } - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: Time.minuteStr - font: { - const scale = text === "11" ? 1.15 : Math.min(1.05, Math.max(hourMetrics.width, minMetrics.width) / minMetrics.width); - return root.font.width(scale * 100).letterSpacing(scale).build(); - } - color: root.colour - - TextMetrics { - id: minMetrics - - font: root.font.build() - text: Time.minuteStr - } - } - - Loader { - Layout.alignment: Qt.AlignVCenter - asynchronous: true - active: GlobalConfig.services.useTwelveHourClock - visible: active - - sourceComponent: StyledText { - text: Time.amPmStr.toLowerCase() - font: Tokens.font.body.builders.small.scale(0.9).build() - color: root.colour - } - } + StyledText { + Layout.alignment: Qt.AlignVCenter + text: Time.format("hh:mm") + color: Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.bar + font.weight: Font.DemiBold } } diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index e6d60cad..b935cab5 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -34,8 +34,8 @@ StyledWindow { property real fsTransitionProg: hasFullscreen ? 1 : 0 readonly property real sdfBorderOffset: 2 * fsTransitionProg // SDFs joins are not exact, so offset by 2px to ensure nothing shows - readonly property real borderThickness: contentItem.Config.border.thickness * (1 - fsTransitionProg) - readonly property real borderRounding: contentItem.Config.border.rounding * (1 - fsTransitionProg) + readonly property real borderThickness: 0 // NoSignal: no screen-edge frame (NsBar owns the top) + readonly property real borderRounding: 0 // NoSignal: no rounded screen frame readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg) readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness @@ -157,7 +157,7 @@ StyledWindow { radius: root.borderRounding borderLeft: root.borderThickness - anchors.margins - root.sdfBorderOffset borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset - borderTop: bar.implicitHeight - anchors.margins - root.sdfBorderOffset + borderTop: -anchors.margins - root.sdfBorderOffset // NoSignal: no top frame borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset } diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml index 4005613c..6531b899 100644 --- a/modules/drawers/Exclusions.qml +++ b/modules/drawers/Exclusions.qml @@ -18,7 +18,7 @@ Scope { ExclusionZone { anchors.top: true - exclusiveZone: root.bar.exclusiveZone + exclusiveZone: 0 // NoSignal: NsBar owns the top exclusive zone } ExclusionZone { @@ -32,7 +32,7 @@ Scope { component ExclusionZone: StyledWindow { screen: root.screen name: "border-exclusion" - exclusiveZone: contentItem.Config.border.thickness + exclusiveZone: 0 // NoSignal: no screen-edge frame mask: Region {} implicitWidth: 1 implicitHeight: 1 diff --git a/modules/nexus/Nexus.qml b/modules/nexus/Nexus.qml index 90035bc3..8669048c 100644 --- a/modules/nexus/Nexus.qml +++ b/modules/nexus/Nexus.qml @@ -16,7 +16,7 @@ Item { onClose: root.close() } - property color blobColour: Colours.tPalette.m3surfaceContainerLow + property color blobColour: Theme.panelBg // NoSignal: fixed dark-glass window surface signal close @@ -30,7 +30,7 @@ Item { BlobGroup { id: blobGroup - smoothing: root.Tokens.rounding.medium + smoothing: 0 // NoSignal: crisp corners (no organic blob smoothing) color: root.blobColour } @@ -38,7 +38,7 @@ Item { anchors.fill: parent group: blobGroup opacity: root.blobColour.a - radius: Tokens.rounding.large + radius: Theme.radius.panel // NoSignal: sharp 4px corners borderLeft: navPane.width + navPane.anchors.margins * 2 borderRight: Tokens.padding.medium diff --git a/modules/nexus/common/ConnectedRect.qml b/modules/nexus/common/ConnectedRect.qml index df5b21ae..945cac78 100644 --- a/modules/nexus/common/ConnectedRect.qml +++ b/modules/nexus/common/ConnectedRect.qml @@ -7,9 +7,10 @@ StyledRect { property bool first property bool last - color: Colours.tPalette.m3surfaceContainer - topLeftRadius: first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall - topRightRadius: first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall - bottomLeftRadius: last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall - bottomRightRadius: last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + // NoSignal: dark-glass cards, 4px outer corners (connected-list look) + color: Theme.fillSubtle + topLeftRadius: first ? Theme.radius.panel : 0 + topRightRadius: first ? Theme.radius.panel : 0 + bottomLeftRadius: last ? Theme.radius.panel : 0 + bottomRightRadius: last ? Theme.radius.panel : 0 } diff --git a/modules/nsbar/BarPill.qml b/modules/nsbar/BarPill.qml new file mode 100644 index 00000000..b044f4d7 --- /dev/null +++ b/modules/nsbar/BarPill.qml @@ -0,0 +1,46 @@ +pragma ComponentBehavior: Bound + +// NoSignal bar module pill: hover-highlightable (radius 5, hover bg); when its +// panel is open it uses accent.soft bg + accent foreground. Children go in a +// centered Row. `active` drives the open/highlight state; `triggered()` fires on +// click (the bar wires this to toggle its openPanel string). + +import QtQuick +import qs.services + +Rectangle { + id: root + + property bool active: false + property int hPad: 9 + signal triggered + + default property alias content: inner.data + + implicitWidth: inner.implicitWidth + hPad * 2 + implicitHeight: 26 + radius: Theme.radius.barModule + color: active ? Theme.accentSoft : mouse.containsMouse ? Theme.hover : "transparent" + + Behavior on color { + ColorAnimation { + duration: Theme.hoverMs + } + } + + Row { + id: inner + + anchors.centerIn: parent + spacing: 6 + } + + MouseArea { + id: mouse + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.triggered() + } +} diff --git a/modules/nsbar/NsBar.qml b/modules/nsbar/NsBar.qml new file mode 100644 index 00000000..de032336 --- /dev/null +++ b/modules/nsbar/NsBar.qml @@ -0,0 +1,257 @@ +pragma ComponentBehavior: Bound + +// NoSignal slim top bar (interface redesign 2026-06-14). +// Flat 38px translucent-glass PanelWindow at the top edge. Three zones: +// left : logo · | · workspaces · | · window-title +// center: clock +// right : media · bell · wifi · bt · volume · battery · | · power +// Each interactive module is a BarPill (hover bg; active = accent.soft + accent +// fg). `openPanel` is the single shell-level "one panel open at a time" string; +// clicking a pill toggles it. The popup panels themselves are built next — for +// now a click just highlights the pill. Reuses the existing service singletons. + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Bluetooth +import Quickshell.Services.UPower +import qs.services +import qs.utils +import qs.components +import qs.modules.bar.components as BC +import qs.modules.bar.components.workspaces as WS + +Variants { + model: Screens.screens + + PanelWindow { + id: win + + required property ShellScreen modelData + + screen: modelData + color: "transparent" + WlrLayershell.namespace: "nsbar" // for Hyprland blur layerrule + WlrLayershell.layer: WlrLayer.Top + + anchors.top: true + anchors.left: true + anchors.right: true + + implicitHeight: Theme.size.barHeight + exclusiveZone: Theme.size.barHeight + + readonly property int hPad: 12 + readonly property int gap: 6 + + // ── Glass surface + 1px bottom border ────────────────────────────── + Rectangle { + anchors.fill: parent + color: Theme.barBg + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 1 + color: Theme.barBorder + } + } + + // ── Left zone ────────────────────────────────────────────────────── + RowLayout { + anchors.left: parent.left + anchors.leftMargin: win.hPad + anchors.verticalCenter: parent.verticalCenter + spacing: win.gap + + BC.OsIcon {} + + Divider {} + + NsWorkspaces {} + + Divider {} + + BarPill { + id: titlePill + + active: NsShell.open === "overview" + onTriggered: NsShell.toggle("overview", mapToItem(null, width / 2, 0).x) + + MaterialIcon { + text: "desktop_windows" + color: titlePill.active ? Theme.accent : Theme.textMuted + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: Hypr.activeToplevel?.title ?? "Desktop" + elide: Text.ElideRight + width: Math.min(implicitWidth, 220) + color: titlePill.active ? Theme.accent : Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.bar + } + } + } + + // ── Center zone: clock ───────────────────────────────────────────── + BarPill { + id: clockPill + + anchors.centerIn: parent + active: NsShell.open === "calendar" + onTriggered: NsShell.toggle("calendar", mapToItem(null, width / 2, 0).x) + + BC.Clock {} + } + + // ── Right zone ───────────────────────────────────────────────────── + RowLayout { + anchors.right: parent.right + anchors.rightMargin: win.hPad + anchors.verticalCenter: parent.verticalCenter + spacing: win.gap + + // Media / now-playing + BarPill { + id: mediaPill + + visible: Players.active !== null + active: NsShell.open === "quicksettings" + onTriggered: NsShell.toggle("quicksettings", mapToItem(null, width / 2, 0).x) + + MaterialIcon { + text: Players.active?.isPlaying ? "pause" : "music_note" + color: mediaPill.active ? Theme.accent : Theme.green + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: Players.active?.trackTitle || "Awake" + elide: Text.ElideRight + width: Math.min(implicitWidth, 90) + color: mediaPill.active ? Theme.accent : Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.bar + } + } + + // System tray + IconPill { + icon: "widgets" + panel: "tray" + } + + // Notifications bell (with unread dot overlaid on the icon) + BarPill { + id: bellPill + + active: NsShell.open === "notifications" + onTriggered: NsShell.toggle("notifications", mapToItem(null, width / 2, 0).x) + + MaterialIcon { + text: Notifs.notClosed.length > 0 ? "notifications" : "notifications_none" + color: bellPill.active ? Theme.accent : Theme.text + + Rectangle { + visible: Notifs.notClosed.length > 0 + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: -1 + anchors.topMargin: 1 + implicitWidth: 6 + implicitHeight: 6 + radius: 3 + color: Theme.accent + } + } + } + + // Wi-Fi + IconPill { + icon: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off" + panel: "network" + } + + // Bluetooth + IconPill { + icon: { + if (!Bluetooth.defaultAdapter?.enabled) + return "bluetooth_disabled"; + if (Bluetooth.devices.values.some(d => d.connected)) + return "bluetooth_connected"; + return "bluetooth"; + } + panel: "bluetooth" + } + + // Volume + IconPill { + icon: Icons.getVolumeIcon(Audio.volume, Audio.muted) + panel: "audio" + } + + // Battery / Quick Settings — always present (control-centre trigger): + // battery + % on laptops, a "tune" icon on desktops. + BarPill { + id: batteryPill + + readonly property bool laptop: UPower.displayDevice.isLaptopBattery + active: NsShell.open === "quicksettings" + onTriggered: NsShell.toggle("quicksettings", mapToItem(null, width / 2, 0).x) + + MaterialIcon { + text: batteryPill.laptop ? Icons.getBatteryIcon(UPower.displayDevice.percentage, [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state)) : "tune" + color: batteryPill.active ? Theme.accent : !batteryPill.laptop || !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? Theme.text : Theme.danger + fill: 1 + } + + StyledText { + visible: batteryPill.laptop + anchors.verticalCenter: parent.verticalCenter + text: Math.round(UPower.displayDevice.percentage * 100) + "%" + color: batteryPill.active ? Theme.accent : Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.bar + } + } + + Divider {} + + // Power + IconPill { + icon: "power_settings_new" + panel: "power" + iconColour: Theme.danger + } + } + + // ── Reusable bits ────────────────────────────────────────────────── + component Divider: Rectangle { + Layout.alignment: Qt.AlignVCenter + implicitWidth: 1 + implicitHeight: 16 + color: Theme.barBorder + } + + // Icon-only pill bound to the shell openPanel state + component IconPill: BarPill { + id: p + + property string icon + property string panel + property color iconColour: Theme.text + + active: NsShell.open === panel + onTriggered: NsShell.toggle(panel, mapToItem(null, width / 2, 0).x) + + MaterialIcon { + text: p.icon + color: p.active ? Theme.accent : p.iconColour + } + } + } +} diff --git a/modules/nsbar/NsOverlay.qml b/modules/nsbar/NsOverlay.qml new file mode 100644 index 00000000..b861bd3d --- /dev/null +++ b/modules/nsbar/NsOverlay.qml @@ -0,0 +1,161 @@ +pragma ComponentBehavior: Bound + +// NoSignal popup overlay: one full-screen layer per screen that hosts the active +// floating panel (driven by NsShell.open). A transparent click-catcher behind the +// panel closes on outside-click; Escape also closes. Panels are positioned below +// the bar, anchored center / right / under-trigger per their `anchorMode`. + +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.services +import "panels" as Panels + +Variants { + model: Screens.screens + + PanelWindow { + id: ov + + required property ShellScreen modelData + + screen: modelData + visible: NsShell.open !== "" + color: "transparent" + WlrLayershell.namespace: "nspanels" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true + exclusiveZone: 0 + + // outside-click closes + MouseArea { + anchors.fill: parent + onClicked: NsShell.close() + } + + // Escape closes + Item { + anchors.fill: parent + focus: ov.visible + Keys.onEscapePressed: NsShell.close() + } + + // full-screen modal: Power Menu + Loader { + anchors.fill: parent + active: NsShell.open === "power" + sourceComponent: powerC + + Component { + id: powerC + + Panels.NsPowerMenu {} + } + } + + Loader { + id: loader + + active: NsShell.open !== "" && NsShell.open !== "power" + y: Theme.size.barHeight + 6 + x: { + if (!item) + return 0; + const w = item.implicitWidth; + const mode = item.anchorMode ?? "right"; + if (mode === "center") + return Math.round((ov.width - w) / 2); + if (mode === "trigger") + return Math.max(8, Math.min(ov.width - w - 8, NsShell.anchorX - w / 2)); + return ov.width - w - 12; // right + } + + // small enter slide + opacity: active ? 1 : 0 + transform: Translate { + y: loader.active ? 0 : -5 + } + Behavior on opacity { + NumberAnimation { + duration: 120 + } + } + + sourceComponent: { + switch (NsShell.open) { + case "calendar": + return calendarC; + case "quicksettings": + return quickSettingsC; + case "network": + return networkC; + case "bluetooth": + return bluetoothC; + case "notifications": + return notificationsC; + case "audio": + return audioC; + case "tray": + return trayC; + case "overview": + return overviewC; + default: + return null; + } + } + + Component { + id: calendarC + + Panels.NsCalendar {} + } + + Component { + id: quickSettingsC + + Panels.NsQuickSettings {} + } + + Component { + id: networkC + + Panels.NsNetwork {} + } + + Component { + id: bluetoothC + + Panels.NsBluetooth {} + } + + Component { + id: notificationsC + + Panels.NsNotifications {} + } + + Component { + id: audioC + + Panels.NsAudio {} + } + + Component { + id: trayC + + Panels.NsSystemTray {} + } + + Component { + id: overviewC + + Panels.NsOverview {} + } + } + } +} diff --git a/modules/nsbar/NsWorkspaces.qml b/modules/nsbar/NsWorkspaces.qml new file mode 100644 index 00000000..0d1e2268 --- /dev/null +++ b/modules/nsbar/NsWorkspaces.qml @@ -0,0 +1,85 @@ +pragma ComponentBehavior: Bound + +// NoSignal workspaces module: numbered 1..N, no capsule. Active = filled accent +// pill with dark (onAccent) text + workspace name (e.g. "2 code"); occupied = +// bright number; empty = faint number. Click switches workspace. + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.services +import qs.components + +RowLayout { + id: root + + spacing: 4 + + Repeater { + model: Config.bar.workspaces.shown + + delegate: Rectangle { + id: ws + + required property int index + readonly property int wsId: index + 1 + readonly property var hw: Hypr.workspaces.values.find(w => w.id === wsId) ?? null + readonly property bool active: Hypr.activeWsId === wsId + readonly property bool occupied: (hw?.lastIpcObject.windows ?? 0) > 0 + readonly property string wsName: { + const n = hw?.name ?? ""; + return n && n !== String(wsId) && !n.startsWith("special") ? n : ""; + } + + Layout.alignment: Qt.AlignVCenter + implicitWidth: row.implicitWidth + (active ? 16 : 10) + implicitHeight: 22 + radius: Theme.radius.barModule + color: active ? Theme.accent : mouse.containsMouse ? Theme.hover : "transparent" + + Behavior on color { + ColorAnimation { + duration: Theme.hoverMs + } + } + Behavior on implicitWidth { + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + + RowLayout { + id: row + + anchors.centerIn: parent + spacing: 5 + + StyledText { + text: ws.wsId + color: ws.active ? Theme.onAccent : ws.occupied ? Theme.text : Theme.textFaint + font.family: Theme.font.family + font.pixelSize: Theme.font.bar + font.weight: ws.active ? Font.DemiBold : Font.Normal + } + + StyledText { + visible: ws.active && ws.wsName !== "" + text: ws.wsName + color: Theme.onAccent + font.family: Theme.font.family + font.pixelSize: Theme.font.bar + } + } + + MouseArea { + id: mouse + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Hypr.dispatch(`workspace ${ws.wsId}`) + } + } + } +} diff --git a/modules/nsbar/panels/NsAudio.qml b/modules/nsbar/panels/NsAudio.qml new file mode 100644 index 00000000..1bd42291 --- /dev/null +++ b/modules/nsbar/panels/NsAudio.qml @@ -0,0 +1,162 @@ +pragma ComponentBehavior: Bound + +// NoSignal Audio Mixer popout (off the volume pill). Output device row, master +// slider, per-app application sliders, input (mic) slider. Wired to Pipewire +// via the Audio service. + +import QtQuick +import QtQuick.Layouts +import qs.services +import qs.components + +NsPanel { + id: root + + implicitWidth: 340 + anchorMode: "right" + + // output device + RowLayout { + Layout.fillWidth: true + spacing: 8 + + MaterialIcon { + text: "speaker" + color: Theme.textMuted + } + + StyledText { + Layout.fillWidth: true + text: Audio.sink?.description || Audio.sink?.name || "Output" + color: Theme.text + elide: Text.ElideRight + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + + MaterialIcon { + text: "expand_more" + color: Theme.textFaint + } + } + + AudioSlider { + icon: Audio.muted ? "volume_off" : "volume_up" + value: Audio.volume + onMoved: v => Audio.setVolume(v) + } + + // applications + StyledText { + visible: Audio.streams.length > 0 + text: "APPLICATIONS" + color: Theme.textFaint + font.family: Theme.font.family + font.pixelSize: Theme.font.label + font.letterSpacing: Theme.font.trackingLabel + } + + Repeater { + model: Audio.streams + + delegate: AudioSlider { + required property var modelData + + Layout.fillWidth: true + icon: "graphic_eq" + label: modelData?.properties?.["application.name"] || modelData?.description || modelData?.name || "App" + value: modelData?.audio?.volume ?? 0 + onMoved: v => { + if (modelData?.audio) + modelData.audio.volume = v; + } + } + } + + // input + StyledText { + text: "INPUT" + Layout.topMargin: 2 + color: Theme.textFaint + font.family: Theme.font.family + font.pixelSize: Theme.font.label + font.letterSpacing: Theme.font.trackingLabel + } + + AudioSlider { + icon: Audio.sourceMuted ? "mic_off" : "mic" + value: Audio.sourceVolume + onMoved: v => Audio.setSourceVolume(v) + } + + component AudioSlider: ColumnLayout { + id: s + + property string icon + property string label: "" + property real value: 0 + signal moved(real v) + + Layout.fillWidth: true + spacing: 3 + + StyledText { + visible: s.label !== "" + text: s.label + color: Theme.textMuted + elide: Text.ElideRight + Layout.fillWidth: true + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } + + RowLayout { + Layout.fillWidth: true + spacing: 10 + + MaterialIcon { + text: s.icon + color: Theme.textMuted + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 6 + radius: 3 + color: Theme.fillSubtle + + Rectangle { + width: parent.width * Math.max(0, Math.min(1, s.value)) + height: parent.height + radius: 3 + color: Theme.accent + } + + Rectangle { + x: parent.width * Math.max(0, Math.min(1, s.value)) - width / 2 + anchors.verticalCenter: parent.verticalCenter + implicitWidth: 14 + implicitHeight: 14 + radius: 7 + color: Theme.accent + } + + MouseArea { + anchors.fill: parent + onPressed: e => s.moved(Math.max(0, Math.min(1, e.x / width))) + onPositionChanged: e => { + if (pressed) + s.moved(Math.max(0, Math.min(1, e.x / width))); + } + } + } + + StyledText { + text: Math.round(s.value * 100) + "%" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } + } + } +} diff --git a/modules/nsbar/panels/NsBluetooth.qml b/modules/nsbar/panels/NsBluetooth.qml new file mode 100644 index 00000000..0c8bf95c --- /dev/null +++ b/modules/nsbar/panels/NsBluetooth.qml @@ -0,0 +1,150 @@ +pragma ComponentBehavior: Bound + +// NoSignal Bluetooth popout (off the BT pill). Title + switch; device rows (icon +// tile peach-tinted if connected, name, status, battery %). Footer "Bluetooth +// settings". Wired to Quickshell.Bluetooth (BlueZ). + +import QtQuick +import QtQuick.Layouts +import Quickshell.Bluetooth +import qs.services +import qs.utils +import qs.components + +NsPanel { + id: root + + implicitWidth: 340 + anchorMode: "right" + + readonly property var adapter: Bluetooth.defaultAdapter + + RowLayout { + Layout.fillWidth: true + + StyledText { + text: "Bluetooth" + color: Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.panelTitle + font.weight: Font.DemiBold + } + + Item { + Layout.fillWidth: true + } + + NsSwitch { + on: root.adapter?.enabled ?? false + onToggled: if (root.adapter) + root.adapter.enabled = !root.adapter.enabled + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + visible: root.adapter?.enabled ?? false + + Repeater { + model: Bluetooth.devices + + delegate: Rectangle { + id: row + + required property BluetoothDevice modelData + readonly property bool connected: modelData?.connected ?? false + + Layout.fillWidth: true + implicitHeight: 44 + radius: Theme.radius.button + color: ma.containsMouse ? Theme.hover : "transparent" + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 10 + + Rectangle { + implicitWidth: 30 + implicitHeight: 30 + radius: 0 + color: row.connected ? Theme.accentSoft : Theme.fillSubtle + + MaterialIcon { + anchors.centerIn: parent + text: Icons.getBluetoothIcon(row.modelData?.icon ?? "") + color: row.connected ? Theme.accent : Theme.text + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + text: row.modelData?.name ?? "?" + color: Theme.text + elide: Text.ElideRight + Layout.fillWidth: true + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + StyledText { + text: row.connected ? "Connected" : (row.modelData?.paired ?? false) ? "Paired" : "Not connected" + color: row.connected ? Theme.accent : Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } + } + + StyledText { + visible: row.connected && (row.modelData?.battery ?? 0) > 0 + text: Math.round((row.modelData?.battery ?? 0) * 100) + "%" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } + } + + MouseArea { + id: ma + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + } + } + + StyledText { + visible: Bluetooth.devices.values.length === 0 + text: "No devices" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + } + + StyledText { + visible: !(root.adapter?.enabled ?? false) + text: "Bluetooth is off" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 4 + implicitHeight: 1 + color: Theme.barBorder + } + + StyledText { + text: "Bluetooth settings" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } +} diff --git a/modules/nsbar/panels/NsCalendar.qml b/modules/nsbar/panels/NsCalendar.qml new file mode 100644 index 00000000..552a3e2d --- /dev/null +++ b/modules/nsbar/panels/NsCalendar.qml @@ -0,0 +1,175 @@ +pragma ComponentBehavior: Bound + +// NoSignal Calendar popout (off the center clock). Month grid (Mon-first), +// today = filled accent circle. Below a divider, a TODAY events section +// (empty-state for now; real events can be wired from ICS/khal later). + +import QtQuick +import QtQuick.Layouts +import qs.services +import qs.components + +NsPanel { + id: root + + implicitWidth: 304 + anchorMode: "center" + + property int monthOffset: 0 + readonly property date base: new Date(Time.date.getFullYear(), Time.date.getMonth() + monthOffset, 1) + readonly property int year: base.getFullYear() + readonly property int month: base.getMonth() + readonly property bool isCurrentMonth: monthOffset === 0 + readonly property int todayDate: Time.date.getDate() + + readonly property var cells: { + const first = new Date(year, month, 1); + const lead = (first.getDay() + 6) % 7; // Monday-first + const dim = new Date(year, month + 1, 0).getDate(); + const arr = []; + for (let i = 0; i < lead; i++) + arr.push(0); + for (let d = 1; d <= dim; d++) + arr.push(d); + return arr; + } + + // ── Header: month + year, prev/next ────────────────────────────────── + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + spacing: 0 + + StyledText { + text: root.base.toLocaleString(Qt.locale(), "MMMM") + color: Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.panelTitle + font.weight: Font.DemiBold + } + + StyledText { + text: root.year + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + } + + Item { + Layout.fillWidth: true + } + + NavBtn { + icon: "chevron_left" + onActivated: root.monthOffset-- + } + + NavBtn { + icon: "chevron_right" + onActivated: root.monthOffset++ + } + } + + // ── Grid: weekday header + days ────────────────────────────────────── + GridLayout { + Layout.fillWidth: true + columns: 7 + rowSpacing: 2 + columnSpacing: 0 + + Repeater { + model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] + + StyledText { + required property string modelData + + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: modelData + color: Theme.textFaint + font.family: Theme.font.family + font.pixelSize: Theme.font.label + } + } + + Repeater { + model: root.cells + + Item { + required property int modelData + readonly property bool today: root.isCurrentMonth && modelData === root.todayDate + + Layout.fillWidth: true + implicitHeight: 30 + + Rectangle { + anchors.centerIn: parent + visible: parent.modelData > 0 + implicitWidth: 26 + implicitHeight: 26 + radius: 13 + color: parent.today ? Theme.accent : "transparent" + + StyledText { + anchors.centerIn: parent + text: parent.parent.modelData > 0 ? parent.parent.modelData : "" + color: parent.parent.today ? Theme.onAccent : Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + } + } + } + } + + // ── Divider ────────────────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 4 + implicitHeight: 1 + color: Theme.barBorder + } + + // ── TODAY events ───────────────────────────────────────────────────── + StyledText { + text: "TODAY" + color: Theme.textFaint + font.family: Theme.font.family + font.pixelSize: Theme.font.label + font.letterSpacing: Theme.font.trackingLabel + } + + StyledText { + text: "No events" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + + component NavBtn: Rectangle { + property string icon + signal activated + + implicitWidth: 24 + implicitHeight: 24 + radius: Theme.radius.button + color: ma.containsMouse ? Theme.hover : "transparent" + + MaterialIcon { + anchors.centerIn: parent + text: parent.icon + color: Theme.textMuted + } + + MouseArea { + id: ma + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: parent.activated() + } + } +} diff --git a/modules/nsbar/panels/NsNetwork.qml b/modules/nsbar/panels/NsNetwork.qml new file mode 100644 index 00000000..92703351 --- /dev/null +++ b/modules/nsbar/panels/NsNetwork.qml @@ -0,0 +1,139 @@ +pragma ComponentBehavior: Bound + +// NoSignal Network popout (off the Wi-Fi pill). Title + switch; list of networks +// (signal glyph accent if connected, SSID, Connected label, lock if secured); +// connected row = accent.soft. Footer "Network settings". Wired to Network/Nmcli. + +import QtQuick +import QtQuick.Layouts +import qs.services +import qs.utils +import qs.components + +NsPanel { + id: root + + implicitWidth: 340 + anchorMode: "right" + + RowLayout { + Layout.fillWidth: true + + StyledText { + text: "Wi-Fi" + color: Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.panelTitle + font.weight: Font.DemiBold + } + + Item { + Layout.fillWidth: true + } + + NsSwitch { + on: Network.wifiEnabled + onToggled: Network.toggleWifi() + } + } + + // network list + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + visible: Network.wifiEnabled + + Repeater { + model: Network.networks + + delegate: Rectangle { + id: row + + required property var modelData + readonly property bool connected: modelData?.active ?? false + + Layout.fillWidth: true + implicitHeight: 40 + radius: Theme.radius.button + color: connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent" + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 10 + + MaterialIcon { + text: Icons.getNetworkIcon(row.modelData?.strength ?? 0) + color: row.connected ? Theme.accent : Theme.text + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + text: row.modelData?.ssid ?? "?" + color: Theme.text + elide: Text.ElideRight + Layout.fillWidth: true + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + StyledText { + visible: row.connected + text: "Connected" + color: Theme.accent + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } + } + + MaterialIcon { + visible: row.modelData?.isSecure ?? false + text: "lock" + color: Theme.textFaint + } + } + + MouseArea { + id: ma + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + } + } + + StyledText { + visible: Network.networks.length === 0 + text: "No networks found" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + } + + StyledText { + visible: !Network.wifiEnabled + text: "Wi-Fi is off" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + + // footer + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 4 + implicitHeight: 1 + color: Theme.barBorder + } + + StyledText { + text: "Network settings" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } +} diff --git a/modules/nsbar/panels/NsNotifications.qml b/modules/nsbar/panels/NsNotifications.qml new file mode 100644 index 00000000..17220980 --- /dev/null +++ b/modules/nsbar/panels/NsNotifications.qml @@ -0,0 +1,171 @@ +pragma ComponentBehavior: Bound + +// NoSignal Notifications popout (off the bell). Header (count + DND + Clear), +// scrollable cards (tinted icon, title, body, time), empty state. Uses Notifs. + +import QtQuick +import QtQuick.Layouts +import qs.services +import qs.utils +import qs.components + +NsPanel { + id: root + + implicitWidth: 380 + anchorMode: "right" + + RowLayout { + Layout.fillWidth: true + + StyledText { + text: "Notifications" + color: Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.panelTitle + font.weight: Font.DemiBold + } + + StyledText { + text: "· " + Notifs.notClosed.length + " new" + color: Theme.textFaint + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + + Item { + Layout.fillWidth: true + } + + Rectangle { + implicitWidth: 26 + implicitHeight: 26 + radius: 13 + color: Notifs.dnd ? Theme.accentSoft : dndMa.containsMouse ? Theme.hover : "transparent" + + MaterialIcon { + anchors.centerIn: parent + text: Notifs.dnd ? "do_not_disturb_on" : "dark_mode" + color: Notifs.dnd ? Theme.accent : Theme.textMuted + } + + MouseArea { + id: dndMa + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Notifs.dnd = !Notifs.dnd + } + } + + StyledText { + text: "Clear" + color: clearMa.containsMouse ? Theme.accent : Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + + MouseArea { + id: clearMa + anchors.fill: parent + anchors.margins: -6 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + for (const n of Notifs.notClosed) + n.notification?.dismiss(); + } + } + } + } + + // cards + ColumnLayout { + Layout.fillWidth: true + spacing: 6 + + Repeater { + model: Notifs.notClosed + + delegate: Rectangle { + id: card + + required property var modelData + + Layout.fillWidth: true + implicitHeight: cardRow.implicitHeight + 20 + radius: Theme.radius.button + color: Theme.fillSubtle + + RowLayout { + id: cardRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 10 + + Rectangle { + Layout.alignment: Qt.AlignTop + implicitWidth: 30 + implicitHeight: 30 + radius: 0 + color: Theme.accentSoft + + MaterialIcon { + anchors.centerIn: parent + text: Icons.getNotifIcon(card.modelData?.summary ?? "", card.modelData?.urgency ?? 1) + color: Theme.accent + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + RowLayout { + Layout.fillWidth: true + StyledText { + Layout.fillWidth: true + text: card.modelData?.summary ?? "" + color: Theme.text + elide: Text.ElideRight + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + font.weight: Font.Medium + } + StyledText { + text: card.modelData?.timeStr ?? "" + color: Theme.textFaint + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } + } + + StyledText { + Layout.fillWidth: true + visible: text !== "" + text: card.modelData?.body ?? "" + color: Theme.textMuted + wrapMode: Text.Wrap + maximumLineCount: 3 + elide: Text.ElideRight + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } + } + } + } + } + + StyledText { + visible: Notifs.notClosed.length === 0 + Layout.topMargin: 6 + text: "No notifications" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + } +} diff --git a/modules/nsbar/panels/NsOverview.qml b/modules/nsbar/panels/NsOverview.qml new file mode 100644 index 00000000..3db395d3 --- /dev/null +++ b/modules/nsbar/panels/NsOverview.qml @@ -0,0 +1,113 @@ +pragma ComponentBehavior: Bound + +// NoSignal Workspaces overview (off the window-title module). Row of workspace +// thumbnails (active = accent border + glow), number + name below, click to +// switch. Wired to Hypr. + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.services +import qs.components + +NsPanel { + id: root + + implicitWidth: 5 * 124 + 16 * 2 + Theme.size.panelPad * 2 - 16 + anchorMode: "center" + + readonly property int shown: Config.bar.workspaces.shown + + RowLayout { + Layout.fillWidth: true + + StyledText { + text: "Workspaces" + color: Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.panelTitle + font.weight: Font.DemiBold + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: root.shown + " desktops" + color: Theme.textFaint + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 16 + + Repeater { + model: root.shown + + delegate: ColumnLayout { + id: cell + + required property int index + readonly property int wsId: index + 1 + readonly property var hw: Hypr.workspaces.values.find(w => w.id === wsId) ?? null + readonly property bool active: Hypr.activeWsId === wsId + readonly property bool occupied: (hw?.lastIpcObject.windows ?? 0) > 0 + readonly property string wsName: { + const n = hw?.name ?? ""; + return n && n !== String(wsId) && !n.startsWith("special") ? n : ""; + } + + spacing: 6 + + Rectangle { + implicitWidth: 108 + implicitHeight: 68 // 16:10-ish + radius: Theme.radius.tile + color: Qt.rgba(0, 0, 0, cell.occupied ? 0.35 : 0.5) + border.width: cell.active ? 2 : 1 + border.color: cell.active ? Theme.accent : Theme.panelBorder + + MaterialIcon { + anchors.centerIn: parent + visible: cell.occupied + text: "web_asset" + color: Theme.textFaint + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Hypr.dispatch(`workspace ${cell.wsId}`); + NsShell.close(); + } + } + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 5 + + StyledText { + text: cell.wsId + color: cell.active ? Theme.accent : Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + font.weight: cell.active ? Font.DemiBold : Font.Normal + } + StyledText { + visible: cell.wsName !== "" + text: cell.wsName + color: cell.active ? Theme.accent : Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + } + } + } + } +} diff --git a/modules/nsbar/panels/NsPowerMenu.qml b/modules/nsbar/panels/NsPowerMenu.qml new file mode 100644 index 00000000..f657d9c3 --- /dev/null +++ b/modules/nsbar/panels/NsPowerMenu.qml @@ -0,0 +1,143 @@ +pragma ComponentBehavior: Bound + +// NoSignal Power Menu — full-screen modal overlay. Big clock + date, then 5 +// action tiles (Lock, Suspend, Log Out, Reboot, Shut Down=danger). Esc / click +// scrim cancels. Rendered full-screen by NsOverlay (open === "power"). + +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.services +import qs.components + +Item { + id: root + + function run(cmd: string): void { + Quickshell.execDetached(["sh", "-c", cmd]); + NsShell.close(); + } + + // scrim + Rectangle { + anchors.fill: parent + color: Theme.overlayBg + + MouseArea { + anchors.fill: parent + onClicked: NsShell.close() + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: 28 + + // big clock + date + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 2 + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Time.format("hh:mm") + color: Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.clockBig + font.weight: Font.Light + font.letterSpacing: Theme.font.trackingClockBig + } + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Time.format("dddd, d MMMM") + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.body + } + } + + // action tiles + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 14 + + Tile { + icon: "lock" + label: "Lock" + onActivated: root.run("loginctl lock-session") + } + Tile { + icon: "bedtime" + label: "Suspend" + onActivated: root.run("systemctl suspend") + } + Tile { + icon: "logout" + label: "Log Out" + onActivated: root.run("hyprctl dispatch exit") + } + Tile { + icon: "restart_alt" + label: "Reboot" + onActivated: root.run("systemctl reboot") + } + Tile { + icon: "power_settings_new" + label: "Shut Down" + danger: true + onActivated: root.run("systemctl poweroff") + } + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: "Press Esc to cancel" + color: Theme.textFaint + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + } + + component Tile: Rectangle { + id: tile + + property string icon + property string label + property bool danger: false + signal activated + + implicitWidth: 96 + implicitHeight: 96 + radius: Theme.radius.panel + color: ma.containsMouse ? Theme.accentSoft : Theme.panelBg + border.width: 1 + border.color: ma.containsMouse ? (tile.danger ? Theme.danger : Theme.accent) : Theme.panelBorder + + ColumnLayout { + anchors.centerIn: parent + spacing: 10 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: tile.icon + color: tile.danger ? Theme.danger : Theme.text + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: tile.label + color: tile.danger ? Theme.danger : Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + } + + MouseArea { + id: ma + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: tile.activated() + } + } +} diff --git a/modules/nsbar/panels/NsQuickSettings.qml b/modules/nsbar/panels/NsQuickSettings.qml new file mode 100644 index 00000000..aeafd188 --- /dev/null +++ b/modules/nsbar/panels/NsQuickSettings.qml @@ -0,0 +1,382 @@ +pragma ComponentBehavior: Bound + +// NoSignal Quick Settings popout (off the media / battery pill). Avatar + user + +// uptime + power button; 2-col toggle grid; brightness + volume sliders; media +// card. Wired to the real services (Network, Bluetooth, Notifs, Audio, +// Brightness, Players); Night Light / Airplane / VPN are stateful toggles for now. + +import QtQuick +import QtQuick.Layouts +import Quickshell.Bluetooth +import qs.services +import qs.utils +import qs.components + +NsPanel { + id: root + + implicitWidth: 380 + anchorMode: "right" + + readonly property var brightMon: Brightness.monitors[0] ?? null + + // ── Header ─────────────────────────────────────────────────────────── + RowLayout { + Layout.fillWidth: true + spacing: 12 + + Rectangle { + implicitWidth: 40 + implicitHeight: 40 + radius: 20 + gradient: Gradient { + orientation: Gradient.Vertical + GradientStop { + position: 0 + color: Theme.accent + } + GradientStop { + position: 1 + color: Theme.blue + } + } + + StyledText { + anchors.centerIn: parent + text: (SysInfo.user || "?").charAt(0).toUpperCase() + color: Theme.onAccent + font.family: Theme.font.family + font.pixelSize: Theme.font.body + font.weight: Font.Bold + } + } + + ColumnLayout { + spacing: 0 + StyledText { + text: SysInfo.user || "user" + color: Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.panelTitle + font.weight: Font.DemiBold + } + StyledText { + text: "uptime " + SysInfo.uptime + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + } + + Item { + Layout.fillWidth: true + } + + Rectangle { + implicitWidth: 34 + implicitHeight: 34 + radius: 17 + color: pwrMa.containsMouse ? Theme.accentSoft : Theme.fillSubtle + + MaterialIcon { + anchors.centerIn: parent + text: "power_settings_new" + color: Theme.danger + } + + MouseArea { + id: pwrMa + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: NsShell.show("power", 0) + } + } + } + + // ── Toggle grid ────────────────────────────────────────────────────── + GridLayout { + Layout.fillWidth: true + columns: 2 + rowSpacing: 8 + columnSpacing: 8 + + QSToggle { + icon: "wifi" + label: "Wi-Fi" + sub: Network.wifiEnabled ? Network.active?.ssid ?? "On" : "Off" + on: Network.wifiEnabled + onToggled: Network.toggleWifi() + } + QSToggle { + icon: "bluetooth" + label: "Bluetooth" + sub: Bluetooth.defaultAdapter?.enabled ? "On" : "Off" + on: Bluetooth.defaultAdapter?.enabled ?? false + onToggled: if (Bluetooth.defaultAdapter) + Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled + } + QSToggle { + icon: "do_not_disturb_on" + label: "Do Not Disturb" + sub: Notifs.dnd ? "On" : "Off" + on: Notifs.dnd + onToggled: Notifs.dnd = !Notifs.dnd + } + QSToggle { + id: nightTog + icon: "nightlight" + label: "Night Light" + sub: on ? "Warm 3500K" : "Off" + onToggled: on = !on + } + QSToggle { + id: airTog + icon: "airplanemode_active" + label: "Airplane" + sub: on ? "On" : "Off" + onToggled: on = !on + } + QSToggle { + id: vpnTog + icon: "vpn_key" + label: "VPN" + sub: on ? "On" : "Off" + onToggled: on = !on + } + } + + // ── Sliders ────────────────────────────────────────────────────────── + QSSlider { + icon: "brightness_6" + value: root.brightMon?.brightness ?? 0 + visible: root.brightMon !== null + onMoved: v => root.brightMon?.setBrightness(v) + } + + QSSlider { + icon: Audio.muted ? "volume_off" : "volume_up" + value: Audio.volume + onMoved: v => Audio.setVolume(v) + } + + // ── Media card ─────────────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 4 + implicitHeight: 80 + radius: Theme.radius.tile + color: Theme.fillSubtle + visible: Players.active !== null + + RowLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 12 + + Rectangle { + implicitWidth: 56 + implicitHeight: 56 + radius: 0 + gradient: Gradient { + orientation: Gradient.Vertical + GradientStop { + position: 0 + color: Theme.accent + } + GradientStop { + position: 1 + color: Theme.blue + } + } + MaterialIcon { + anchors.centerIn: parent + text: "music_note" + color: Theme.onAccent + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + StyledText { + Layout.fillWidth: true + text: Players.active?.trackTitle || "Awake" + color: Theme.text + elide: Text.ElideRight + font.family: Theme.font.family + font.pixelSize: Theme.font.body + font.weight: Font.DemiBold + } + StyledText { + Layout.fillWidth: true + text: Players.active?.trackArtist || "" + color: Theme.textMuted + elide: Text.ElideRight + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + + RowLayout { + spacing: 16 + Layout.topMargin: 2 + + MediaBtn { + icon: "skip_previous" + onActivated: Players.previous() + } + MediaBtn { + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + onActivated: Players.togglePlaying() + } + MediaBtn { + icon: "skip_next" + onActivated: Players.next() + } + } + } + } + } + + // ── Reusable bits ──────────────────────────────────────────────────── + component QSToggle: Rectangle { + id: tile + + property string icon + property string label + property string sub + property bool on: false + signal toggled + + Layout.fillWidth: true + implicitHeight: 56 + radius: Theme.radius.button + color: tile.on ? Theme.accentSoft : Theme.fillSubtle + border.width: tile.on ? 1 : 0 + border.color: Theme.accent + + RowLayout { + anchors.fill: parent + anchors.margins: 11 + spacing: 10 + + Rectangle { + implicitWidth: 30 + implicitHeight: 30 + radius: 15 + color: tile.on ? Theme.accent : Qt.rgba(1, 1, 1, 0.06) + + MaterialIcon { + anchors.centerIn: parent + text: tile.icon + color: tile.on ? Theme.onAccent : Theme.text + } + } + + ColumnLayout { + spacing: 0 + StyledText { + text: tile.label + color: Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + font.weight: Font.Medium + } + StyledText { + text: tile.sub + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: tile.toggled() + } + } + + component QSSlider: RowLayout { + id: s + + property string icon + property real value: 0 + signal moved(real v) + + Layout.fillWidth: true + spacing: 10 + + MaterialIcon { + text: s.icon + color: Theme.textMuted + } + + Rectangle { + id: track + + Layout.fillWidth: true + implicitHeight: 6 + radius: 3 + color: Theme.fillSubtle + + Rectangle { + width: parent.width * Math.max(0, Math.min(1, s.value)) + height: parent.height + radius: 3 + color: Theme.accent + } + + Rectangle { + x: parent.width * Math.max(0, Math.min(1, s.value)) - width / 2 + anchors.verticalCenter: parent.verticalCenter + implicitWidth: 14 + implicitHeight: 14 + radius: 7 + color: Theme.accent + } + + MouseArea { + anchors.fill: parent + onPressed: e => s.moved(Math.max(0, Math.min(1, e.x / width))) + onPositionChanged: e => { + if (pressed) + s.moved(Math.max(0, Math.min(1, e.x / width))); + } + } + } + + StyledText { + text: Math.round(s.value * 100) + "%" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } + } + + component MediaBtn: Rectangle { + property string icon + signal activated + + implicitWidth: 28 + implicitHeight: 28 + radius: 14 + color: mbm.containsMouse ? Theme.hover : "transparent" + + MaterialIcon { + anchors.centerIn: parent + text: parent.icon + color: Theme.text + } + + MouseArea { + id: mbm + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: parent.activated() + } + } +} diff --git a/modules/nsbar/panels/NsSystemTray.qml b/modules/nsbar/panels/NsSystemTray.qml new file mode 100644 index 00000000..52c3c1b3 --- /dev/null +++ b/modules/nsbar/panels/NsSystemTray.qml @@ -0,0 +1,100 @@ +pragma ComponentBehavior: Bound + +// NoSignal System Tray popout (off the tray pill). Grid of StatusNotifierItems; +// left-click activates, right-click opens the item's menu. Uses SystemTray. + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.SystemTray +import qs.services +import qs.components + +NsPanel { + id: root + + implicitWidth: 260 + anchorMode: "right" + + StyledText { + text: "System Tray" + color: Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.panelTitle + font.weight: Font.DemiBold + } + + GridLayout { + Layout.fillWidth: true + columns: 4 + rowSpacing: 8 + columnSpacing: 8 + visible: SystemTray.items.values.length > 0 + + Repeater { + model: SystemTray.items + + delegate: ColumnLayout { + id: cell + + required property var modelData + + Layout.alignment: Qt.AlignHCenter + spacing: 4 + + Rectangle { + Layout.alignment: Qt.AlignHCenter + implicitWidth: 44 + implicitHeight: 44 + radius: Theme.radius.tile + color: cellMa.containsMouse ? Theme.hover : Theme.fillSubtle + + Image { + anchors.centerIn: parent + width: 22 + height: 22 + source: cell.modelData?.icon ?? "" + sourceSize.width: 44 + sourceSize.height: 44 + fillMode: Image.PreserveAspectFit + } + + MouseArea { + id: cellMa + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + onClicked: mouse => { + if (mouse.button === Qt.RightButton) + cell.modelData?.display(cell, 0, height); + else if (cell.modelData?.onlyMenu) + cell.modelData?.display(cell, 0, height); + else + cell.modelData?.activate(); + } + } + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: 50 + text: cell.modelData?.title || cell.modelData?.id || "" + color: Theme.textMuted + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } + } + } + } + + StyledText { + visible: SystemTray.items.values.length === 0 + text: "No tray items" + color: Theme.textMuted + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } +} diff --git a/services/NsShell.qml b/services/NsShell.qml new file mode 100644 index 00000000..aa07cfc8 --- /dev/null +++ b/services/NsShell.qml @@ -0,0 +1,35 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +// NoSignal shell-level popup state. "One panel open at a time" — `open` is the +// id of the active panel ("" = none). `anchorX` is the scene x of the trigger +// (a bar pill) center, used to position under-trigger / right-anchored popups. +// The NsBar pills call toggle(); the NsOverlay reads `open` and renders the panel. + +import QtQuick +import Quickshell + +Singleton { + id: root + + property string open: "" + property real anchorX: 0 + + function toggle(id: string, x: real): void { + if (root.open === id) { + root.open = ""; + } else { + root.open = id; + root.anchorX = x; + } + } + + function show(id: string, x: real): void { + root.open = id; + root.anchorX = x; + } + + function close(): void { + root.open = ""; + } +} diff --git a/services/Theme.qml b/services/Theme.qml new file mode 100644 index 00000000..f90f7e86 --- /dev/null +++ b/services/Theme.qml @@ -0,0 +1,91 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +// NoSignal design-system tokens (interface redesign 2026-06-14). +// Fixed dark-glass chrome + text, but the ACCENT is Material You (wallpaper- +// derived) per the user's choice. Components in the redesigned bar/panels read +// from here instead of hard-coding hex values. See the design handoff README. +// +// NB: the "backdrop blur" in the spec is done at the compositor level (Hyprland +// layerrules blur on the shell namespaces), not in QML — these fills are the +// translucent surface colours that sit over that blur. + +import QtQuick +import Quickshell +import qs.services + +Singleton { + id: root + + // ── Accent: Material You, dynamic from wallpaper ────────────────────── + readonly property color accent: Colours.palette.m3primary + readonly property color onAccent: Colours.palette.m3onPrimary // contrasting text/icon on accent fills + // accent.soft — translucent accent for active-module bg / selected rows + readonly property color accentSoft: Qt.alpha(accent, 0.15) + readonly property color accentSofter: Qt.alpha(accent, 0.13) + + // ── Secondary semantic accents (fixed) ─────────────────────────────── + readonly property color blue: "#8FB5D6" // avatar gradient, event tint + readonly property color green: "#86C09A" // now-playing tint + readonly property color danger: "#E0746A" // power button + Shut Down + + // ── Surfaces: fixed dark glass ─────────────────────────────────────── + readonly property color barBg: Qt.rgba(15 / 255, 18 / 255, 24 / 255, 0.60) + readonly property color barBorder: Qt.rgba(1, 1, 1, 0.06) + readonly property color panelBg: Qt.rgba(17 / 255, 20 / 255, 26 / 255, 0.90) + readonly property color panelBorder: Qt.rgba(1, 1, 1, 0.09) + readonly property color overlayBg: Qt.rgba(8 / 255, 10 / 255, 14 / 255, 0.82) + + // panel drop shadow + inset highlight + readonly property color shadow: Qt.rgba(0, 0, 0, 0.55) + readonly property color insetHighlight: Qt.rgba(1, 1, 1, 0.05) + + // ── Text (fixed) ───────────────────────────────────────────────────── + readonly property color text: Qt.rgba(233 / 255, 238 / 255, 243 / 255, 0.92) + readonly property color textMuted: Qt.rgba(233 / 255, 238 / 255, 243 / 255, 0.50) + readonly property color textFaint: Qt.rgba(233 / 255, 238 / 255, 243 / 255, 0.34) + + // ── Fills / states (fixed) ─────────────────────────────────────────── + readonly property color hover: Qt.rgba(1, 1, 1, 0.07) + readonly property color fillSubtle: Qt.rgba(1, 1, 1, 0.05) + + // ── Typography ─────────────────────────────────────────────────────── + readonly property string fontFamily: "JetBrainsMono Nerd Font" + readonly property QtObject font: QtObject { + readonly property string family: root.fontFamily + readonly property int bar: 12 + readonly property int panelTitle: 13 + readonly property int body: 12 + readonly property int bodySmall: 11 + readonly property int label: 10 // section labels (uppercase, tracked) + readonly property int meta: 10 // times, %, device sub + readonly property int clockBig: 52 // power-menu clock + readonly property real trackingLabel: 1.6 + readonly property real trackingBar: -0.1 + readonly property real trackingClockBig: 2 + } + + // ── Shape ──────────────────────────────────────────────────────────── + // NoSignal: no curved corners anywhere — all rectangular elements are square. + readonly property QtObject radius: QtObject { + readonly property int panel: 0 + readonly property int button: 0 + readonly property int barModule: 0 + readonly property int tile: 0 + readonly property int full: 1000 // genuine circles only (toggle icons, avatar, switch knob) + } + + // ── Spacing / sizing ───────────────────────────────────────────────── + readonly property QtObject size: QtObject { + readonly property int barHeight: 38 // exclusive zone + readonly property int popoutTopRight: 44 // right-anchored panels + readonly property int popoutTopCenter: 48 // centered panels + readonly property int panelPad: 16 // 14-18 + readonly property int gap: 12 // 8-16 + readonly property int rowMin: 30 // panel row hit target + readonly property int barIcon: 16 // 15-17 + } + + // ── Animation ──────────────────────────────────────────────────────── + readonly property int hoverMs: 120 +} diff --git a/shell.qml b/shell.qml index 273d541c..c3b5a1eb 100644 --- a/shell.qml +++ b/shell.qml @@ -6,6 +6,7 @@ import "modules" import "modules/drawers" +import "modules/nsbar" import "modules/background" import "modules/areapicker" import "modules/lock" @@ -17,7 +18,9 @@ ShellRoot { GSFLoader {} Background {} - Drawers {} + Drawers {} // NoSignal: kept for launcher/OSD/lock; its bar is collapsed (see BarWrapper) + NsBar {} // NoSignal redesign: slim glass top bar + NsOverlay {} // NoSignal redesign: floating popout panels AreaPicker {} Lock { id: lock