Merge branch 'main' into fix/proper-scheme-previews
C++ / fmt (pull_request) Successful in 3s
JS/TS / fmt (pull_request) Successful in 16s
JS/TS / lint (pull_request) Successful in 17s
Python / static (pull_request) Successful in 1m0s
Rust / fmt (pull_request) Successful in 1m0s
C++ / build (pull_request) Successful in 2m14s
Rust / build (pull_request) Successful in 2m9s
Rust / clippy (pull_request) Successful in 1m26s
Python / verify (pull_request) Successful in 2m50s
C++ / clang-tidy (pull_request) Successful in 4m1s

This commit is contained in:
2026-08-17 18:02:22 +02:00
13 changed files with 2101 additions and 446 deletions
+81 -30
View File
@@ -1,28 +1,36 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Modules.Launcher.Services
import qs.Modules.Launcher.Items
import qs.Components
import Quickshell
import ZShell.Config
import qs.Components
import qs.Services
import qs.Modules.Launcher.Items
import qs.Modules.Launcher.Services
CustomListView {
id: root
readonly property string displayState: stateForText(displayText)
property string displayText
readonly property string requestedState: stateForText(search.text)
required property SearchBar search
required property PersistentProperties visibilities
highlightFollowsCurrentItem: false
highlightRangeMode: ListView.ApplyRange
implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxAppsShown, count) - spacing
orientation: Qt.Vertical
preferredHighlightBegin: 0
preferredHighlightEnd: height
spacing: Tokens.spacing.small
state: {
const text = search.text;
function resultsForText(text: string): var {
switch (stateForText(text)) {
case "actions":
return Actions.query(text);
case "calc":
return [0];
case "variant":
return SchemeVariants.query(text);
default:
return Apps.search(text);
}
}
function stateForText(text: string): string {
const prefix = Config.launcher.actionPrefix;
if (text.startsWith(prefix)) {
for (const action of ["calc", "scheme", "variant"])
@@ -34,6 +42,20 @@ CustomListView {
return "apps";
}
function syncDisplayText(): void {
if (visibilities.launcher && requestedState === displayState)
displayText = search.text;
}
highlightFollowsCurrentItem: false
highlightRangeMode: ListView.ApplyRange
implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxAppsShown, count) - spacing
orientation: Qt.Vertical
preferredHighlightBegin: 0
preferredHighlightEnd: height
spacing: Tokens.spacing.small
state: visibilities.launcher ? requestedState : displayState
verticalLayoutDirection: ListView.BottomToTop
CustomScrollBar.vertical: CustomScrollBar {
@@ -44,19 +66,21 @@ CustomListView {
Anim {
from: 0
properties: "opacity,scale"
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
addDisplaced: Transition {
Anim {
duration: Tokens.anim.durations.small
property: "y"
type: Anim.StandardSmall
}
Anim {
properties: "opacity,scale"
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
displaced: Transition {
@@ -65,8 +89,9 @@ CustomListView {
}
Anim {
properties: "opacity,scale"
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
highlight: CustomRect {
@@ -85,7 +110,7 @@ CustomListView {
}
}
model: ScriptModel {
id: model
values: root.resultsForText(root.displayText)
onValuesChanged: root.currentIndex = 0
}
@@ -95,8 +120,9 @@ CustomListView {
}
Anim {
properties: "opacity,scale"
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
remove: Transition {
@@ -104,8 +130,9 @@ CustomListView {
Anim {
from: 1
properties: "opacity,scale"
property: "opacity"
to: 0
type: Anim.DefaultEffects
}
}
states: [
@@ -113,7 +140,6 @@ CustomListView {
name: "apps"
PropertyChanges {
model.values: Apps.search(search.text)
root.delegate: appItem
}
},
@@ -121,7 +147,6 @@ CustomListView {
name: "actions"
PropertyChanges {
model.values: Actions.query(search.text)
root.delegate: actionItem
}
},
@@ -129,7 +154,6 @@ CustomListView {
name: "calc"
PropertyChanges {
model.values: [0]
root.delegate: calcItem
}
},
@@ -137,7 +161,6 @@ CustomListView {
name: "variant"
PropertyChanges {
model.values: SchemeVariants.query(search.text)
root.delegate: variantItem
}
}
@@ -147,7 +170,7 @@ CustomListView {
ParallelAnimation {
Anim {
duration: Tokens.anim.durations.small
easing.bezierCurve: Tokens.anim.curves.expressiveEffects
easing.bezierCurve: Tokens.anim.curves.standardAccel
from: 1
property: "opacity"
target: root
@@ -156,7 +179,7 @@ CustomListView {
Anim {
duration: Tokens.anim.durations.small
easing.bezierCurve: Tokens.anim.curves.expressiveEffects
easing.bezierCurve: Tokens.anim.curves.standardAccel
from: 1
property: "scale"
target: root
@@ -165,14 +188,24 @@ CustomListView {
}
PropertyAction {
properties: "values,delegate"
targets: [model, root]
property: "delegate"
target: root
value: null
}
ScriptAction {
script: root.displayText = root.search.text
}
PropertyAction {
property: "delegate"
target: root
}
ParallelAnimation {
Anim {
duration: Tokens.anim.durations.small
easing.bezierCurve: Tokens.anim.curves.expressiveEffects
easing.bezierCurve: Tokens.anim.curves.standardDecel
from: 0
property: "opacity"
target: root
@@ -181,7 +214,7 @@ CustomListView {
Anim {
duration: Tokens.anim.durations.small
easing.bezierCurve: Tokens.anim.curves.expressiveEffects
easing.bezierCurve: Tokens.anim.curves.standardDecel
from: 0.9
property: "scale"
target: root
@@ -197,6 +230,8 @@ CustomListView {
}
}
Component.onCompleted: displayText = search.text
Component {
id: appItem
@@ -228,4 +263,20 @@ CustomListView {
list: root
}
}
Connections {
function onTextChanged() {
root.syncDisplayText();
}
target: root.search
}
Connections {
function onLauncherChanged() {
root.syncDisplayText();
}
target: root.visibilities
}
}
+53
View File
@@ -0,0 +1,53 @@
import Quickshell
import QtQuick
import ZShell.Components
import ZShell.Config
import qs.Modules.Launcher.Items
import qs.Components
import qs.Helpers
CarouselView {
id: root
required property var panels
readonly property var screen: QsWindow.window?.screen
required property SearchBar search
readonly property var values: Wallpapers.query(root.search.text.split(" ").slice(1).join(" "))
required property var visibilities
delegateProperties: ({
visibilities: root.visibilities
})
focalWidth: Config.launcher.sizes.wallpaperWidth
glideDuration: 250
glideEasingType: Easing.OutCubic
implicitHeight: tileHeight + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal
itemSpacing: Tokens.spacing.small
maxWidth: {
if (!screen)
return 0;
const barMargins = Config.bar.border * 2;
let margins = Math.round(screen.width / 6);
if (visibilities.utilities || visibilities.sidebar)
margins = Config.sidebar.sizes.width;
return screen.width - (barMargins + margins) * 2 - Config.bar.rounding * 4;
}
minEdgeWidth: 56
model: values
tileHeight: focalWidth / 16 * 9
delegate: Component {
WallpaperTile {
}
}
Component.onCompleted: {
const idx = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent);
if (idx >= 0)
jumpToIndex(idx);
}
Component.onDestruction: Wallpapers.stopPreview()
onPreviewIndexChanged: (realIndex, modelData) => {
Wallpapers.preview(modelData.path);
}
}
+2 -2
View File
@@ -110,9 +110,9 @@ Item {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.implicitWidth
sourceComponent: WallpaperList {
content: root.content
sourceComponent: CarouselView {
panels: root.panels
search: root.search
visibilities: root.visibilities
-96
View File
@@ -1,96 +0,0 @@
import ZShell.Models
import Quickshell
import QtQuick
import qs.Components
import qs.Helpers
import ZShell.Config
import qs.Modules
import qs.Services
Item {
id: root
required property FileSystemEntry modelData
required property PersistentProperties visibilities
implicitHeight: image.height + label.height + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal
implicitWidth: image.width + Tokens.padding.larger * 2
opacity: 0
scale: 0.5
z: PathView.z ?? 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Component.onCompleted: {
scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0);
opacity = Qt.binding(() => PathView.onPath ? 1 : 0);
}
StateLayer {
radius: Tokens.rounding.normal
onClicked: {
Wallpapers.setWallpaper(root.modelData.path);
root.visibilities.launcher = false;
}
}
Elevation {
anchors.fill: image
level: 4
opacity: root.PathView.isCurrentItem ? 1 : 0
radius: image.radius
Behavior on opacity {
Anim {
}
}
}
CustomClippingRect {
id: image
anchors.horizontalCenter: parent.horizontalCenter
color: Colors.tPalette.m3surfaceContainer
implicitHeight: implicitWidth / 16 * 9
implicitWidth: Config.launcher.sizes.wallpaperWidth
radius: Tokens.rounding.small
y: Tokens.padding.large
MaterialIcon {
anchors.centerIn: parent
color: Colors.tPalette.m3outline
font.pointSize: Tokens.font.size.extraLarge * 2
font.weight: 600
text: "image"
}
CachingImage {
anchors.fill: parent
cache: true
path: root.modelData.path
smooth: !root.PathView.view.moving
}
}
CustomText {
id: label
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: image.bottom
anchors.topMargin: Tokens.spacing.small / 2
elide: Text.ElideRight
font.pointSize: Tokens.font.size.normal
horizontalAlignment: Text.AlignHCenter
renderType: Text.QtRendering
text: root.modelData.relativePath
width: image.width - Tokens.padding.normal * 2
}
}
+58
View File
@@ -0,0 +1,58 @@
import Quickshell
import QtQuick
import ZShell.Config
import qs.Components
import qs.Helpers
import qs.Services
Item {
id: root
required property bool isCurrent
required property var modelData // FileSystemEntry-like {path, relativePath}
required property real tileHeight
required property real tileWidth
required property PersistentProperties visibilities
signal requestActivate
implicitHeight: tileHeight + Tokens.padding.large * 2
implicitWidth: tileWidth
CustomClippingRect {
id: image
anchors.horizontalCenter: parent.horizontalCenter
color: Colors.tPalette.m3surfaceContainer
implicitHeight: root.tileHeight
implicitWidth: root.tileWidth
radius: Tokens.rounding.small
y: Tokens.padding.large
MaterialIcon {
anchors.centerIn: parent
color: Colors.tPalette.m3outline
font.pointSize: Tokens.font.size.extraLarge * 2
font.weight: 600
text: "image"
}
CachingImage {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
cache: true
fillMode: Image.PreserveAspectCrop
height: root.tileHeight
path: root.modelData ? root.modelData.path : ""
smooth: root.isCurrent
sourceSize.height: root.tileHeight
sourceSize.width: root.tileHeight * (16 / 9)
width: root.tileHeight * (16 / 9)
}
StateLayer {
onClicked: root.requestActivate()
}
}
}
-93
View File
@@ -1,93 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Components
import qs.Helpers
import ZShell.Config
import qs.Modules.Launcher.Items
PathView {
id: root
required property var content
readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.9 + Tokens.padding.larger * 2
readonly property int numItems: {
const screen = QsWindow.window?.screen;
if (!screen)
return 0;
// Screen width - 4x outer rounding - 2x max side thickness (cause centered)
const barMargins = panels.bar.implicitWidth;
let outerMargins = 0;
if ((visibilities.utilities || visibilities.sidebar) && panels.utilities.implicitWidth > outerMargins)
outerMargins = panels.utilities.implicitWidth;
const maxWidth = screen.width - Config.bar.rounding * 4 - (barMargins + outerMargins) * 2;
if (maxWidth <= 0)
return 0;
const maxItemsOnScreen = Math.floor(maxWidth / itemWidth);
const visible = Math.min(maxItemsOnScreen, Config.launcher.maxWallpapers, scriptModel.values.length);
if (visible === 2)
return 1;
if (visible > 1 && visible % 2 === 0)
return visible - 1;
return visible;
}
required property var panels
required property SearchBar search
required property var visibilities
cacheItemCount: 4
highlightRangeMode: PathView.StrictlyEnforceRange
implicitWidth: Math.min(numItems, count) * itemWidth
pathItemCount: numItems
preferredHighlightBegin: 0.5
preferredHighlightEnd: 0.5
snapMode: PathView.SnapToItem
delegate: WallpaperItem {
visibilities: root.visibilities
}
model: ScriptModel {
id: scriptModel
readonly property string search: root.search.text.split(" ").slice(1).join(" ")
values: Wallpapers.query(search)
onValuesChanged: root.currentIndex = search ? 0 : values.findIndex(w => w.path === Wallpapers.actualCurrent)
}
path: Path {
startY: root.height / 2
PathAttribute {
name: "z"
value: 0
}
PathLine {
relativeY: 0
x: root.width / 2
}
PathAttribute {
name: "z"
value: 1
}
PathLine {
relativeY: 0
x: root.width
}
}
Component.onCompleted: currentIndex = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent)
Component.onDestruction: Wallpapers.stopPreview()
onCurrentItemChanged: {
if (currentItem)
Wallpapers.preview(currentItem.modelData.path);
}
}
+22 -79
View File
@@ -5,103 +5,46 @@ import Quickshell.Hyprland
import QtQuick
import qs.Components
import qs.Helpers
import ZShell.Config
import ZShell.Internal
Item {
id: root
property bool completed
property real cropHeight: displayData?.height ?? 1.0
property real cropWidth: displayData?.width ?? 1.0
property real cropX: displayData?.x ?? 0.0
property real cropY: displayData?.y ?? 0.0
property WallpaperImage current
readonly property var displayData: Wallpapers.getCrop(screen.name)
required property ShellScreen screen
property size screenResolution: Qt.size(screen.width * screenScale, screen.height * screenScale)
property real screenScale: Hyprland.monitorFor(screen).scale
property string source: Wallpapers.current
anchors.fill: parent
Component.onCompleted: {
Hyprland.refreshMonitors();
Component.onCompleted: Hyprland.refreshMonitors()
if (source)
Qt.callLater(() => {
current = imgComp.createObject(this, {
source
});
completed = true;
});
}
onSourceChanged: {
if (!source)
current = null;
else
current = imgComp.createObject(this, {
source: source
});
}
WallpaperImage {
id: img
Component {
id: imgComp
anchors.fill: parent
cropHeight: root.displayData?.height ?? 1.0
cropWidth: root.displayData?.width ?? 1.0
cropX: root.displayData?.x ?? 0.0
cropY: root.displayData?.y ?? 0.0
fadeDuration: 150
screenResolution: root.screenResolution
source: Wallpapers.current
WallpaperImage {
id: img
anchors.fill: parent
cropHeight: root.cropHeight
cropWidth: root.cropWidth
cropX: root.cropX
cropY: root.cropY
opacity: 0
screenResolution: root.screenResolution
source: root.source
Behavior on cropHeight {
Anim {
id: heightAnim
}
Behavior on cropHeight {
Anim {
}
Behavior on cropWidth {
Anim {
id: widthAnim
}
}
Behavior on cropWidth {
Anim {
}
Behavior on cropX {
Anim {
id: xAnim
}
}
Behavior on cropX {
Anim {
}
Behavior on cropY {
Anim {
id: yAnim
}
}
Anim on opacity {
id: anim
from: 0
running: false
to: 1
type: Anim.SlowEffects
}
onStatusChanged: {
if (status === Image.Ready) {
anim.start();
}
}
Timer {
id: destroyTimer
interval: anim.duration * 2
running: root.current !== img && root.current?.status === Image.Ready
onTriggered: Qt.callLater(() => img.destroy())
}
Behavior on cropY {
Anim {
}
}
}