Fix/3958 workspace indicator icon perf (#3961)

port 2.0

(cherry picked from commit b2e1817aad)
This commit is contained in:
hecate cantus authored and dux-ci[bot] committed 2026-10-27 17:10:38 +00:00
1 parent 69fc822ffd
commit d2ad778e6f
4 files changed
+57 -25

No files matched your search

@@ -83,7 +83,7 @@ BasePill {
function getWorkspaceIcons(ws) {
_desktopEntriesUpdateTrigger;
if (!root.opt("showWorkspaceApps") || !ws || !root.useNativeWorkspaces || ws.placeholder) {
if (!root.opt("showWorkspaceApps") || !ws || !root.useNativeWorkspaces || ws.placeholder || !IconThemeService.ready) {
return [];
}
@@ -675,8 +675,8 @@ BasePill {
const didReorder = wasDragging && root.dragTargetIndex >= 0 && root.dragTargetIndex !== root.dragSourceIndex;
if (didReorder) {
const sourceWs = root.workspaceList[root.dragSourceIndex];
const targetWs = root.workspaceList[root.dragTargetIndex];
const sourceWs = root.recordOf(root.workspaceList[root.dragSourceIndex]);
const targetWs = root.recordOf(root.workspaceList[root.dragTargetIndex]);
if (sourceWs && targetWs && sourceWs.id !== undefined && targetWs.idx !== undefined) {
root.suppressShiftAnimation = true;
@@ -983,6 +983,7 @@ BasePill {
Repeater {
model: ScriptModel {
values: loadedIcons.slice(0, root.opt("maxWorkspaceIcons"))
comparisonMode: ObjectComparison.Structure
}
delegate: Item {
width: root.appIconSize
@@ -1141,6 +1142,7 @@ BasePill {
Repeater {
model: ScriptModel {
values: loadedIcons.slice(0, root.opt("maxWorkspaceIcons"))
comparisonMode: ObjectComparison.Structure
}
delegate: Item {
width: root.appIconSize
@@ -1281,6 +1283,8 @@ BasePill {
}
readonly property var rootCurrentWorkspace: root.currentWorkspace
onRootCurrentWorkspaceChanged: updateAllData()
readonly property bool iconIndexReady: IconThemeService.ready
onIconIndexReadyChanged: updateAllData()
Connections {
target: SettingsData
function onBarConfigsChanged() {
+9 -1
View File
@@ -40,6 +40,14 @@ function chainIndex(path, searchDirs, looseDirs) {
return searchDirs.length + looseDirs.length;
}
const LARGEST_PREFERRED_RASTER = 256;
function rasterScore(size) {
if (size <= LARGEST_PREFERRED_RASTER)
return LARGEST_PREFERRED_RASTER + size;
return Math.max(0, LARGEST_PREFERRED_RASTER - size / 8);
}
function score(path, searchDirs, looseDirs) {
let s = 0;
if (path.includes("/apps/"))
@@ -59,7 +67,7 @@ function score(path, searchDirs, looseDirs) {
} else {
const m = path.match(/\/(\d+)(?:x\d+)?(?:@\d+x)?\//);
if (m)
s += Math.min(parseInt(m[1]), 999);
s += rasterScore(parseInt(m[1]));
}
return s;
}
+28 -21
View File
@@ -61,25 +61,24 @@ QtObject {
"minCount": showPadding ? paddingCount : 0,
"showSpecial": showSpecial
});
if (CompositorService.ephemeralWorkspaces)
return hyprlandSlotList(baseList);
if (!showPadding || CompositorService.supportsPersistentWorkspaces || (useAqueous && baseList.length === 0))
return baseList;
return padWorkspaces(baseList);
const slots = slotList(baseList);
if (CompositorService.ephemeralWorkspaces || !showPadding || CompositorService.supportsPersistentWorkspaces || (useAqueous && baseList.length === 0))
return slots;
return padWorkspaces(slots);
}
readonly property var realWorkspaces: workspaceList.filter(ws => ws && !recordOf(ws).placeholder)
readonly property var realWorkspaces: workspaceList.map(ws => recordOf(ws)).filter(ws => ws && !ws.placeholder)
readonly property bool hasWorkspaces: realWorkspaces.length > 0
readonly property bool available: useNativeWorkspaces || (useExtWorkspace && hasWorkspaces)
onCompositorNameChanged: {
_placeholderPool = [];
_hyprSlotPool = {};
_slotPool = {};
}
// Hyprland creates/destroys workspaces on empty enter/leave; slots keyed by id keep delegate identity so pills animate instead of popping
property var _hyprSlotPool: ({})
readonly property Component _hyprSlotComponent: Component {
property var _slotPool: ({})
readonly property Component _slotComponent: Component {
QtObject {
property var ws: null
}
@@ -91,19 +90,27 @@ QtObject {
return entry.ws;
}
function _hyprSlot(key, ws) {
let slot = _hyprSlotPool[key];
if (!slot) {
slot = _hyprSlotComponent.createObject(root);
_hyprSlotPool[key] = slot;
function slotList(raw) {
const live = {};
const slots = raw.map(ws => {
const key = ws.id !== null && ws.id !== undefined && !ws.special ? ws.id : (ws.special ? "special:" : "name:") + (ws.name ?? "");
let slot = _slotPool[key];
if (!slot) {
slot = _slotComponent.createObject(root);
_slotPool[key] = slot;
}
if (slot.ws !== ws)
slot.ws = ws;
live[key] = true;
return slot;
});
for (const key in _slotPool) {
if (live[key])
continue;
_slotPool[key].destroy();
delete _slotPool[key];
}
if (slot.ws !== ws)
slot.ws = ws;
return slot;
}
function hyprlandSlotList(raw) {
return raw.map(ws => _hyprSlot(ws.id > 0 ? ws.id : (ws.special ? "special:" : "name:") + (ws.name ?? ""), ws));
return slots;
}
// Stable placeholder instances so ScriptModel reuses padding delegates instead of recreating them on workspace churn
@@ -32,6 +32,19 @@ test("index keeps the existing category, root, format, size and tie priorities",
assert.equal(build(ties).icon, ties[0]);
});
test("index prefers rasters up to 256px over larger ones, and the smallest of those otherwise", () => {
const cases = [
["/system/hicolor/apps/1024x1024/icon.png", "/system/hicolor/apps/256x256/icon.png"],
["/system/hicolor/apps/512x512/icon.png", "/system/hicolor/apps/128x128/icon.png"],
["/system/hicolor/apps/1024x1024/icon.png", "/system/hicolor/apps/512x512/icon.png"],
["/system/hicolor/apps/256x256/icon.png", "/system/hicolor/apps/scalable/icon.png"]
];
for (const [loser, winner] of cases) {
assert.equal(build([loser, winner]).icon, winner);
assert.equal(build([winner, loser]).icon, winner);
}
});
test("index bounds retained paths by resolvable names and handles object property names", () => {
const paths = ["/user/theme/apps/icon.svg", "/user/theme/apps/16/icon.png", "/system/pixmaps/loose.xpm", "/user/theme/apps/__proto__.svg", "/user/theme/apps/constructor.png", "/user/theme/apps/not a name.svg"];
const result = build(paths);