// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell import Quickshell.Widgets import QtQuick Flickable { contentHeight: col.implicitHeight clip: true property string liveCursorCursor: "" Column { id: col width: parent.width spacing: 12 Text { text: Icons.scanning ? "scanning…" : `${Icons.iconThemes.length} icon themes · ${Icons.cursorThemes.length} cursor themes` color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Text { text: "Icon theme" color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } } // Flow, not a horizontal strip: the cards wrap onto further rows rather // than running off the right edge, and the tab scrolls vertically. Flow { width: parent.width spacing: 10 Repeater { model: Icons.iconThemes Rectangle { id: iconCard required property string modelData readonly property bool current: Icons.currentIcon === modelData width: 230; height: 150; radius: 10 color: current ? Qt.alpha(Theme.accent, 0.2) : Qt.alpha(Theme.surface, 0.35) border.width: current ? 2 : 1 border.color: current ? Theme.accent : "transparent" MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: Icons.applyIcon(modelData) } Column { width: parent.width - 16 anchors.centerIn: parent spacing: 10 Row { anchors.horizontalCenter: parent.horizontalCenter spacing: 8 Repeater { model: ["folder", "text-x-generic", "image-x-generic", "network-wireless"] IconImage { required property string modelData readonly property string glyph: (Icons.iconPreview[iconCard.modelData] ?? {})[modelData] ?? "" // Absolute paths must carry file://, or they // resolve against the config's qrc base and // fail; Gtk's builtin icons come back as // /org/gtk gresource paths Qt cannot open. source: glyph.indexOf("/") === 0 && glyph.indexOf("/org/gtk/") !== 0 ? "file://" + glyph : "" implicitSize: 44 } } } Text { width: parent.width horizontalAlignment: Text.AlignHCenter text: iconCard.current ? modelData + " current" : modelData color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } wrapMode: Text.WrapAnywhere maximumLineCount: 2 elide: Text.ElideRight } } } } } Row { spacing: 10 Text { anchors.verticalCenter: parent.verticalCenter text: "Cursor theme" color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } } Item { width: 12; height: 1 } Text { anchors.verticalCenter: parent.verticalCenter text: "Size" color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } Repeater { model: [16, 24, 32, 48, 64] Tab { required property var modelData anchors.verticalCenter: parent.verticalCenter text: String(modelData) selected: Icons.cursorSize === modelData onClicked: Icons.setCursorSize(modelData) } } } Flow { width: parent.width spacing: 10 Repeater { model: Icons.cursorThemes Rectangle { id: cursorCard required property string modelData readonly property bool current: Icons.currentCursor === modelData width: 230; height: 150; radius: 10 color: current ? Qt.alpha(Theme.accent, 0.2) : Qt.alpha(Theme.surface, 0.35) border.width: current ? 2 : 1 border.color: current ? Theme.accent : "transparent" HoverHandler { onHoveredChanged: { if (hovered) { // Previews are already batched on open; hovering // only switches the real cursor and reverts it. liveCursorCursor = modelData; Quickshell.execDetached(["hyprctl", "setcursor", modelData, String(Icons.cursorSize)]); } else if (liveCursorCursor === modelData) { liveCursorCursor = ""; Quickshell.execDetached(["hyprctl", "setcursor", Icons.currentCursor, String(Icons.cursorSize)]); } } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: Icons.applyCursor(modelData) } Column { width: parent.width - 16 anchors.centerIn: parent spacing: 10 Row { anchors.horizontalCenter: parent.horizontalCenter spacing: 8 Repeater { // The cases that tell themes apart: arrow, hand, // text and horizontal resize. model: ["left_ptr", "hand2", "xterm", "resize"] Image { required property string modelData readonly property string p: (Icons.cursorPreview[cursorCard.modelData] ?? {})[modelData] ?? "" width: 44; height: 44 asynchronous: true source: p !== "" ? "file://" + p : "" fillMode: Image.PreserveAspectFit } } } Text { width: parent.width horizontalAlignment: Text.AlignHCenter text: cursorCard.current ? modelData + " current" : modelData color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } wrapMode: Text.WrapAnywhere maximumLineCount: 2 elide: Text.ElideRight } } } } } Text { width: parent.width wrapMode: Text.Wrap visible: Icons.notice !== "" text: Icons.notice color: Theme.green font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } } onVisibleChanged: if (!visible && liveCursorCursor !== "") { Quickshell.execDetached(["hyprctl", "setcursor", Icons.currentCursor, String(Icons.cursorSize)]); liveCursorCursor = ""; } // A Loader destroys this item on tab switch or drawer close, and visible // does not necessarily change first, so revert the real cursor here too. Component.onDestruction: if (liveCursorCursor !== "") { Quickshell.execDetached(["hyprctl", "setcursor", Icons.currentCursor, String(Icons.cursorSize)]); liveCursorCursor = ""; } }