nosignal-shell/modules/bar/Bar.qml
28allday b1e2bb3bc6 feat(bar): horizontal top bar — Phase A geometry (NoSignal layout)
Convert the bar from a vertical left-edge strip to a horizontal top bar. The
shell had no orientation abstraction, so this flips the geometry across the bar
and the drawer/region/interaction layer:

- Bar.qml: ColumnLayout -> RowLayout; hit-testing (checkPopout/handleWheel),
  scroll halves and popout-center anchoring all keyed off x instead of y;
  first/last padding -> left/right margins.
- BarWrapper.qml: the bar's "thickness" is now its height (clampedHeight/
  contentHeight/implicitHeight); content anchors left/right/top.
- drawers: Exclusions (exclusive zone moved left->top), Panels (content inset
  topMargin = bar height), Regions + ContentWindow (panel->window coordinate
  mapping and the thick border flip left->top), Interactions (bar-region check
  y<barHeight, panel Y-origin uses bar height, scroll passes x, bar drag-reveal
  is vertical).
- widgets: Clock (Row, HH:MM with a colon), StatusIcons (Row), Workspaces +
  Workspace (Row), Tray (Row, expand chevron at the right) — each sizes to the
  bar height instead of width.

Verified live in the dev VM: top bar renders with centered-clock entries, the
exclusion zone keeps windows below the bar, and the dashboard drops down
correctly. Popout fly-outs still anchor sideways (Phase B).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:26:55 +01:00

217 lines
7.8 KiB
QML

pragma ComponentBehavior: Bound
import "popouts" as BarPopouts
import "components"
import "components/workspaces"
import QtQuick
import QtQuick.Layouts
import Quickshell
import Caelestia.Config
import qs.components
import qs.services
// NoSignal: horizontal top bar. The widgets flow left->right in a RowLayout and
// the bar's "thickness" is its HEIGHT; hit-testing / scroll / popout anchoring all
// key off the x-axis (the position along the bar). Upstream caelestia is a
// vertical left-edge bar — this is the NoSignal layout fork (Phase 3).
RowLayout {
id: root
required property ShellScreen screen
required property DrawerVisibilities visibilities
required property BarPopouts.Wrapper popouts
required property bool fullscreen
readonly property int hPadding: Tokens.padding.large
function closeTray(): void {
if (!Config.bar.tray.compact)
return;
for (let i = 0; i < repeater.count; i++) {
const loader = repeater.itemAt(i) as WrappedLoader;
if (loader?.enabled && loader.id === "tray") {
(loader.item as Tray).expanded = false;
}
}
}
function checkPopout(x: real): void {
const ch = childAt(x, height / 2) as WrappedLoader;
if (ch?.id !== "tray")
closeTray();
if (!ch) {
popouts.hasCurrent = false;
return;
}
const id = ch.id;
const left = ch.x;
if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
const items = (ch.item as StatusIcons).items;
const icon = items.childAt(mapToItem(items, x, 0).x, items.height / 2);
if (icon) {
popouts.currentName = icon.name;
popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, icon.implicitWidth / 2, 0).x);
popouts.hasCurrent = true;
}
} else if (id === "tray" && Config.bar.popouts.tray) {
const tray = ch.item as Tray;
if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, x, tray.implicitHeight / 2)))) {
const index = Math.floor(((x - left - tray.padding * 2 + tray.spacing) / tray.layout.implicitWidth) * tray.items.count);
const trayItem = tray.items.itemAt(index);
if (trayItem) {
popouts.currentName = `traymenu${index}`;
popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, trayItem.implicitWidth / 2, 0).x);
popouts.hasCurrent = true;
} else {
popouts.hasCurrent = false;
}
} else {
popouts.hasCurrent = false;
tray.expanded = true;
}
} else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) {
popouts.currentName = id.toLowerCase();
popouts.currentCenter = (ch.item as Item).mapToItem(root, (ch.item as Item).implicitWidth / 2, 0).x ?? 0;
popouts.hasCurrent = true;
}
}
function handleWheel(x: real, angleDelta: point): void {
const ch = childAt(x, height / 2) as WrappedLoader;
if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) {
// Workspace scroll
const mon = (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor);
const specialWs = mon?.lastIpcObject.specialWorkspace.name;
if (specialWs?.length > 0)
Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`);
else if (angleDelta.y < 0 || (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1)
Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`);
} else if (x < screen.width / 2 && Config.bar.scrollActions.volume) {
// Volume scroll on left half
if (angleDelta.y > 0)
Audio.incrementVolume();
else if (angleDelta.y < 0)
Audio.decrementVolume();
} else if (Config.bar.scrollActions.brightness) {
// Brightness scroll on right half
const monitor = Brightness.getMonitorForScreen(screen);
if (angleDelta.y > 0)
monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement);
else if (angleDelta.y < 0)
monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement);
}
}
spacing: Tokens.spacing.medium
Repeater {
id: repeater
model: Config.bar.entries
DelegateChooser {
role: "id"
DelegateChoice {
roleValue: "spacer"
delegate: WrappedLoader {
Layout.fillWidth: enabled
}
}
DelegateChoice {
roleValue: "logo"
delegate: WrappedLoader {
sourceComponent: OsIcon {}
}
}
DelegateChoice {
roleValue: "workspaces"
delegate: WrappedLoader {
sourceComponent: Workspaces {
screen: root.screen
fullscreen: root.fullscreen
}
}
}
DelegateChoice {
roleValue: "activeWindow"
delegate: WrappedLoader {
Layout.fillWidth: true
visible: !root.fullscreen
sourceComponent: ActiveWindow {
bar: root
monitor: Brightness.getMonitorForScreen(root.screen)
}
}
}
DelegateChoice {
roleValue: "tray"
delegate: WrappedLoader {
visible: !root.fullscreen
sourceComponent: Tray {}
}
}
DelegateChoice {
roleValue: "clock"
delegate: WrappedLoader {
visible: !root.fullscreen
sourceComponent: Clock {}
}
}
DelegateChoice {
roleValue: "statusIcons"
delegate: WrappedLoader {
visible: !root.fullscreen
sourceComponent: StatusIcons {}
}
}
DelegateChoice {
roleValue: "power"
delegate: WrappedLoader {
sourceComponent: Power {
visibilities: root.visibilities
}
}
}
}
}
component WrappedLoader: Loader {
required enabled
required property string id
required property int index
function findFirstEnabled(): Item {
const count = repeater.count;
for (let i = 0; i < count; i++) {
const item = repeater.itemAt(i);
if (item?.enabled)
return item;
}
return null;
}
function findLastEnabled(): Item {
for (let i = repeater.count - 1; i >= 0; i--) {
const item = repeater.itemAt(i);
if (item?.enabled)
return item;
}
return null;
}
asynchronous: true
Layout.alignment: Qt.AlignVCenter
// Cursed ahh thing to add padding to first and last enabled components
Layout.leftMargin: findFirstEnabled() === this ? root.hPadding : 0
Layout.rightMargin: findLastEnabled() === this ? root.hPadding : 0
visible: enabled
active: enabled
}
}