From 86cf291663627e71245df32af469e313f17b7172 Mon Sep 17 00:00:00 2001 From: chlorospingus Date: Wed, 25 Feb 2026 03:09:27 -0800 Subject: [PATCH] Merge status bar and launcher into morphing top bar --- BarShape.qml | 43 ++++++ Launcher.qml | 321 ++++++++++++++++------------------------- Status.qml | 63 ++++++++ StatusBar.qml | 139 ------------------ TopBar.qml | 172 ++++++++++++++++++++++ Widgets/Workspaces.qml | 36 +++++ shell.qml | 2 +- 7 files changed, 436 insertions(+), 340 deletions(-) create mode 100644 BarShape.qml create mode 100644 Status.qml delete mode 100644 StatusBar.qml create mode 100644 TopBar.qml create mode 100644 Widgets/Workspaces.qml diff --git a/BarShape.qml b/BarShape.qml new file mode 100644 index 0000000..478768e --- /dev/null +++ b/BarShape.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Shapes + +Shape { + id: background + preferredRendererType: Shape.CurveRenderer + required property var color + required property var radius + ShapePath { + strokeWidth: 0 + fillColor: color + startX: 0 + startY: 0 + PathArc { + x: radius; y: Math.min(radius, background.height/2) + radiusX: radius; radiusY: y + direction: PathArc.Clockwise + } + PathLine { x: radius; y: background.height - Math.min(radius, background.height/2) } + PathArc { + x: radius * 2; y: background.height + radiusX: radius; radiusY: Math.min(radius, background.height/2) + direction: PathArc.Counterclockwise + } + PathLine { x: background.width - radius * 2; y: background.height } + PathArc { + x: background.width - radius; y: background.height - Math.min(radius, background.height/2) + radiusX: radius; radiusY: Math.min(radius, background.height/2) + direction: PathArc.Counterclockwise + } + PathLine { x: background.width - radius; y: Math.min(radius, background.height/2) } + PathArc { + x: background.width; y: 0 + radiusX: radius; radiusY: Math.min(radius, background.height/2) + direction: PathArc.Clockwise + } + } + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + } +} diff --git a/Launcher.qml b/Launcher.qml index 40b6011..112bac3 100644 --- a/Launcher.qml +++ b/Launcher.qml @@ -5,37 +5,10 @@ import QtQuick import QtQuick.Effects import "./emojis.mjs" as Emojis -PanelWindow { - id: root - anchors { - top: true - } - visible: false - implicitHeight: 400 - implicitWidth: 1000 - exclusionMode: ExclusionMode.Ignore - color: "transparent" - WlrLayershell.layer: WlrLayershell.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive - - IpcHandler { - target: "launcher" - function open() {root.open()} - function clone() {root.close()} - function toggle() {root.visible ? root.close() : root.open()} - } - function open() { - input.text = "" - root.visible = true - exit.stop(); - entry.start(); - } - function close() { - input.text = "" - entry.stop(); - exit.start(); - } - +Item { + height: currentItems.length ? 180 : 60 + width: 700 + property var show: input.text.length property var currentItems: { if (input.text.length === 0 || input.text === ":") { return [] @@ -55,194 +28,142 @@ PanelWindow { Rectangle { anchors { top: parent.top - horizontalCenter: parent.horizontalCenter - topMargin: 100 + left: parent.left + right: parent.right + margins: 6 } + height: 48 color: "#181818" - height: currentItems.length ? 180 : 60 - Behavior on height { NumberAnimation { - duration: 300 - easing.type: Easing.OutQuint + radius: 10 + TextInput { + id: input + anchors.verticalCenter: parent.verticalCenter + x: parent.width/2 - this.width/2 + Behavior on x {NumberAnimation {duration: 300; easing.type: Easing.OutQuint}} + focus: true + font.pixelSize: 20 + color: "white" + cursorDelegate: Item {} + onCursorPositionChanged: cursorPosition = text.length + Keys.onPressed: (event) => { switch (event.key) { + case Qt.Key_Escape: + root.close(); + break; + case Qt.Key_Return: + list.currentItem.execute(); + root.close(); + break; + case Qt.Key_Right: + list.incrementCurrentIndex(); + break; + case Qt.Key_Left: + list.decrementCurrentIndex(); + break; + }} + } + } + ListView { + id: list + anchors { + top: input.parent.bottom + left: parent.left + right: parent.right + margins: 6 + } + height: 114 + highlightFollowsCurrentItem: true + onCurrentIndexChanged: background.indexChanged() + orientation: ListView.Horizontal + spacing: 6 + + add: Transition { NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 150 }} - width: 700 - radius: 12 - - RectangularShadow { - z: -1 - anchors.fill: parent - radius: 12 - blur: 10 - spread: 2 - } - Rectangle { - anchors { - top: parent.top - left: parent.left - right: parent.right - margins: 6 + remove: Transition { NumberAnimation { + property: "opacity" + from: 1 + to: 0 + duration: 150 + }} + displaced: Transition { + NumberAnimation { + property: "x" + duration: 300 + easing.type: Easing.OutQuint + onStarted: console.log("displaced") } - height: 48 - color: "#222222" - radius: 10 - TextInput { - id: input - anchors.verticalCenter: parent.verticalCenter - x: parent.width/2 - this.width/2 - Behavior on x {NumberAnimation {duration: 300; easing.type: Easing.OutQuint}} - focus: true - font.pixelSize: 20 - color: "white" - cursorDelegate: Item {} - onCursorPositionChanged: cursorPosition = text.length - Keys.onPressed: (event) => { switch (event.key) { - case Qt.Key_Escape: - root.close(); - break; - case Qt.Key_Return: - list.currentItem.execute(); - root.close(); - break; - case Qt.Key_Right: - list.incrementCurrentIndex(); - break; - case Qt.Key_Left: - list.decrementCurrentIndex(); - break; - }} - } - } - ListView { - id: list - anchors { - top: input.parent.bottom - left: parent.left - right: parent.right - margins: 6 - } - height: 114 - highlightFollowsCurrentItem: true - orientation: ListView.Horizontal - spacing: 6 - - add: Transition { NumberAnimation { + NumberAnimation { property: "opacity" - from: 0 to: 1 duration: 150 - }} - remove: Transition { NumberAnimation { - property: "opacity" - from: 1 - to: 0 - duration: 150 - }} - displaced: Transition { - NumberAnimation { - property: "x" - duration: 300 - easing.type: Easing.OutQuint - onStarted: console.log("displaced") - } - NumberAnimation { - property: "opacity" - to: 1 - duration: 150 - } } + } - model: ScriptModel {values: currentItems.slice(0, 5)} - delegate: Rectangle { - id: item + model: ScriptModel {values: currentItems.slice(0, 5)} + delegate: Rectangle { + id: item + anchors { + top: parent?.top + bottom: parent?.bottom + } + width: (700 - 6*6)/5 + color: "#20181818" + ListView.onIsCurrentItemChanged: ListView.isCurrentItem ? selectAnim.start() : deselectAnim.start() + property var selectAnim: ColorAnimation { + target: item + property: "color" + to: "#10ffffff" + duration: 200 + easing.type: Easing.OutQuint + } + property var deselectAnim: ColorAnimation { + target: item + onStarted: selectAnim.stop() + property: "color" + to: "#00181818" + duration: 200 + easing.type: Easing.InQuint + } + radius: 10 + function execute() { + modelData.emoji + ? Quickshell.clipboardText = modelData.emoji + : modelData.execute() + } + Column { anchors { - top: parent?.top - bottom: parent?.bottom + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter } - width: (700 - 6*6)/5 - color: "#20181818" - ListView.onIsCurrentItemChanged: ListView.isCurrentItem ? selectAnim.start() : deselectAnim.start() - property var selectAnim: ColorAnimation { - target: item - property: "color" - to: "#10ffffff" - duration: 200 - easing.type: Easing.OutQuint + Image { + visible: !modelData.emoji + anchors.horizontalCenter: parent.horizontalCenter + width: 64 + height: width + source: Quickshell.iconPath(modelData.icon) } - property var deselectAnim: ColorAnimation { - target: item - onStarted: selectAnim.stop() - property: "color" - to: "#00181818" - duration: 200 - easing.type: Easing.InQuint + Text { + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: 64 + text: modelData.emoji ?? "" } - radius: 10 - function execute() { - modelData.emoji - ? Quickshell.clipboardText = modelData.emoji - : modelData.execute() - } - Column { + Text { anchors { left: parent.left right: parent.right - verticalCenter: parent.verticalCenter - } - Image { - visible: !modelData.emoji - anchors.horizontalCenter: parent.horizontalCenter - width: 64 - height: width - source: Quickshell.iconPath(modelData.icon) - } - Text { - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: 64 - text: modelData.emoji ?? "" - } - Text { - anchors { - left: parent.left - right: parent.right - } - width: parent.width - 12 - text: modelData.name - color: "white" - font.pixelSize: 16 - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight } + width: parent.width - 12 + text: modelData.name + color: "white" + font.pixelSize: 16 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight } } } - transform: Translate { - y: -50 - NumberAnimation on y { - id: entry - to: 0 - duration: 300 - easing.type: Easing.OutQuint - } - NumberAnimation on y { - id: exit - to: -50 - duration: 300 - easing.type: Easing.InQuint - onFinished: root.visible = false - } - } - opacity: 0 - NumberAnimation on opacity { - running: entry.running - to: 1 - duration: entry.duration/2 - } - SequentialAnimation on opacity { - running: exit.running - PauseAnimation {duration: exit.duration/2} - NumberAnimation { - to: 0 - duration: exit.duration/2 - } - } } } diff --git a/Status.qml b/Status.qml new file mode 100644 index 0000000..d9cf0dd --- /dev/null +++ b/Status.qml @@ -0,0 +1,63 @@ +import Quickshell +import QtQuick +import "./Widgets" as Widgets + +Item { + width: 800 + height: 36 + Row { + anchors { + top: parent.top + left: parent.left + bottom: parent.bottom + } + Item { + anchors { + top: parent.top + bottom: parent.bottom + } + width: height + Rectangle { + anchors { + fill: parent + margins: 4 + } + radius: 12 + color: parent.hover.hovered ? "#333333" : "#222222" + Image { + anchors.centerIn: parent + width: 20 + height: width + source: Qt.resolvedUrl("./arch.svg") + sourceSize {width: width; height: height} + } + } + property var hover: HoverHandler {} + property var click: TapHandler { + onTapped: launcher.open() + } + } + } + Widgets.Workspaces { + height: parent.height + anchors { + top: parent.top + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + } + Row { + anchors { + top: parent.top + right: parent.right + bottom: parent.bottom + rightMargin: 12 + } + Text { + anchors.verticalCenter: parent.verticalCenter + property var clock: SystemClock {} + color: "white" + text: Qt.formatDateTime(clock.date, "ddd MMM dd · hh:mm") + } + } +} diff --git a/StatusBar.qml b/StatusBar.qml deleted file mode 100644 index b848f6c..0000000 --- a/StatusBar.qml +++ /dev/null @@ -1,139 +0,0 @@ -import Quickshell -import Quickshell.Hyprland -import Quickshell.Wayland -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import QtQuick.Shapes -import "." as Shell - -PanelWindow { - id: root - anchors { - top: true - } - implicitWidth: 1024 - implicitHeight: 1 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - WlrLayershell.layer: WlrLayer.Overlay - exclusionMode: ExclusionMode.Ignore - property real radius: 16 - property var bgColor: "#222222" - property int fullHeight: 48 - color: "transparent" - Shape { - id: background - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - width: 1000 - height: 0 - property real radius: 12 - property var color: "white" - preferredRendererType: Shape.CurveRenderer - NumberAnimation on height { - id: entry - running: false - to: fullHeight - duration: 300 - easing.type: Easing.OutBack - easing.overshoot: 1.5 - onStarted: root.implicitHeight = 60 - } - NumberAnimation on height { - id: exit - running: false - to: 0 - duration: 300 - easing.type: Easing.InCubic - onFinished: root.implicitHeight = 1 - } - ShapePath { - strokeWidth: 0 - fillColor: root.bgColor - startX: 0 - startY: 0 - PathArc { - x: root.radius; y: Math.min(root.radius, background.height/2) - radiusX: root.radius; radiusY: y - direction: PathArc.Clockwise - } - PathLine { x: root.radius; y: background.height - Math.min(root.radius, background.height/2) } - PathArc { - x: root.radius * 2; y: background.height - radiusX: root.radius; radiusY: Math.min(root.radius, background.height/2) - direction: PathArc.Counterclockwise - } - PathLine { x: background.width - root.radius * 2; y: background.height } - PathArc { - x: background.width - root.radius; y: background.height - Math.min(root.radius, background.height/2) - radiusX: root.radius; radiusY: Math.min(root.radius, background.height/2) - direction: PathArc.Counterclockwise - } - PathLine { x: background.width - root.radius; y: Math.min(root.radius, background.height/2) } - PathArc { - x: background.width; y: 0 - radiusX: root.radius; radiusY: Math.min(root.radius, background.height/2) - direction: PathArc.Clockwise - } - } - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - } - } - Row { - anchors { - left: background.left - bottom: background.bottom - leftMargin: root.radius * 2 - } - height: fullHeight - Button { - anchors { - top: parent.top - bottom: parent.bottom - } - width: 20 - background: Image { - anchors.centerIn: parent - width: 20 - height: width - source: Qt.resolvedUrl("./arch.svg") - sourceSize {width: width; height: height} - } - } - } - Shell.Workspaces { - height: fullHeight - anchors { - horizontalCenter: background.horizontalCenter - bottom: background.bottom - } - } - Row { - anchors { - right: background.right - bottom: background.bottom - rightMargin: root.radius * 2 - } - height: fullHeight - Text { - property var clock: SystemClock {} - anchors.verticalCenter: parent.verticalCenter - color: "white" - text: Qt.formatDateTime(clock.date, "ddd MMM dd · hh:mm") - } - } - MouseArea { - anchors.fill: background - hoverEnabled: true - onExited: { - entry.stop() - exit.start() - } - } - HoverHandler { onHoveredChanged: if (hovered) { - exit.stop() - entry.start() - }} -} diff --git a/TopBar.qml b/TopBar.qml new file mode 100644 index 0000000..5987916 --- /dev/null +++ b/TopBar.qml @@ -0,0 +1,172 @@ +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import Quickshell.Wayland +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Shapes +import "." as Shell + +PanelWindow { + id: root + anchors { + top: true + } + implicitWidth: 1024 + implicitHeight: 200 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + WlrLayershell.layer: WlrLayer.Overlay + exclusionMode: ExclusionMode.Ignore + color: "transparent" + property bool up: false + + IpcHandler { + target: "topbar" + function open() { root.open() } + function close() { root.close() } + function toggle() { + root.up ? close() : open() + } + } + function open() { + background.index = 0 + background.entry.start() + } + function close() { + background.exit.start() + } + + Shell.BarShape { + id: background + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + color: "#222222" + radius: 16 + width: children[0].width + radius*2 + height: children[0].height + component DefaultTransition: Behavior { + enabled: root.up + NumberAnimation { + duration: 500 + easing.type: Easing.OutQuint + } + } + DefaultTransition on width { } + DefaultTransition on height { } + property int index: 0 + HoverHandler { onHoveredChanged: if (!hovered) { + root.close() + }} + onIndexChanged: { + width = children[index].width + radius*2 + height = children[index].height + for (const [i, child] of children.entries()) { + if (i === index) { + if (root.up) child.fadeIn.start() + else child.opacity = 1 + } + else { + if (root.up) child.fadeOut.start() + else child.opacity = 0 + } + } + } + + component Display: Item { + id: display + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + } + width: children[0].width + height: children[0].height + opacity: 0 + property var fadeIn: SequentialAnimation { + PauseAnimation {duration: 200} + NumberAnimation { + target: display + property: "opacity" + to: 1 + duration: 300 + easing.type: Easing.OutQuint + } + onStarted: fadeOut.stop() + } + property var fadeOut: NumberAnimation { + target: display + property: "opacity" + to: 0 + duration: 300 + onStarted: fadeIn.stop() + easing.type: Easing.OutQuint + } + property var entry: NumberAnimation { + target: display + property: "anchors.topMargin" + to: 0 + duration: 300 + easing.type: Easing.OutQuint + onStarted: exit.stop() + } + property var exit: NumberAnimation { + target: display + property: "anchors.topMargin" + to: -height + duration: 300 + easing.type: Easing.InQuint + onStarted: entry.stop() + } + } + Display { opacity: 1; Shell.Status { + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + } + } } + Display { Shell.Launcher { + onShowChanged: if (show) background.index = 1; else background.index = 0 + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + } + } } + + property var entry: NumberAnimation { + target: background + property: "height" + to: background.children[background.index].height + duration: 300 + easing.type: Easing.OutQuint + onStarted: { + root.implicitHeight = 200 + background.exit.stop() + root.up = true + for (const child of background.children) { + child.entry.start() + } + } + } + property var exit: NumberAnimation { + target: background + property: "height" + to: 0 + duration: 300 + easing.type: Easing.InQuint + onStarted: { + background.entry.stop() + for (const child of background.children) { + child.exit.start() + } + } + onFinished: { + root.implicitHeight = 1 + root.up = false + background.index = 0 + } + } + } + HoverHandler { onHoveredChanged: if (hovered) { + root.open() + }} +} diff --git a/Widgets/Workspaces.qml b/Widgets/Workspaces.qml new file mode 100644 index 0000000..58a4f0e --- /dev/null +++ b/Widgets/Workspaces.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Shapes +import Quickshell.Hyprland + +Row { + Repeater { + model: 9 + delegate: Shape { + height: 14 + width: 24 + property var workspace: Hyprland.workspaces.values.find(ws => ws.id === modelData+1) + preferredRendererType: Shape.CurveRenderer + anchors.verticalCenter: parent.verticalCenter + ShapePath { + strokeWidth: workspace ? 7 : 4 + Behavior on strokeWidth {NumberAnimation { + duration: 150 + }} + strokeColor: "white" + fillColor: "transparent" + PathAngleArc { + moveToStart: true + centerX: width/2 + centerY: height/2 + startAngle: 0 + sweepAngle: 360 + radiusX: workspace ? 3.5 : 5 + Behavior on radiusX {NumberAnimation { + duration: 150 + }} + radiusY: radiusX + } + } + } + } +} diff --git a/shell.qml b/shell.qml index 1ed9ba4..b1a8897 100644 --- a/shell.qml +++ b/shell.qml @@ -5,7 +5,7 @@ import "." as Shell ShellRoot { Shell.Wall {} Shell.Launcher {} - Shell.StatusBar {} + Shell.TopBar {} Shell.Boateye {} Shell.Lock { id: lock