aboutsummaryrefslogtreecommitdiffstats
path: root/appearance/IconsTab.qml
diff options
context:
space:
mode:
Diffstat (limited to 'appearance/IconsTab.qml')
-rw-r--r--appearance/IconsTab.qml253
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 = "";
+ }
+}