Feat: MD3 wallpaper launcher carousel #146

Merged
zach merged 16 commits from feat/wallpaper-picker-rework into main 2026-08-17 18:01:41 +02:00
4 changed files with 168 additions and 30 deletions
Showing only changes of commit 69c2bfae9d - Show all commits
+1
View File
@@ -26,6 +26,7 @@ Singleton {
function query(search: string): list<var> {
search = transformSearch(search);
console.log(!search, search, ...list);
if (!search)
return [...list];
+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
}
}
@@ -21,6 +21,7 @@ constexpr int kMaxLayoutCapacity = 64;
CarouselView::CarouselView(QQuickItem* parent) : QQuickItem(parent) {
setAcceptedMouseButtons(Qt::LeftButton);
setFiltersChildMouseEvents(true);
setFlag(QQuickItem::ItemHasContents, false);
setClip(false);
@@ -80,6 +81,89 @@ void CarouselView::setDelegate(QQmlComponent* c) {
relayout();
}
bool CarouselView::childMouseEventFilter(QQuickItem* item, QEvent* event) {
Q_UNUSED(item);
auto* mouseEvent = dynamic_cast<QMouseEvent*>(event);
if (!mouseEvent) return false;
const QPointF pos = mapFromItem(item, mouseEvent->position());
switch (mouseEvent->type()) {
case QEvent::MouseButtonPress: {
if (mouseEvent->button() != Qt::LeftButton) return false;
m_dragging = true;
m_dragActive = false;
m_pressPos = pos;
m_pressContentX = m_contentX;
m_dragTimer.start();
m_lastMoveX = pos.x();
m_lastMoveT = 0;
m_velocity = 0;
return false;
}
case QEvent::MouseMove: {
if (!m_dragging) return false;
const qreal dx = pos.x() - m_pressPos.x();
if (!m_dragActive && std::abs(dx) > kDragThreshold) {
m_dragActive = true;
m_pressContentX = m_contentX;
cancelAnimations();
grabMouse();
}
if (!m_dragActive) return false;
m_contentX = m_pressContentX - dx;
wrapContentIfNeeded();
updateCurrentIndexFromContentX(true);
relayout();
const qint64 t = m_dragTimer.elapsed();
const qint64 dt = t - m_lastMoveT;
if (dt > 0) {
const qreal instVel = (m_lastMoveX - pos.x()) / (dt / 1000.0);
m_velocity = m_velocity * 0.7 + instVel * 0.3;
}
m_lastMoveX = pos.x();
m_lastMoveT = t;
mouseEvent->accept();
return true;
}
case QEvent::MouseButtonRelease: {
if (!m_dragging) return false;
if (!m_dragActive) {
m_dragging = false;
return false;
}
return false;
}
case QEvent::MouseButtonDblClick:
return false;
default:
return false;
}
}
void CarouselView::setModel(const QVariantList& m) {
m_model = m;
@@ -118,6 +118,8 @@ class CarouselView : public QQuickItem {
void previewIndexChanged(int realIndex, QVariant modelData);
protected:
bool childMouseEventFilter(QQuickItem* item, QEvent* event) override;
void geometryChange(const QRectF& newGeo, const QRectF& oldGeo) override;
void mousePressEvent(QMouseEvent* event) override;