pragma ComponentBehavior: Bound import Quickshell import QtQuick import ZShell.Config import qs.Modules.Launcher.Items import qs.Components import qs.Helpers Item { id: root readonly property var arrangement: { const W = width; const F = focalWidth; const M = minEdgeWidth; const sp = itemSpacing; if (W <= 0 || F <= 0) return { n: 0, sizes: [F], effectiveExtraWidth: 0 }; let bestN = 0; let bestSizes = [F]; let bestExtraWidth = 0; for (let n = 1; ; n++) { const ratio = Math.pow(M / F, 1 / n); const sizes = [F]; for (let i = 1; i <= n; i++) { sizes.push(i === n ? M : F * Math.pow(ratio, i)); } let decreasedWidth = 0; for (let i = 1; i <= n; i++) decreasedWidth += pitch - sizes[i]; const effectiveWidth = W + decreasedWidth * 2; const requiredWidth = (2 * n + 1) * pitch; if (requiredWidth > effectiveWidth) break; bestN = n; bestSizes = sizes; bestExtraWidth = decreasedWidth; } return { n: bestN, sizes: bestSizes, effectiveExtraWidth: bestExtraWidth }; } readonly property int centerBlockIndex: Math.floor(loopMultiplier / 2) readonly property int centerBlockStart: centerBlockIndex * realCount readonly property int currentRealIndex: realIndexOf(view.currentIndex) readonly property real focalWidth: Config.launcher.sizes.wallpaperWidth readonly property real itemSpacing: Tokens.spacing.small readonly property var keylines: { const sp = itemSpacing; const sizes = arrangement.sizes; const n = arrangement.n; const right = [ { io: 0, lo: 0, sz: sizes[0] } ]; let lo = sizes[0] / 2; for (let i = 1; i <= n; i++) { const size = sizes[i]; lo += sp + size / 2; right.push({ io: i, lo: lo, sz: size }); lo += size / 2; } right.push({ io: n + 1, lo: width / 2 - sp, sz: 0 }); const k = []; for (let i = right.length - 1; i >= 1; i--) { k.push({ io: -right[i].io, lo: -right[i].lo, sz: right[i].sz }); } for (const item of right) k.push(item); for (let i = 0; i < k.length; i++) k[i].loc = k[i].io * pitch; return k; } readonly property int loopMultiplier: 401 readonly property real minEdgeWidth: 56 required property var panels readonly property real pitch: focalWidth + itemSpacing readonly property int realCount: values.length readonly property var screen: QsWindow.window?.screen required property SearchBar search readonly property real shrinkRatio: 0.63 readonly property real tileHeight: focalWidth / 16 * 9 readonly property var values: Wallpapers.query(root.search.text.split(" ").slice(1).join(" ")) readonly property int virtualCount: realCount > 0 ? realCount * loopMultiplier : 0 required property var visibilities function contentXForIndex(index) { return index * pitch + pitch / 2 - width / 2; } function decrementCurrentIndex() { if (realCount === 0) return; goTo(view.currentIndex - 1); } function glideTo(dest) { if (Math.abs(dest - view.contentX) < 0.5) return; view.cancelFlick(); snapAnim.stop(); snapAnim.from = view.contentX; snapAnim.to = dest; snapAnim.start(); } function goTo(virtualIndex) { glideTo(contentXForIndex(virtualIndex)); } function incrementCurrentIndex() { if (realCount === 0) return; goTo(view.currentIndex + 1); } function indexForContentX(x) { return Math.round((x + width / 2 - pitch / 2) / pitch); } function realIndexOf(virtualIndex) { if (realCount === 0) return -1; return ((virtualIndex % realCount) + realCount) % realCount; } function sample(childLoc) { const k = keylines; if (childLoc <= k[0].loc) return { c: k[0].lo, s: 0 }; if (childLoc >= k[k.length - 1].loc) return { c: k[k.length - 1].lo, s: 0 }; for (let i = 0; i < k.length - 1; i++) { const a = k[i]; const b = k[i + 1]; if (childLoc > b.loc) continue; const span = b.loc - a.loc; const t = span > 0 ? (childLoc - a.loc) / span : 0; return { c: a.lo + (b.lo - a.lo) * t, s: a.sz + (b.sz - a.sz) * t }; } return { c: 0, s: 0 }; } function selectReal(realIdx) { if (realCount === 0) return; const curBlock = Math.floor(view.currentIndex / realCount); const current = view.currentIndex; let target = curBlock * realCount + realIdx; if (target - current > realCount / 2) target -= realCount; else if (current - target > realCount / 2) target += realCount; goTo(target); } function snapTarget(x) { return contentXForIndex(indexForContentX(x)); } function snapToNearest() { glideTo(snapTarget(view.contentX)); } implicitHeight: tileHeight + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal implicitWidth: { if (!screen) return 0; const barMargins = Config.bar.border * 2; let margins = 0; if (visibilities.utilities || visibilities.sidebar) margins = panels.utilities.implicitWidth; const maxWidth = screen.width - (barMargins + margins) * 2 - Config.bar.rounding * 4; return maxWidth; } width: implicitWidth Component.onCompleted: { const idx = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent); if (idx >= 0) view.contentX = contentXForIndex(centerBlockStart + idx); } Component.onDestruction: Wallpapers.stopPreview() ListView { id: view anchors.fill: parent boundsBehavior: Flickable.StopAtBounds cacheBuffer: root.width clip: false currentIndex: root.centerBlockStart displayMarginBeginning: root.arrangement.effectiveExtraWidth displayMarginEnd: root.arrangement.effectiveExtraWidth interactive: true model: root.virtualCount orientation: ListView.Horizontal snapMode: ListView.NoSnap Anim on contentX { id: snapAnim running: false type: Anim.Standard } delegate: Item { id: slot readonly property real childLoc: index * root.pitch + root.pitch / 2 - view.contentX - view.width / 2 required property int index readonly property bool isCurrent: index === view.currentIndex readonly property var keyline: root.sample(childLoc) readonly property var modelData: realIdx >= 0 ? root.values[realIdx] : null readonly property int realIdx: root.realIndexOf(index) readonly property real tileWidth: keyline.s height: view.height visible: modelData !== null width: root.pitch WallpaperTile { anchors.verticalCenter: parent.verticalCenter isCurrent: slot.isCurrent modelData: slot.modelData tileHeight: root.tileHeight tileWidth: slot.tileWidth visibilities: root.visibilities x: (slot.keyline.c - slot.childLoc) + (slot.width - width) / 2 onRequestActivate: { if (slot.isCurrent) { Wallpapers.setWallpaper(slot.modelData.path); root.visibilities.launcher = false; } else { root.goTo(slot.index); } } } } onContentXChanged: { if (root.realCount === 0) return; const blockWidth = root.realCount * root.pitch; const centerX = root.contentXForIndex(root.centerBlockStart); const blocksFromCenter = Math.round((contentX - centerX) / blockWidth); if (Math.abs(blocksFromCenter) >= 2) { const shift = blocksFromCenter * blockWidth; contentX -= shift; if (snapAnim.running) snapAnim.to -= shift; } currentIndex = Math.round((contentX + width / 2 - root.pitch / 2) / root.pitch); } onCurrentIndexChanged: { const realIdx = root.realIndexOf(currentIndex); if (realIdx >= 0) Wallpapers.preview(root.values[realIdx].path); } onFlickEnded: root.snapToNearest() onMovementEnded: root.snapToNearest() } }