From 4e7f2d0a372741a2eaa56dfb713d3c01fb62aed9 Mon Sep 17 00:00:00 2001 From: chlorospingus Date: Sat, 27 Dec 2025 22:52:01 -0800 Subject: [PATCH] Add launcher --- shell.qml | 338 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 shell.qml diff --git a/shell.qml b/shell.qml new file mode 100644 index 0000000..aa5642f --- /dev/null +++ b/shell.qml @@ -0,0 +1,338 @@ +import QtQml +import QtQml.Models +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick.Shapes +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets + +PanelWindow { + id: root + + function hide() { + visible = false + searchInput.text = "" + list.visible = false + } + + IpcHandler { + target: "launcher" + function show() { + visible = true + list.visible = true + } + function hide() { + root.hide() + } + function toggle() { visible ? hide() : show() } + } + + implicitWidth: 800 + exclusiveZone: 0 + focusable: true + anchors { + right: true + top: true + bottom: true + } + margins { + top: { + if (ToplevelManager.activeToplevel?.fullscreen) { + return 0 + } + return 36; + } + } + readonly property int radius: 20 + readonly property var font: { + family: "comfortaa" + } + readonly property int entry_height: 100 + color: "transparent" + Item { + anchors.fill: parent + Keys.onPressed: (event) => { + switch (event.key) { + case Qt.Key_Down: + list.incrementCurrentIndex() + break; + case Qt.Key_Up: + list.decrementCurrentIndex() + break; + case Qt.Key_Escape: + root.visible = false + break; + case Qt.Key_Return: + list.currentItem.execute() + root.hide() + } + } + + ListView { + id: list + spacing: 6 + anchors.fill: parent + layer.enabled: true + highlightMoveDuration: 150 + highlightRangeMode: ListView.StrictlyEnforceRange + preferredHighlightBegin: 120 + preferredHighlightEnd: this.height/2 + 200 + property var currentApps: { + let apps = Array.from(DesktopEntries.applications.values); + apps.sort((a, b) => a.name.localeCompare(b.name)); + if (searchInput.text.length === 0) { + return apps; + } + return apps.filter(app => { + return app.name.toLowerCase().search(searchInput.text.toLowerCase()) != -1 + }); + } + + + model: currentApps + delegate: Item { + height: entry_height + width: root.width + 10 + property var leftMargin: (Math.pow(Math.abs(this.y - list.contentY - root.height/2), 1.8) / 1400) + anchors { + left: parent?.left + leftMargin: leftMargin + selectedOffset + } + z: -index + property real selectedOffset: ListView.isCurrentItem ? 16 : 64 + function execute() { modelData.execute() } + Behavior on selectedOffset { + NumberAnimation {duration: 500; easing.type: Easing.OutQuint} + } + Button { + id: button + anchors { + fill: parent + // left: parent.left + // right: parent.right + // verticalCenter: parent.verticalCenter + } + onClicked: modelData.execute() + Component.onCompleted: { + } + + // Calculate background color based on average color of icon + Canvas { + id: canvas + width: icon.width + height: icon.height + visible: false + onImageLoaded: { + console.log("loaded"); + requestPaint(); + } + Component.onCompleted: loadImage(button.iconPath) + } + property color color: { + if (!canvas.available) { + return null + } + const ctx = canvas.getContext("2d"); + ctx.drawImage(button.iconPath, 0, 0, canvas.width, canvas.height) + const pixels = ctx.getImageData(0, 0, 100, 100).data; + let avg = [0, 0, 0, 0]; + let count = 0; + for (let i = 0; i < pixels.length; i += 4) { + avg[0] += pixels[i+0]; + avg[1] += pixels[i+1]; + avg[2] += pixels[i+2]; + count += pixels[i+3]; + } + return Qt.rgba(avg[0]/count, avg[1]/count, avg[2]/count, 1); + } + property var iconPath: Quickshell.iconPath(modelData.icon) + + // Background gradient + Rectangle { + anchors.fill: parent + radius: root.radius + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { + color: Qt.darker(button.color, 16) + position: 0 + } + GradientStop { + color: Qt.darker(button.color, 4) + position: 1 + } + } + } + + // Triangles + Item { + id: triangles + anchors.fill: parent + clip: true + layer.enabled: true + Instantiator { + model: 6 + delegate: Shape { + id: tri + property real x_size: Math.random() * button.width/12 + button.width/10 + property real y_size: x_size * Math.SQRT1_2 + + property real y_anim: 0 + property real y_off: Math.random() * entry_height + x: Math.random() * (button.width + x_size*2) - x_size + y: (y_anim + y_off) % (entry_height + y_size) - y_size + ShapePath { + strokeWidth: 2 + strokeColor: Qt.darker(button.color, 3) + fillColor: "transparent" + + startX: tri.x_size/2 + startY: 0 + PathLine {x: 0; y: tri.y_size} + PathLine {x: tri.x_size; y: tri.y_size} + PathLine {x: tri.x_size/2; y: 0} + } + PropertyAnimation on y_anim { + from: entry_height + tri.y_size + to: 0 + duration: Math.random() * 12000 + 3500 + loops: Animation.Infinite + } + } + onObjectAdded: (index, obj) => obj.parent = triangles + } + } + + Item { + anchors.fill: parent + clip: true + Image { + id: icon + source: button.iconPath + width: 80 + height: width + anchors { + right: parent.right + rightMargin: 140 + verticalCenter: parent.verticalCenter + } + } + } + + // App name/description + Column { + anchors { + leftMargin: 12 + left: parent.left + verticalCenter: parent.verticalCenter + } + spacing: 4 + Text { + text: modelData.name + font.family: root.font + font.pixelSize: 16 + font.bold: true + color: "white" + } + Text { + visible: modelData.comment.length > 0 + width: parent.parent.width - icon.anchors.rightMargin-icon.width - 20 + text: modelData.comment + font.family: root.font + color: "white" + elide: Text.ElideRight + } + } + + // Drop shadow + background: RectangularShadow { + anchors.fill: parent + radius: root.radius + blur: 8 + spread: 4 + opacity: 0.6 + } + } + } + } + + Item { + id: searchBox + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: 12 + rightMargin: 12 + } + height: 80 + + TextInput { + z: 5 + id: searchInput + font.family: root.font + font.pixelSize: 16 + font.bold: true + focus: true + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: 45 + } + color: "white" + onActiveFocusChanged: focus = true; + } + Rectangle { + color: "#333" + radius: root.radius + anchors { + fill: parent + leftMargin: 35 + } + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + width: parent.width - 60 + radius: root.radius + color: "#222" + } + transform: [Shear { + xFactor: -0.15 + }] + RectangularShadow { + z: -1 + anchors.fill: parent + radius: root.radius + blur: 8 + spread: 4 + opacity: 0.6 + } + } + Image { + source: Quickshell.iconPath("search") + anchors { + right: searchBox.right + verticalCenter: searchBox.verticalCenter + rightMargin: 26 + } + width: 24 + height: 24 + } + Text { + text: "Search apps..." + anchors.fill: searchInput + font: searchInput.font + color: "#888" + visible: searchInput.text.length === 0 + } + } + + } +}