diff --git a/quickshell/Common/CacheData.qml b/quickshell/Common/CacheData.qml
index 4b8d3428..6de7a8bd 100644
--- a/quickshell/Common/CacheData.qml
+++ b/quickshell/Common/CacheData.qml
@@ -178,6 +178,33 @@ Singleton {
}
}
+ function loadLauncherCache() {
+ try {
+ var content = launcherCacheFile.text();
+ if (content && content.trim())
+ return JSON.parse(content);
+ } catch (e) {
+ console.warn("CacheData: Failed to parse launcher cache:", e.message);
+ }
+ return null;
+ }
+
+ function saveLauncherCache(sections) {
+ if (_loading)
+ return;
+ launcherCacheFile.setText(JSON.stringify(sections));
+ }
+
+ FileView {
+ id: launcherCacheFile
+
+ path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/launcher_cache.json"
+ blockLoading: true
+ blockWrites: true
+ atomicWrites: true
+ watchChanges: false
+ }
+
FileView {
id: cacheFile
diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml
index add80034..10de89ca 100644
--- a/quickshell/Modals/DankLauncherV2/Controller.qml
+++ b/quickshell/Modals/DankLauncherV2/Controller.qml
@@ -28,6 +28,7 @@ Item {
property bool keyboardNavigationActive: false
property var _modeSectionsCache: ({})
property bool _queryDrivenSearch: false
+ property bool _diskCacheConsumed: false
property var sectionViewModes: ({})
property var pluginViewPreferences: ({})
property int gridColumns: SettingsData.appLauncherGridColumns
@@ -52,6 +53,8 @@ Item {
target: AppSearchService
function onCacheVersionChanged() {
_clearModeCache();
+ if (!searchQuery && searchMode === "all")
+ performSearch();
}
}
@@ -423,6 +426,30 @@ Item {
var restoreSelection = preserveSelectionAfterUpdate(shouldResetSelection);
var cachedSections = AppSearchService.getCachedDefaultSections();
+ if (!cachedSections && !_diskCacheConsumed && !searchQuery && searchMode === "all" && !pluginFilter) {
+ _diskCacheConsumed = true;
+ var diskSections = _loadDiskCache();
+ if (diskSections) {
+ activePluginId = "";
+ activePluginName = "";
+ activePluginCategories = [];
+ activePluginCategory = "";
+ clearActivePluginViewPreference();
+ for (var i = 0; i < diskSections.length; i++) {
+ if (collapsedSections[diskSections[i].id] !== undefined)
+ diskSections[i].collapsed = collapsedSections[diskSections[i].id];
+ }
+ _applyHighlights(diskSections, "");
+ flatModel = Scorer.flattenSections(diskSections);
+ sections = diskSections;
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+ }
+
if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) {
activePluginId = "";
activePluginName = "";
@@ -431,6 +458,7 @@ Item {
clearActivePluginViewPreference();
var modeCache = _getCachedModeData("all");
if (modeCache) {
+ _applyHighlights(modeCache.sections, "");
sections = modeCache.sections;
flatModel = modeCache.flatModel;
} else {
@@ -442,6 +470,7 @@ Item {
copy.collapsed = collapsedSections[s.id];
return copy;
});
+ _applyHighlights(newSections, "");
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
_setCachedModeData("all", sections, flatModel);
@@ -489,6 +518,7 @@ Item {
}
}
+ _applyHighlights(newSections, triggerMatch.query);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel);
@@ -522,6 +552,7 @@ Item {
if (cachedSections && !searchQuery) {
var modeCache = _getCachedModeData("apps");
if (modeCache) {
+ _applyHighlights(modeCache.sections, "");
sections = modeCache.sections;
flatModel = modeCache.flatModel;
} else {
@@ -536,6 +567,7 @@ Item {
copy.collapsed = collapsedSections[s.id];
return copy;
});
+ _applyHighlights(newSections, "");
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
_setCachedModeData("apps", sections, flatModel);
@@ -564,6 +596,7 @@ Item {
}
}
+ _applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel);
@@ -623,6 +656,7 @@ Item {
}
}
+ _applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel);
@@ -699,11 +733,13 @@ Item {
}
}
+ _applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) {
AppSearchService.setCachedDefaultSections(sections, flatModel);
+ _saveDiskCache(sections);
}
selectedFlatIndex = restoreSelection(flatModel);
@@ -762,6 +798,7 @@ Item {
newSections[i].collapsed = collapsedSections[sid];
}
+ _applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel);
@@ -835,6 +872,7 @@ Item {
newSections.sort(function (a, b) {
return a.priority - b.priority;
});
+ _applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
if (selectedFlatIndex >= flatModel.length) {
@@ -1184,6 +1222,83 @@ Item {
_modeSectionsCache = {};
}
+ function _saveDiskCache(sectionsData) {
+ var serializable = [];
+ for (var i = 0; i < sectionsData.length; i++) {
+ var s = sectionsData[i];
+ var items = [];
+ var srcItems = s.items || [];
+ for (var j = 0; j < srcItems.length; j++) {
+ var it = srcItems[j];
+ items.push({
+ id: it.id,
+ type: it.type,
+ name: it.name || "",
+ subtitle: it.subtitle || "",
+ icon: it.icon || "",
+ iconType: it.iconType || "image",
+ iconFull: it.iconFull || "",
+ section: it.section || "",
+ isCore: it.isCore || false,
+ isBuiltInLauncher: it.isBuiltInLauncher || false,
+ pluginId: it.pluginId || ""
+ });
+ }
+ serializable.push({
+ id: s.id,
+ title: s.title || "",
+ icon: s.icon || "",
+ priority: s.priority || 0,
+ items: items
+ });
+ }
+ CacheData.saveLauncherCache(serializable);
+ }
+
+ function _loadDiskCache() {
+ var cached = CacheData.loadLauncherCache();
+ if (!cached || !Array.isArray(cached) || cached.length === 0)
+ return null;
+
+ var sectionsData = [];
+ for (var i = 0; i < cached.length; i++) {
+ var s = cached[i];
+ var items = [];
+ var srcItems = s.items || [];
+ for (var j = 0; j < srcItems.length; j++) {
+ var it = srcItems[j];
+ items.push({
+ id: it.id || "",
+ type: it.type || "app",
+ name: it.name || "",
+ subtitle: it.subtitle || "",
+ icon: it.icon || "",
+ iconType: it.iconType || "image",
+ iconFull: it.iconFull || "",
+ section: it.section || "",
+ isCore: it.isCore || false,
+ isBuiltInLauncher: it.isBuiltInLauncher || false,
+ pluginId: it.pluginId || "",
+ data: {
+ id: it.id
+ },
+ actions: [],
+ primaryAction: null,
+ _diskCached: true
+ });
+ }
+ sectionsData.push({
+ id: s.id || "",
+ title: s.title || "",
+ icon: s.icon || "",
+ priority: s.priority || 0,
+ items: items,
+ collapsed: false
+ });
+ }
+ return sectionsData;
+ }
+
function updateSelectedItem() {
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
var entry = flatModel[selectedFlatIndex];
@@ -1193,6 +1308,48 @@ Item {
}
}
+ function _applyHighlights(sectionsData, query) {
+ if (!query || query.length === 0) {
+ for (var i = 0; i < sectionsData.length; i++) {
+ var items = sectionsData[i].items;
+ for (var j = 0; j < items.length; j++) {
+ var item = items[j];
+ item._hName = item.name || "";
+ item._hSub = item.subtitle || "";
+ item._hRich = false;
+ }
+ }
+ return;
+ }
+
+ var highlightColor = Theme.primary;
+ var nameColor = Theme.surfaceText;
+ var subColor = Theme.surfaceVariantText;
+ var lowerQuery = query.toLowerCase();
+
+ for (var i = 0; i < sectionsData.length; i++) {
+ var items = sectionsData[i].items;
+ for (var j = 0; j < items.length; j++) {
+ var item = items[j];
+ item._hName = _highlightField(item.name || "", lowerQuery, query.length, nameColor, highlightColor);
+ item._hSub = _highlightField(item.subtitle || "", lowerQuery, query.length, subColor, highlightColor);
+ item._hRich = true;
+ }
+ }
+ }
+
+ function _highlightField(text, lowerQuery, queryLen, baseColor, highlightColor) {
+ if (!text)
+ return "";
+ var idx = text.toLowerCase().indexOf(lowerQuery);
+ if (idx === -1)
+ return text;
+ var before = text.substring(0, idx);
+ var match = text.substring(idx, idx + queryLen);
+ var after = text.substring(idx + queryLen);
+ return '' + before + '' + match + '' + after + '';
+ }
+
function getCurrentSectionViewMode() {
if (selectedFlatIndex < 0 || selectedFlatIndex >= flatModel.length)
return "list";
@@ -1334,6 +1491,10 @@ Item {
}
function executeSelected() {
+ if (searchDebounce.running) {
+ searchDebounce.stop();
+ performSearch();
+ }
if (!selectedItem)
return;
executeItem(selectedItem);
diff --git a/quickshell/Modals/DankLauncherV2/GridItem.qml b/quickshell/Modals/DankLauncherV2/GridItem.qml
index d6d2fdc0..a7f39c33 100644
--- a/quickshell/Modals/DankLauncherV2/GridItem.qml
+++ b/quickshell/Modals/DankLauncherV2/GridItem.qml
@@ -35,21 +35,6 @@ Rectangle {
readonly property int computedIconSize: Math.min(48, Math.max(32, width * 0.45))
- function highlightText(text, query, baseColor) {
- if (!text || !query || query.length === 0)
- return text;
- var lowerText = text.toLowerCase();
- var lowerQuery = query.toLowerCase();
- var idx = lowerText.indexOf(lowerQuery);
- if (idx === -1)
- return text;
- var before = text.substring(0, idx);
- var match = text.substring(idx, idx + query.length);
- var after = text.substring(idx + query.length);
- var highlightColor = Theme.primary;
- return '' + before + '' + '' + match + '' + '' + after + '';
- }
-
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent"
@@ -78,15 +63,8 @@ Rectangle {
Text {
width: parent.width
- text: {
- var query = root.controller?.searchQuery ?? "";
- var name = root.item?.name ?? "";
- var baseColor = root.isSelected ? Theme.primary : Theme.surfaceText;
- if (!query)
- return name;
- return root.highlightText(name, query, baseColor);
- }
- textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText
+ text: root.item?._hName ?? root.item?.name ?? ""
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
font.family: Theme.fontFamily
diff --git a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
index e9502850..3c555904 100644
--- a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
+++ b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
@@ -34,7 +34,7 @@ Popup {
return false;
}
- readonly property bool isCoreApp: item?.type === "app" && item?.isCore
+ readonly property bool isCoreApp: item?.type === "app" && !!item?.isCore
readonly property var coreAppData: isCoreApp ? item?.data ?? null : null
readonly property var desktopEntry: !isCoreApp ? (item?.data ?? null) : null
readonly property string appId: {
diff --git a/quickshell/Modals/DankLauncherV2/NavigationHelpers.js b/quickshell/Modals/DankLauncherV2/NavigationHelpers.js
index f13e2bfd..d5300e10 100644
--- a/quickshell/Modals/DankLauncherV2/NavigationHelpers.js
+++ b/quickshell/Modals/DankLauncherV2/NavigationHelpers.js
@@ -25,6 +25,9 @@ function findPrevNonHeaderIndex(flatModel, startIndex) {
}
function getSectionBounds(flatModel, sectionId) {
+ if (flatModel._sectionBounds && flatModel._sectionBounds[sectionId])
+ return flatModel._sectionBounds[sectionId];
+
var start = -1, end = -1;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) {
diff --git a/quickshell/Modals/DankLauncherV2/ResultItem.qml b/quickshell/Modals/DankLauncherV2/ResultItem.qml
index 4ada194a..e4ad6753 100644
--- a/quickshell/Modals/DankLauncherV2/ResultItem.qml
+++ b/quickshell/Modals/DankLauncherV2/ResultItem.qml
@@ -33,21 +33,6 @@ Rectangle {
}
}
- function highlightText(text, query, baseColor) {
- if (!text || !query || query.length === 0)
- return text;
- var lowerText = text.toLowerCase();
- var lowerQuery = query.toLowerCase();
- var idx = lowerText.indexOf(lowerQuery);
- if (idx === -1)
- return text;
- var before = text.substring(0, idx);
- var match = text.substring(idx, idx + query.length);
- var after = text.substring(idx + query.length);
- var highlightColor = Theme.primary;
- return '' + before + '' + '' + match + '' + '' + after + '';
- }
-
width: parent?.width ?? 200
height: 52
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent"
@@ -110,14 +95,8 @@ Rectangle {
Text {
width: parent.width
- text: {
- var query = root.controller?.searchQuery ?? "";
- var name = root.item?.name ?? "";
- if (!query)
- return name;
- return root.highlightText(name, query, Theme.surfaceText);
- }
- textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText
+ 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
@@ -128,16 +107,8 @@ Rectangle {
Text {
width: parent.width
- text: {
- var query = root.controller?.searchQuery ?? "";
- var subtitle = root.item?.subtitle ?? "";
- if (!subtitle)
- return "";
- if (!query)
- return subtitle;
- return root.highlightText(subtitle, query, Theme.surfaceVariantText);
- }
- textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText
+ 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
@@ -193,7 +164,7 @@ Rectangle {
}
Rectangle {
- visible: root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
+ visible: !!root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
width: typeBadge.implicitWidth + Theme.spacingS * 2
height: 20
radius: 10
diff --git a/quickshell/Modals/DankLauncherV2/ResultsList.qml b/quickshell/Modals/DankLauncherV2/ResultsList.qml
index afd4c51b..2cab9bf2 100644
--- a/quickshell/Modals/DankLauncherV2/ResultsList.qml
+++ b/quickshell/Modals/DankLauncherV2/ResultsList.qml
@@ -11,11 +11,111 @@ Item {
property var controller: null
property int gridColumns: controller?.gridColumns ?? 4
+ property var _visualRows: []
+ property var _flatIndexToRowMap: ({})
+ property var _cumulativeHeights: []
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
+ function _rebuildVisualModel() {
+ var sections = root.controller?.sections ?? [];
+ var rows = [];
+ var indexMap = {};
+ var cumHeights = [];
+ var cumY = 0;
+
+ for (var s = 0; s < sections.length; s++) {
+ var section = sections[s];
+ var sectionId = section.id;
+
+ cumHeights.push(cumY);
+ rows.push({
+ _rowId: "h_" + sectionId,
+ type: "header",
+ section: section,
+ sectionId: sectionId,
+ height: 32
+ });
+ cumY += 32;
+
+ if (section.collapsed)
+ continue;
+
+ var versionTrigger = root.controller?.viewModeVersion ?? 0;
+ void (versionTrigger);
+ var mode = root.controller?.getSectionViewMode(sectionId) ?? "list";
+ var items = section.items ?? [];
+ var flatStartIndex = section.flatStartIndex ?? 0;
+
+ if (mode === "list") {
+ for (var i = 0; i < items.length; i++) {
+ var flatIdx = flatStartIndex + i;
+ indexMap[flatIdx] = rows.length;
+ cumHeights.push(cumY);
+ rows.push({
+ _rowId: items[i].id,
+ type: "list_item",
+ item: items[i],
+ flatIndex: flatIdx,
+ sectionId: sectionId,
+ height: 52
+ });
+ cumY += 52;
+ }
+ } else {
+ var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns;
+ var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / root.gridColumns);
+ var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
+ var numRows = Math.ceil(items.length / cols);
+
+ for (var r = 0; r < numRows; r++) {
+ var rowItems = [];
+ for (var c = 0; c < cols; c++) {
+ var idx = r * cols + c;
+ if (idx >= items.length)
+ break;
+ var fi = flatStartIndex + idx;
+ indexMap[fi] = rows.length;
+ rowItems.push({
+ item: items[idx],
+ flatIndex: fi
+ });
+ }
+ cumHeights.push(cumY);
+ rows.push({
+ _rowId: "gr_" + sectionId + "_" + r,
+ type: "grid_row",
+ items: rowItems,
+ sectionId: sectionId,
+ viewMode: mode,
+ cols: cols,
+ height: cellHeight
+ });
+ cumY += cellHeight;
+ }
+ }
+ }
+
+ root._flatIndexToRowMap = indexMap;
+ root._cumulativeHeights = cumHeights;
+ root._visualRows = rows;
+ }
+
+ onGridColumnsChanged: Qt.callLater(_rebuildVisualModel)
+ onWidthChanged: Qt.callLater(_rebuildVisualModel)
+
+ Connections {
+ target: root.controller
+ function onSectionsChanged() {
+ Qt.callLater(root._rebuildVisualModel);
+ }
+ function onViewModeVersionChanged() {
+ Qt.callLater(root._rebuildVisualModel);
+ }
+ }
+
function resetScroll() {
- mainFlickable.contentY = 0;
+ mainListView.contentY = mainListView.originY;
}
function ensureVisible(index) {
@@ -24,75 +124,10 @@ Item {
var entry = controller.flatModel[index];
if (!entry || entry.isHeader)
return;
- scrollItemIntoView(index, entry.sectionId);
- }
-
- function scrollItemIntoView(flatIndex, sectionId) {
- var sections = controller?.sections ?? [];
- var sectionIndex = -1;
- for (var i = 0; i < sections.length; i++) {
- if (sections[i].id === sectionId) {
- sectionIndex = i;
- break;
- }
- }
- if (sectionIndex < 0)
+ var rowIndex = _flatIndexToRowMap[index];
+ if (rowIndex === undefined)
return;
- var itemInSection = 0;
- var foundSection = false;
- for (var i = 0; i < controller.flatModel.length && i < flatIndex; i++) {
- var e = controller.flatModel[i];
- if (e.isHeader && e.section?.id === sectionId)
- foundSection = true;
- else if (foundSection && !e.isHeader && e.sectionId === sectionId)
- itemInSection++;
- }
-
- var mode = controller.getSectionViewMode(sectionId);
- var sectionY = 0;
- for (var i = 0; i < sectionIndex; i++) {
- sectionY += getSectionHeight(sections[i]);
- }
-
- var itemY, itemHeight;
- if (mode === "list") {
- itemY = itemInSection * 52;
- itemHeight = 52;
- } else {
- var cols = controller.getGridColumns(sectionId);
- var cellWidth = mode === "tile" ? Math.floor(mainFlickable.width / 3) : Math.floor(mainFlickable.width / root.gridColumns);
- var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
- var row = Math.floor(itemInSection / cols);
- itemY = row * cellHeight;
- itemHeight = cellHeight;
- }
-
- var targetY = sectionY + 32 + itemY;
- var targetBottom = targetY + itemHeight;
- var stickyHeight = mainFlickable.contentY > 0 ? 32 : 0;
-
- var shadowPadding = 24;
- if (targetY < mainFlickable.contentY + stickyHeight) {
- mainFlickable.contentY = Math.max(0, targetY - 32);
- } else if (targetBottom > mainFlickable.contentY + mainFlickable.height - shadowPadding) {
- mainFlickable.contentY = Math.min(mainFlickable.contentHeight - mainFlickable.height, targetBottom - mainFlickable.height + shadowPadding);
- }
- }
-
- function getSectionHeight(section) {
- var mode = controller?.getSectionViewMode(section.id) ?? "list";
- if (section.collapsed)
- return 32;
-
- if (mode === "list") {
- return 32 + (section.items?.length ?? 0) * 52;
- } else {
- var cols = controller?.getGridColumns(section.id) ?? root.gridColumns;
- var rows = Math.ceil((section.items?.length ?? 0) / cols);
- var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / cols);
- var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
- return 32 + rows * cellHeight;
- }
+ mainListView.positionViewAtIndex(rowIndex, ListView.Contain);
}
function getSelectedItemPosition() {
@@ -104,42 +139,30 @@ Item {
if (!entry || entry.isHeader)
return fallback;
- var sections = controller.sections;
- var sectionIndex = -1;
- for (var i = 0; i < sections.length; i++) {
- if (sections[i].id === entry.sectionId) {
- sectionIndex = i;
- break;
- }
- }
- if (sectionIndex < 0)
+ var rowIndex = _flatIndexToRowMap[controller.selectedFlatIndex];
+ if (rowIndex === undefined)
return fallback;
- var sectionY = 0;
- for (var i = 0; i < sectionIndex; i++) {
- sectionY += getSectionHeight(sections[i]);
+ var rowY = (rowIndex < _cumulativeHeights.length) ? _cumulativeHeights[rowIndex] : 0;
+ var row = _visualRows[rowIndex];
+ if (!row)
+ return fallback;
+
+ var itemX = width / 2;
+ var itemH = row.height;
+
+ if (row.type === "grid_row") {
+ var rowItems = row.items;
+ for (var i = 0; i < rowItems.length; i++) {
+ if (rowItems[i].flatIndex === controller.selectedFlatIndex) {
+ var cellWidth = row.viewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / row.cols);
+ itemX = i * cellWidth + cellWidth / 2;
+ break;
+ }
+ }
}
- var mode = controller.getSectionViewMode(entry.sectionId);
- var itemInSection = entry.indexInSection || 0;
-
- var itemY, itemX, itemH;
- if (mode === "list") {
- itemY = sectionY + 32 + itemInSection * 52;
- itemX = width / 2;
- itemH = 52;
- } else {
- var cols = controller.getGridColumns(entry.sectionId);
- var cellWidth = mode === "tile" ? Math.floor(width / 3) : Math.floor(width / cols);
- var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
- var row = Math.floor(itemInSection / cols);
- var col = itemInSection % cols;
- itemY = sectionY + 32 + row * cellHeight;
- itemX = col * cellWidth + cellWidth / 2;
- itemH = cellHeight;
- }
-
- var visualY = itemY - mainFlickable.contentY + itemH / 2;
+ var visualY = rowY - mainListView.contentY + mainListView.originY + itemH / 2;
var clampedY = Math.max(40, Math.min(height - 40, visualY));
return mapToItem(null, itemX, clampedY);
}
@@ -153,161 +176,124 @@ Item {
}
}
- DankFlickable {
- id: mainFlickable
+ DankListView {
+ id: mainListView
anchors.fill: parent
- contentWidth: width
- contentHeight: sectionsColumn.height
clip: true
+ scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0
- Component.onCompleted: {
- verticalScrollBar.targetFlickable = mainFlickable;
- verticalScrollBar.parent = root;
- verticalScrollBar.z = 102;
- verticalScrollBar.anchors.right = root.right;
- verticalScrollBar.anchors.top = root.top;
- verticalScrollBar.anchors.bottom = root.bottom;
+ model: ScriptModel {
+ values: root._visualRows
+ objectProp: "_rowId"
}
- Column {
- id: sectionsColumn
- width: parent.width
+ add: null
+ remove: null
+ displaced: null
+ move: null
- Repeater {
- model: ScriptModel {
- values: root.controller?.sections ?? []
- objectProp: "id"
+ delegate: Item {
+ id: delegateRoot
+ required property var modelData
+ required property int index
+
+ width: mainListView.width
+ height: modelData?.height ?? 52
+
+ SectionHeader {
+ anchors.fill: parent
+ visible: delegateRoot.modelData?.type === "header"
+ section: delegateRoot.modelData?.section ?? null
+ controller: root.controller
+ viewMode: {
+ var vt = root.controller?.viewModeVersion ?? 0;
+ void (vt);
+ return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
+ }
+ canChangeViewMode: {
+ var vt = root.controller?.viewModeVersion ?? 0;
+ void (vt);
+ return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
+ }
+ canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
+ }
+
+ ResultItem {
+ anchors.fill: parent
+ visible: delegateRoot.modelData?.type === "list_item"
+ item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
+ isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
+
+ onClicked: {
+ if (root.controller && delegateRoot.modelData?.item) {
+ root.controller.executeItem(delegateRoot.modelData.item);
+ }
}
- Column {
- id: sectionDelegate
- required property var modelData
- required property int index
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
+ }
+ }
- readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
- readonly property string sectionId: modelData?.id ?? ""
- readonly property string currentViewMode: {
- void (versionTrigger);
- return root.controller?.getSectionViewMode(sectionId) ?? "list";
- }
- readonly property bool isGridMode: currentViewMode === "grid" || currentViewMode === "tile"
- readonly property bool isCollapsed: modelData?.collapsed ?? false
+ Row {
+ id: gridRowContent
+ anchors.fill: parent
+ visible: delegateRoot.modelData?.type === "grid_row"
- width: sectionsColumn.width
+ Repeater {
+ model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
- SectionHeader {
- width: parent.width
- height: 32
- section: sectionDelegate.modelData
- controller: root.controller
- viewMode: sectionDelegate.currentViewMode
- canChangeViewMode: root.controller?.canChangeSectionViewMode(sectionDelegate.sectionId) ?? false
- canCollapse: root.controller?.canCollapseSection(sectionDelegate.sectionId) ?? false
- }
+ Item {
+ id: gridCellDelegate
+ required property var modelData
+ required property int index
- Column {
- id: listContent
- width: parent.width
- visible: !sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
+ readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
- Repeater {
- model: ScriptModel {
- values: sectionDelegate.isGridMode || sectionDelegate.isCollapsed ? [] : (sectionDelegate.modelData?.items ?? [])
- objectProp: "id"
+ width: cellWidth
+ height: delegateRoot.height
+
+ GridItem {
+ width: parent.width - 4
+ height: parent.height - 4
+ anchors.centerIn: parent
+ visible: delegateRoot.modelData?.viewMode === "grid"
+ item: gridCellDelegate.modelData?.item ?? null
+ isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
+
+ onClicked: {
+ if (root.controller && gridCellDelegate.modelData?.item) {
+ root.controller.executeItem(gridCellDelegate.modelData.item);
+ }
}
- ResultItem {
- required property var modelData
- required property int index
-
- readonly property int computedFlatIndex: (sectionDelegate.modelData?.flatStartIndex ?? 0) + index
-
- width: listContent.width
- height: 52
- item: modelData
- isSelected: computedFlatIndex === root.controller?.selectedFlatIndex
- controller: root.controller
- flatIndex: computedFlatIndex
-
- onClicked: {
- if (root.controller) {
- root.controller.executeItem(modelData);
- }
- }
-
- onRightClicked: (mouseX, mouseY) => {
- root.itemRightClicked(computedFlatIndex, modelData, mouseX, mouseY);
- }
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
}
- }
- Grid {
- id: gridContent
- width: parent.width
- visible: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
- columns: sectionDelegate.currentViewMode === "tile" ? 3 : root.gridColumns
+ TileItem {
+ width: parent.width - 4
+ height: parent.height - 4
+ anchors.centerIn: parent
+ visible: delegateRoot.modelData?.viewMode === "tile"
+ item: gridCellDelegate.modelData?.item ?? null
+ isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
- readonly property real cellWidth: sectionDelegate.currentViewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / root.gridColumns)
- readonly property real cellHeight: sectionDelegate.currentViewMode === "tile" ? cellWidth * 0.75 : cellWidth + 24
-
- Repeater {
- model: ScriptModel {
- values: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed ? (sectionDelegate.modelData?.items ?? []) : []
- objectProp: "id"
+ onClicked: {
+ if (root.controller && gridCellDelegate.modelData?.item) {
+ root.controller.executeItem(gridCellDelegate.modelData.item);
+ }
}
- Item {
- id: gridDelegateItem
- required property var modelData
- required property int index
-
- width: gridContent.cellWidth
- height: gridContent.cellHeight
-
- readonly property int cachedFlatIndex: (sectionDelegate.modelData?.flatStartIndex ?? 0) + index
-
- GridItem {
- width: parent.width - 4
- height: parent.height - 4
- anchors.centerIn: parent
- visible: sectionDelegate.currentViewMode === "grid"
- item: gridDelegateItem.modelData
- isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
- controller: root.controller
- flatIndex: gridDelegateItem.cachedFlatIndex
-
- onClicked: {
- if (root.controller) {
- root.controller.executeItem(gridDelegateItem.modelData);
- }
- }
-
- onRightClicked: (mouseX, mouseY) => {
- root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
- }
- }
-
- TileItem {
- width: parent.width - 4
- height: parent.height - 4
- anchors.centerIn: parent
- visible: sectionDelegate.currentViewMode === "tile"
- item: gridDelegateItem.modelData
- isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
- controller: root.controller
- flatIndex: gridDelegateItem.cachedFlatIndex
-
- onClicked: {
- if (root.controller) {
- root.controller.executeItem(gridDelegateItem.modelData);
- }
- }
-
- onRightClicked: (mouseX, mouseY) => {
- root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
- }
- }
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
}
}
@@ -324,9 +310,9 @@ Item {
height: 24
z: 100
visible: {
- if (mainFlickable.contentHeight <= mainFlickable.height)
+ if (mainListView.contentHeight <= mainListView.height)
return false;
- var atBottom = mainFlickable.contentY >= mainFlickable.contentHeight - mainFlickable.height - 5;
+ var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
if (atBottom)
return false;
@@ -371,23 +357,31 @@ Item {
readonly property var stickyHeaderSection: {
if (!root.controller?.sections || root.controller.sections.length === 0)
return null;
- var sections = root.controller.sections;
- if (sections.length === 0)
- return null;
- var scrollY = mainFlickable.contentY;
+ var scrollY = mainListView.contentY - mainListView.originY;
if (scrollY <= 0)
return null;
- var y = 0;
- for (var i = 0; i < sections.length; i++) {
- var section = sections[i];
- var sectionHeight = root.getSectionHeight(section);
- if (scrollY < y + sectionHeight)
- return section;
- y += sectionHeight;
+ var rows = root._visualRows;
+ var heights = root._cumulativeHeights;
+ if (rows.length === 0 || heights.length === 0)
+ return null;
+
+ var lo = 0;
+ var hi = rows.length - 1;
+ while (lo < hi) {
+ var mid = (lo + hi + 1) >> 1;
+ if (mid < heights.length && heights[mid] <= scrollY)
+ lo = mid;
+ else
+ hi = mid - 1;
}
- return sections[sections.length - 1];
+
+ for (var i = lo; i >= 0; i--) {
+ if (rows[i].type === "header")
+ return rows[i].section;
+ }
+ return null;
}
SectionHeader {
diff --git a/quickshell/Modals/DankLauncherV2/Scorer.js b/quickshell/Modals/DankLauncherV2/Scorer.js
index bde47736..0e917e28 100644
--- a/quickshell/Modals/DankLauncherV2/Scorer.js
+++ b/quickshell/Modals/DankLauncherV2/Scorer.js
@@ -220,6 +220,7 @@ function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSec
function flattenSections(sections) {
var flat = []
+ var bounds = {}
for (var i = 0; i < sections.length; i++) {
var section = sections[i]
@@ -231,7 +232,8 @@ function flattenSections(sections) {
sectionIndex: i
})
- section.flatStartIndex = flat.length
+ var itemStart = flat.length
+ section.flatStartIndex = itemStart
if (!section.collapsed) {
for (var j = 0; j < section.items.length; j++) {
@@ -244,7 +246,18 @@ function flattenSections(sections) {
})
}
}
+
+ var itemEnd = flat.length - 1
+ var itemCount = flat.length - itemStart
+ if (itemCount > 0) {
+ bounds[section.id] = {
+ start: itemStart,
+ end: itemEnd,
+ count: itemCount
+ }
+ }
}
+ flat._sectionBounds = bounds
return flat
}
diff --git a/quickshell/Modals/DankLauncherV2/TileItem.qml b/quickshell/Modals/DankLauncherV2/TileItem.qml
index 1869fe62..07c7bee1 100644
--- a/quickshell/Modals/DankLauncherV2/TileItem.qml
+++ b/quickshell/Modals/DankLauncherV2/TileItem.qml
@@ -23,21 +23,6 @@ Rectangle {
border.width: isSelected ? 2 : 0
border.color: Theme.primary
- function highlightText(text, query, baseColor) {
- if (!text || !query || query.length === 0)
- return text;
- var lowerText = text.toLowerCase();
- var lowerQuery = query.toLowerCase();
- var idx = lowerText.indexOf(lowerQuery);
- if (idx === -1)
- return text;
- var before = text.substring(0, idx);
- var match = text.substring(idx, idx + query.length);
- var after = text.substring(idx + query.length);
- var highlightColor = Theme.primary;
- return '' + before + '' + '' + match + '' + '' + after + '';
- }
-
readonly property string toplevelId: item?.data?.toplevelId ?? ""
readonly property var waylandToplevel: {
if (!toplevelId || !item?.pluginId)
@@ -133,14 +118,8 @@ Rectangle {
id: labelText
anchors.fill: parent
anchors.margins: Theme.spacingXS
- text: {
- var query = root.controller?.searchQuery ?? "";
- var name = root.item?.name ?? "";
- if (!query)
- return name;
- return root.highlightText(name, query, Theme.surfaceText);
- }
- textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText
+ text: root.item?._hName ?? root.item?.name ?? ""
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
font.pixelSize: Theme.fontSizeSmall
font.family: Theme.fontFamily
color: Theme.surfaceText
diff --git a/quickshell/Widgets/DankListView.qml b/quickshell/Widgets/DankListView.qml
index 7b490717..888de7c7 100644
--- a/quickshell/Widgets/DankListView.qml
+++ b/quickshell/Widgets/DankListView.qml
@@ -6,6 +6,7 @@ import qs.Widgets
ListView {
id: listView
+ property real scrollBarTopMargin: 0
property real mouseWheelSpeed: 60
property real savedY: 0
property bool justChanged: false
@@ -208,5 +209,6 @@ ListView {
ScrollBar.vertical: DankScrollbar {
id: vbar
+ topPadding: listView.scrollBarTopMargin
}
}