Triangles clip includes border radius

This commit is contained in:
2025-12-28 12:38:13 -08:00
parent 4e7f2d0a37
commit b9b05ccf4d

347
shell.qml
View File

@@ -7,7 +7,6 @@ import QtQuick.Shapes
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets import Quickshell.Widgets
PanelWindow { PanelWindow {
@@ -15,8 +14,8 @@ PanelWindow {
function hide() { function hide() {
visible = false visible = false
searchInput.text = ""
list.visible = false list.visible = false
searchInput.text = ""
} }
IpcHandler { IpcHandler {
@@ -24,9 +23,11 @@ PanelWindow {
function show() { function show() {
visible = true visible = true
list.visible = true list.visible = true
list.modelChanged
} }
function hide() { function hide() {
root.hide() root.hide()
searchInput.text = ""
} }
function toggle() { visible ? hide() : show() } function toggle() { visible ? hide() : show() }
} }
@@ -39,19 +40,24 @@ PanelWindow {
top: true top: true
bottom: true bottom: true
} }
margins {
top: {
if (ToplevelManager.activeToplevel?.fullscreen) {
return 0
}
return 36;
}
}
readonly property int radius: 20 readonly property int radius: 20
readonly property var font: { readonly property var font: {
family: "comfortaa" family: "comfortaa"
} }
readonly property int entry_height: 100 readonly property int entry_height: 100
property var currentApps: {
let apps = Array.from(DesktopEntries.applications.values);
if (searchInput.text.length === 0) {
apps.sort((a, b) => a.name.localeCompare(b.name));
return apps;
}
apps = apps.filter(app =>
app.name.toLowerCase().search(searchInput.text.toLowerCase()) !== -1
);
return apps
}
property string lastLaunched: ""
color: "transparent" color: "transparent"
Item { Item {
anchors.fill: parent anchors.fill: parent
@@ -68,33 +74,27 @@ PanelWindow {
break; break;
case Qt.Key_Return: case Qt.Key_Return:
list.currentItem.execute() list.currentItem.execute()
root.hide()
} }
} }
// TODO: Swap ListView for ScriptModel
ListView { ListView {
id: list id: list
spacing: 6 spacing: 6
anchors.fill: parent anchors.fill: parent
layer.enabled: true
highlightMoveDuration: 150 highlightMoveDuration: 150
highlightRangeMode: ListView.StrictlyEnforceRange highlightRangeMode: ListView.StrictlyEnforceRange
preferredHighlightBegin: 120 preferredHighlightBegin: 120
preferredHighlightEnd: this.height/2 + 200 preferredHighlightEnd: this.height/2
property var currentApps: { onModelChanged: {
let apps = Array.from(DesktopEntries.applications.values); if (searchInput.text === "")
apps.sort((a, b) => a.name.localeCompare(b.name)); currentIndex = currentApps.findIndex(entry =>
if (searchInput.text.length === 0) { entry.id === lastLaunched
return apps; );
}
return apps.filter(app => {
return app.name.toLowerCase().search(searchInput.text.toLowerCase()) != -1
});
} }
model: currentApps model: currentApps
delegate: Item { delegate: Button {
height: entry_height height: entry_height
width: root.width + 10 width: root.width + 10
property var leftMargin: (Math.pow(Math.abs(this.y - list.contentY - root.height/2), 1.8) / 1400) property var leftMargin: (Math.pow(Math.abs(this.y - list.contentY - root.height/2), 1.8) / 1400)
@@ -104,158 +104,168 @@ PanelWindow {
} }
z: -index z: -index
property real selectedOffset: ListView.isCurrentItem ? 16 : 64 property real selectedOffset: ListView.isCurrentItem ? 16 : 64
function execute() { modelData.execute() } function execute() {
modelData.execute()
root.lastLaunched = modelData.id
root.hide()
}
Behavior on selectedOffset { Behavior on selectedOffset {
NumberAnimation {duration: 500; easing.type: Easing.OutQuint} NumberAnimation {duration: 500; easing.type: Easing.OutQuint}
} }
Button { id: button
id: button onClicked: execute()
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 // Calculate background color based on average color of icon
Canvas { Canvas {
id: canvas id: canvas
width: icon.width width: icon.width/2
height: icon.height height: icon.height/2
visible: false visible: false
onImageLoaded: { onImageLoaded: {
console.log("loaded"); requestPaint();
requestPaint();
}
Component.onCompleted: loadImage(button.iconPath)
} }
property color color: { Component.onCompleted: loadImage(button.iconPath)
if (!canvas.available) { }
return null property var color: {
} if (!canvas.available) {
const ctx = canvas.getContext("2d"); return null
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) 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) {
let c_max = Math.max(pixels[i+0], pixels[i+1], pixels[i+2]);
let c_min = Math.min(pixels[i+0], pixels[i+1], pixels[i+2]);
let saturation = c_max === 0 ? 0 : (c_max - c_min) / c_max;
avg[0] += pixels[i+0] * saturation;
avg[1] += pixels[i+1] * saturation;
avg[2] += pixels[i+2] * saturation;
count += pixels[i+3] * saturation;
}
return Qt.rgba(avg[0]/count, avg[1]/count, avg[2]/count, 1);
}
property var iconPath: Quickshell.iconPath(modelData.icon)
// Background gradient // Background gradient
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: root.radius
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.darker(button.color, 8)
position: 0
}
GradientStop {
color: Qt.darker(button.color, 4)
position: 1
}
}
}
// Triangles
Item {
id: triangles
visible: false
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, 2.5)
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
running: root.visible
to: 0
duration: Math.random() * 12000 + 5000
loops: Animation.Infinite
}
}
onObjectAdded: (index, obj) => obj.parent = triangles
}
}
MultiEffect {
source: triangles
anchors.fill: triangles
maskEnabled: true
maskSource: ShaderEffectSource { sourceItem: Rectangle {
x: triangles.x
y: triangles.y
width: triangles.width
height: triangles.height
radius: root.radius 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 {
Item { anchors.fill: parent
id: triangles clip: true
anchors.fill: parent Image {
clip: true id: icon
layer.enabled: true source: button.iconPath
Instantiator { width: 80
model: 6 height: width
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 { anchors {
leftMargin: 12 right: parent.right
left: parent.left rightMargin: 140
verticalCenter: parent.verticalCenter 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 // App name/description
background: RectangularShadow { Column {
anchors.fill: parent anchors {
radius: root.radius leftMargin: 12
blur: 8 left: parent.left
spread: 4 verticalCenter: parent.verticalCenter
opacity: 0.6
} }
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
} }
} }
} }
@@ -315,6 +325,13 @@ PanelWindow {
opacity: 0.6 opacity: 0.6
} }
} }
Text {
text: "Search apps..."
anchors.fill: searchInput
font: searchInput.font
color: "#888"
visible: searchInput.text.length === 0
}
Image { Image {
source: Quickshell.iconPath("search") source: Quickshell.iconPath("search")
anchors { anchors {
@@ -325,14 +342,6 @@ PanelWindow {
width: 24 width: 24
height: 24 height: 24
} }
Text {
text: "Search apps..."
anchors.fill: searchInput
font: searchInput.font
color: "#888"
visible: searchInput.text.length === 0
}
} }
} }
} }