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 += ""; + out += "…"; + } else { + while (openTags.length > 0) + out += ""; + } + + 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 } } }