diff options
Diffstat (limited to 'appearance/IconsTab.qml')
| -rw-r--r-- | appearance/IconsTab.qml | 253 |
1 files changed, 253 insertions, 0 deletions
diff --git a/appearance/IconsTab.qml b/appearance/IconsTab.qml new file mode 100644 index 0000000..908a003 --- /dev/null +++ b/appearance/IconsTab.qml @@ -0,0 +1,253 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// 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: "" + // Clicking a cursor card stages it; Apply commits. The hover preview above + // switches the real cursor and reverts, so staging must not be confused + // with it: pendingCursor survives unhover, liveCursorCursor does not. + property string pendingCursor: "" + + 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 + readonly property bool staged: pendingCursor === modelData + width: 230; height: 150; radius: 10 + color: current || staged ? Qt.alpha(Theme.accent, 0.2) : Qt.alpha(Theme.surface, 0.35) + border.width: current || staged ? 2 : 1 + border.color: staged ? Theme.accent + : current ? Qt.alpha(Theme.accent, 0.5) + : "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: pendingCursor = 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.staged ? modelData + " staged" + : cursorCard.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: 8 + Text { + anchors.verticalCenter: parent.verticalCenter + text: pendingCursor !== "" ? "click Apply to set" : "click a cursor theme to stage it" + color: Theme.overlay + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + } + Tab { + text: "Apply" + selected: pendingCursor !== "" + enabled: pendingCursor !== "" + onClicked: { + // The hover revert would otherwise fire on the way out and + // set the old theme back over the one just applied. + liveCursorCursor = ""; + Icons.applyCursor(pendingCursor); + pendingCursor = ""; + } + } + Tab { + text: "Reset" + enabled: pendingCursor !== "" + onClicked: pendingCursor = "" + } + } + + 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 = ""; + } +} |
