[plasma/plasma-desktop] emojier: emojier: allow zooming the emoji grid

Nate Graham <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 5d6be2cec17cefb3fbedb591fd7b7c543c721651 by Nate Graham, on behalf of Jason Uithol.
Committed on 27/07/2026 at 19:45.
Pushed by ngraham into branch 'master'.

emojier: allow zooming the emoji grid

You can now zoom with Ctrl+scroll, keyboard shortcuts, and toolbar buttons.
This enlarges or shrinks the cells of the emoji grid, clamped between 1.5 and
8 grid units. Plain scrolling is unaffected and still scrolls the list.

The glyph font size is derived from the cell height instead of a fixed point
size, so it scales with the zoom (this also lets the inert minimumPointSize be
dropped). Size changes are coalesced on a short throttle so a fast wheel spin
triggers at most one relayout per frame instead of one per event. The skin-tone
variant popup tracks the same size.

M  +105  -4    emojier/app/ui/CategoryPage.qml
M  +1    -1    emojier/app/ui/Emojier.qml
M  +17   -0    emojier/emojierplugin.cpp
M  +7    -0    emojier/emojierplugin.h
M  +3    -0    emojier/emojiersettings.kcfg

https://invent.kde.org/plasma/plasma-desktop/-/commit/5d6be2cec17cefb3fbedb591fd7b7c543c721651

diff --git a/emojier/app/ui/CategoryPage.qml b/emojier/app/ui/CategoryPage.qml
index cb5a705dcc..f66eaa0119 100644
--- a/emojier/app/ui/CategoryPage.qml
+++ b/emojier/app/ui/CategoryPage.qml
@@ -187,6 +187,39 @@ Kirigami.ScrollablePage {
                 checked: view.model.skinTone == SkinTone.Dark
                 onTriggered: view.model.skinTone = SkinTone.Dark
             }
+        },
+        Kirigami.Action {
+            icon.name: "zoom-out"
+            displayHint: Kirigami.DisplayHint.IconOnly
+            enabled: emojiView.targetSize > emojiView.minSize + 0.5
+            text: i18nc("@action:button zoom the emoji grid out", "Zoom Out")
+            onTriggered: emojiView.applyZoom(-1)
+        },
+        Kirigami.Action {
+            text: i18nc("@info:status label for the current zoom-level indicator", "Zoom level")
+            displayComponent: QQC2.Label {
+                text: i18nc("@info:status current emoji zoom level as a percentage, %1 is a number", "%1%",
+                            Math.round(emojiView.targetSize / emojiView.defaultSize * 100))
+                textFormat: Text.PlainText
+                horizontalAlignment: Text.AlignHCenter
+                verticalAlignment: Text.AlignVCenter
+                leftPadding: Kirigami.Units.smallSpacing
+                rightPadding: Kirigami.Units.smallSpacing
+            }
+        },
+        Kirigami.Action {
+            icon.name: "zoom-in"
+            displayHint: Kirigami.DisplayHint.IconOnly
+            enabled: emojiView.targetSize < emojiView.maxSize - 0.5
+            text: i18nc("@action:button zoom the emoji grid in", "Zoom In")
+            onTriggered: emojiView.applyZoom(1)
+        },
+        Kirigami.Action {
+            icon.name: "zoom-original"
+            displayHint: Kirigami.DisplayHint.IconOnly
+            enabled: Math.abs(emojiView.targetSize - emojiView.defaultSize) > 0.5
+            text: i18nc("@action:button reset the emoji grid zoom to 100%", "Reset Zoom")
+            onTriggered: emojiView.resetZoom()
         }
     ]
 
@@ -200,7 +233,8 @@ Kirigami.ScrollablePage {
 
                 Layout.alignment: Qt.AlignHCenter
                 Layout.preferredHeight: contentHeight
-                readonly property real desiredSize: Kirigami.Units.gridUnit * 3
+                // Track the main grid's zoom so variants match the chosen size.
+                readonly property real desiredSize: emojiView.desiredSize
 
                 cellWidth: desiredSize
                 cellHeight: desiredSize
@@ -229,6 +263,22 @@ Kirigami.ScrollablePage {
         }
     }
 
+    Shortcut {
+        sequences: [StandardKey.ZoomIn]
+        enabled: emojiView.targetSize < emojiView.maxSize - 0.5
+        onActivated: emojiView.applyZoom(1)
+    }
+    Shortcut {
+        sequences: [StandardKey.ZoomOut]
+        enabled: emojiView.targetSize > emojiView.minSize + 0.5
+        onActivated: emojiView.applyZoom(-1)
+    }
+    Shortcut {
+        sequence: "Ctrl+0"
+        enabled: Math.abs(emojiView.targetSize - emojiView.defaultSize) > 0.5
+        onActivated: emojiView.resetZoom()
+    }
+
     Component {
         id: menuComponent
 
@@ -260,14 +310,66 @@ Kirigami.ScrollablePage {
     GridView {
         id: emojiView
 
-        readonly property real desiredSize: Kirigami.Units.gridUnit * 3
+        readonly property real defaultSize: Kirigami.Units.gridUnit * 3
+        readonly property real minSize: Kirigami.Units.gridUnit * 1.5
+        readonly property real maxSize: defaultSize * 2.75
+        readonly property real zoomStep: 0.25
+
+        property real desiredSize: defaultSize
+        // Ctrl+wheel zoom accumulates into targetSize; desiredSize (which drives
+        // the relayout) is updated on a throttle so a fast wheel spin coalesces
+        // into at most one relayout per frame instead of one per event.
+        property real targetSize: defaultSize
         readonly property int columnsToHave: Math.ceil(width / desiredSize)
         readonly property int delayInterval: Math.min(300, columnsToHave * 10)
         property int hoveredIndex: -1
 
+        function applyZoom(steps: int): void {
+            const stepPx = defaultSize * zoomStep
+            const index = Math.round((targetSize - defaultSize) / stepPx) + steps
+            targetSize = Math.max(minSize, Math.min(maxSize, defaultSize + index * stepPx))
+            emoji.emojiZoom = targetSize / defaultSize
+            if (!zoomThrottle.running) {
+                desiredSize = targetSize
+                zoomThrottle.start()
+            }
+        }
+
+        function resetZoom(): void {
+            targetSize = defaultSize
+            desiredSize = defaultSize
+            emoji.emojiZoom = 1
+        }
+
+        // Clamp in case the stored zoom or the size bounds changed since it was saved.
+        Component.onCompleted: {
+            const saved = Math.max(minSize, Math.min(maxSize, defaultSize * emoji.emojiZoom))
+            targetSize = saved
+            desiredSize = saved
+        }
+
         cellWidth: width / columnsToHave
         cellHeight: desiredSize
 
+        Timer {
+            id: zoomThrottle
+            interval: 33 // ms; caps relayouts at ~30 Hz so a fast spin coalesces
+            onTriggered: emojiView.desiredSize = emojiView.targetSize
+        }
+
+        MouseArea {
+            anchors.fill: parent
+            acceptedButtons: Qt.NoButton
+            onWheel: wheel => {
+                if (wheel.modifiers & Qt.ControlModifier) {
+                    emojiView.applyZoom(wheel.angleDelta.y > 0 ? 1 : -1)
+                    wheel.accepted = true
+                } else {
+                    wheel.accepted = false
+                }
+            }
+        }
+
         model: CategoryModelFilter {
             id: filter
             sourceModel: SearchModelFilter {
@@ -322,9 +424,8 @@ Kirigami.ScrollablePage {
 
             text: model.display
             contentItem: QQC2.Label {
-                font.pointSize: 25
+                font.pixelSize: Math.round(emojiLabel.height * 0.6)
                 font.family: 'emoji' // Avoid monochrome fonts like DejaVu Sans
-                minimumPointSize: 10
                 text: emojiLabel.text
                 textFormat: Text.PlainText
                 horizontalAlignment: Text.AlignHCenter
diff --git a/emojier/app/ui/Emojier.qml b/emojier/app/ui/Emojier.qml
index 4d6e647c60..ac2aaea021 100644
--- a/emojier/app/ui/Emojier.qml
+++ b/emojier/app/ui/Emojier.qml
@@ -22,7 +22,7 @@ Kirigami.ApplicationWindow {
     minimumWidth: Math.round(minimumHeight * 1.25)
     // minimumHeight is set in Component.onCompleted to avoid a binding loop
 
-    width: Kirigami.Units.gridUnit * 25
+    width: Kirigami.Units.gridUnit * 27
     height: Kirigami.Units.gridUnit * 25
 
     pageStack.initialPage: Qt.resolvedUrl("CategoryPage.qml")// { }
diff --git a/emojier/emojierplugin.cpp b/emojier/emojierplugin.cpp
index 797842d8e3..9c54fc1257 100644
--- a/emojier/emojierplugin.cpp
+++ b/emojier/emojierplugin.cpp
@@ -205,6 +205,23 @@ int EmojiModel::skinTone() const
     return m_settings.skinTone();
 }
 
+void EmojiModel::setEmojiZoom(qreal emojiZoom)
+{
+    if (m_settings.emojiZoom() == emojiZoom) {
+        return;
+    }
+    m_settings.setEmojiZoom(emojiZoom);
+    // Persist immediately rather than at destruction: the emojier is a
+    // launch-pick-close popup, and its recent list is saved the same way.
+    m_settings.save();
+    Q_EMIT emojiZoomChanged();
+}
+
+qreal EmojiModel::emojiZoom() const
+{
+    return m_settings.emojiZoom();
+}
+
 RecentEmojiModel::RecentEmojiModel()
 {
     refresh();
diff --git a/emojier/emojierplugin.h b/emojier/emojierplugin.h
index 9b14abebfa..d668bc9efb 100644
--- a/emojier/emojierplugin.h
+++ b/emojier/emojierplugin.h
@@ -100,6 +100,7 @@ class EmojiModel : public AbstractEmojiModel
     QML_ELEMENT
     Q_PROPERTY(QStringList categories MEMBER m_categories CONSTANT)
     Q_PROPERTY(int skinTone READ skinTone WRITE setSkinTone NOTIFY skinToneChanged)
+    Q_PROPERTY(qreal emojiZoom READ emojiZoom WRITE setEmojiZoom NOTIFY emojiZoomChanged)
     Q_PROPERTY(QAbstractItemModel *twoToneEmojiModel MEMBER m_TwoToneEmojiModel CONSTANT)
 public:
     enum EmojiRole {
@@ -117,6 +118,12 @@ public:
 
     Q_SIGNAL void skinToneChanged();
 
+    qreal emojiZoom() const;
+
+    void setEmojiZoom(qreal emojiZoom);
+
+    Q_SIGNAL void emojiZoomChanged();
+
 private:
     QStringList m_categories;
     TwoToneEmojiModel *m_TwoToneEmojiModel;
diff --git a/emojier/emojiersettings.kcfg b/emojier/emojiersettings.kcfg
index 9b22780a59..848f42db30 100644
--- a/emojier/emojiersettings.kcfg
+++ b/emojier/emojiersettings.kcfg
@@ -17,5 +17,8 @@
       </choices>
       <default>Neutral</default>
     </entry>
+    <entry name="emojiZoom" type="Double">
+      <default>1.0</default>
+    </entry>
   </group>
 </kcfg>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.