diff --git a/quickshell/Common/htmlElide.js b/quickshell/Common/htmlElide.js
new file mode 100644
index 00000000..99a112b5
--- /dev/null
+++ b/quickshell/Common/htmlElide.js
@@ -0,0 +1,93 @@
+.pragma library
+
+function stripHtmlTags(html) {
+ if (!html)
+ return "";
+ return String(html)
+ .replace(/<[^>]+>/g, "")
+ .replace(/ /g, " ")
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, "\"")
+ .replace(/'/g, "'");
+}
+
+function elideRichText(html, visibleBudget) {
+ if (!html)
+ return "";
+ if (visibleBudget <= 0)
+ return "";
+
+ var out = "";
+ var visible = 0;
+ var i = 0;
+ var openTags = [];
+ var len = html.length;
+
+ while (i < len && visible < visibleBudget) {
+ var ch = html.charAt(i);
+ if (ch === "<") {
+ var end = html.indexOf(">", i);
+ if (end < 0)
+ break;
+ var tag = html.substring(i, end + 1);
+ out += tag;
+ var isClose = tag.charAt(1) === "/";
+ var match = tag.match(/^<\/?([a-zA-Z]+)/);
+ var name = match ? match[1] : "";
+ if (isClose) {
+ if (openTags.length > 0 && openTags[openTags.length - 1] === name)
+ openTags.pop();
+ } else if (!tag.endsWith("/>") && name) {
+ openTags.push(name);
+ }
+ i = end + 1;
+ } else if (ch === "&") {
+ var eend = html.indexOf(";", i);
+ if (eend < 0 || eend - i > 6) {
+ out += "&";
+ visible++;
+ i++;
+ } else {
+ out += html.substring(i, eend + 1);
+ visible++;
+ i = eend + 1;
+ }
+ } else {
+ out += ch;
+ visible++;
+ i++;
+ }
+ }
+
+ while (i < len && html.charAt(i) === "<") {
+ var tend = html.indexOf(">", i);
+ if (tend < 0)
+ break;
+ var ttag = html.substring(i, tend + 1);
+ out += ttag;
+ var tisClose = ttag.charAt(1) === "/";
+ var tmatch = ttag.match(/^<\/?([a-zA-Z]+)/);
+ var tname = tmatch ? tmatch[1] : "";
+ if (tisClose) {
+ if (openTags.length > 0 && openTags[openTags.length - 1] === tname)
+ openTags.pop();
+ } else if (!ttag.endsWith("/>") && tname) {
+ openTags.push(tname);
+ }
+ i = tend + 1;
+ }
+
+ if (i < len) {
+ out = out.replace(/\s+$/, "");
+ while (openTags.length > 0)
+ out += "" + openTags.pop() + ">";
+ out += "…";
+ } else {
+ while (openTags.length > 0)
+ out += "" + openTags.pop() + ">";
+ }
+
+ return out;
+}
diff --git a/quickshell/Modals/DankLauncherV2/ResultItem.qml b/quickshell/Modals/DankLauncherV2/ResultItem.qml
index de00caa9..83d44eb5 100644
--- a/quickshell/Modals/DankLauncherV2/ResultItem.qml
+++ b/quickshell/Modals/DankLauncherV2/ResultItem.qml
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
+import "../../Common/htmlElide.js" as HtmlElide
Rectangle {
id: root
@@ -72,131 +73,159 @@ Rectangle {
}
}
- Row {
- anchors.fill: parent
+ AppIconRenderer {
+ id: iconRenderer
+ width: 36
+ height: 36
+ anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ iconValue: root.iconValue
+ iconSize: 36
+ fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
+ materialIconSizeAdjustment: 12
+ }
+
+ Item {
+ id: textColumn
+ anchors.left: iconRenderer.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: rightContent.left
+ anchors.rightMargin: rightContent.width > 0 ? Theme.spacingM : 0
+ anchors.verticalCenter: parent.verticalCenter
+ height: nameText.implicitHeight + (subText.visible ? subText.height + 2 : 0)
+
+ Text {
+ id: nameText
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ text: root.item?._hName ?? root.item?.name ?? ""
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ font.family: Theme.fontFamily
+ color: Theme.surfaceText
+ wrapMode: Text.WordWrap
+ maximumLineCount: 1
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ TextMetrics {
+ id: subProbe
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: Theme.fontFamily
+ elide: Qt.ElideRight
+ elideWidth: textColumn.width
+ text: root.item?._hRich ? HtmlElide.stripHtmlTags(root.item?._hSub ?? "") : ""
+ }
+
+ readonly property int _richBudget: {
+ if (!subProbe.text)
+ return 0;
+ var e = subProbe.elidedText;
+ return e.endsWith("…") ? e.length - 1 : e.length;
+ }
+
+ Text {
+ id: subText
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: nameText.bottom
+ anchors.topMargin: 2
+ text: root.item?._hRich ? HtmlElide.elideRichText(root.item._hSub ?? "", textColumn._richBudget) : (root.item?.subtitle ?? "")
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: Theme.fontFamily
+ color: Theme.surfaceVariantText
+ wrapMode: Text.WordWrap
+ maximumLineCount: 1
+ elide: Text.ElideRight
+ visible: (root.item?.subtitle ?? "").length > 0
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ Row {
+ id: rightContent
+ anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
- spacing: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
- AppIconRenderer {
- width: 36
- height: 36
+ Rectangle {
+ id: allModeToggle
+ visible: root.item?.type === "plugin_browse"
+ width: 28
+ height: 28
+ radius: 14
anchors.verticalCenter: parent.verticalCenter
- iconValue: root.iconValue
- iconSize: 36
- fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
- materialIconSizeAdjustment: 12
- }
+ color: allModeToggleArea.containsMouse ? Theme.surfaceHover : "transparent"
- Column {
- anchors.verticalCenter: parent.verticalCenter
- width: parent.width - 36 - Theme.spacingM * 3 - rightContent.width
- spacing: 2
-
- Text {
- width: parent.width
- text: root.item?._hName ?? root.item?.name ?? ""
- textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
- font.pixelSize: Theme.fontSizeMedium
- font.weight: Font.Medium
- font.family: Theme.fontFamily
- color: Theme.surfaceText
- elide: Text.ElideRight
- horizontalAlignment: Text.AlignLeft
+ property bool isAllowed: {
+ if (root.item?.type !== "plugin_browse")
+ return false;
+ var pluginId = root.item?.data?.pluginId;
+ if (!pluginId)
+ return false;
+ SettingsData.launcherPluginVisibility;
+ return SettingsData.getPluginAllowWithoutTrigger(pluginId);
}
- Text {
- width: parent.width
- text: root.item?._hSub ?? root.item?.subtitle ?? ""
- textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
- font.pixelSize: Theme.fontSizeSmall
- font.family: Theme.fontFamily
- color: Theme.surfaceVariantText
- elide: Text.ElideRight
- clip: true
- visible: (root.item?.subtitle ?? "").length > 0
- horizontalAlignment: Text.AlignLeft
+ DankIcon {
+ anchors.centerIn: parent
+ name: allModeToggle.isAllowed ? "visibility" : "visibility_off"
+ size: 18
+ color: allModeToggle.isAllowed ? Theme.primary : Theme.surfaceVariantText
}
- }
- Row {
- id: rightContent
- anchors.verticalCenter: parent.verticalCenter
- spacing: Theme.spacingS
-
- Rectangle {
- id: allModeToggle
- visible: root.item?.type === "plugin_browse"
- width: 28
- height: 28
- radius: 14
- anchors.verticalCenter: parent.verticalCenter
- color: allModeToggleArea.containsMouse ? Theme.surfaceHover : "transparent"
-
- property bool isAllowed: {
- if (root.item?.type !== "plugin_browse")
- return false;
+ MouseArea {
+ id: allModeToggleArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
var pluginId = root.item?.data?.pluginId;
if (!pluginId)
- return false;
- SettingsData.launcherPluginVisibility;
- return SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ return;
+ SettingsData.setPluginAllowWithoutTrigger(pluginId, !allModeToggle.isAllowed);
}
+ }
+ }
- DankIcon {
- anchors.centerIn: parent
- name: allModeToggle.isAllowed ? "visibility" : "visibility_off"
- size: 18
- color: allModeToggle.isAllowed ? Theme.primary : Theme.surfaceVariantText
- }
+ Rectangle {
+ visible: !!root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
+ width: typeBadge.implicitWidth + Theme.spacingS * 2
+ height: 20
+ radius: 10
+ color: Theme.surfaceVariantAlpha
+ anchors.verticalCenter: parent.verticalCenter
- MouseArea {
- id: allModeToggleArea
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: {
- var pluginId = root.item?.data?.pluginId;
- if (!pluginId)
- return;
- SettingsData.setPluginAllowWithoutTrigger(pluginId, !allModeToggle.isAllowed);
+ StyledText {
+ id: typeBadge
+ anchors.centerIn: parent
+ text: {
+ if (!root.item)
+ return "";
+ switch (root.item.type) {
+ case "plugin":
+ return I18n.tr("Plugin");
+ case "file":
+ return root.item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File");
+ default:
+ return "";
}
}
+ font.pixelSize: Theme.fontSizeSmall - 2
+ color: Theme.surfaceVariantText
}
+ }
- Rectangle {
- visible: !!root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
- width: typeBadge.implicitWidth + Theme.spacingS * 2
- height: 20
- radius: 10
- color: Theme.surfaceVariantAlpha
- anchors.verticalCenter: parent.verticalCenter
-
- StyledText {
- id: typeBadge
- anchors.centerIn: parent
- text: {
- if (!root.item)
- return "";
- switch (root.item.type) {
- case "plugin":
- return I18n.tr("Plugin");
- case "file":
- return root.item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File");
- default:
- return "";
- }
- }
- font.pixelSize: Theme.fontSizeSmall - 2
- color: Theme.surfaceVariantText
- }
- }
-
- SourceBadge {
- anchors.verticalCenter: parent.verticalCenter
- source: root.item?.type === "app" ? (root.item.source || "") : ""
- glyphSize: 14
- }
+ SourceBadge {
+ anchors.verticalCenter: parent.verticalCenter
+ source: root.item?.type === "app" ? (root.item.source || "") : ""
+ glyphSize: 14
}
}
}