Keep image picker work bounded for large theme collections

This commit is contained in:
Ric Lewis committed 2026-10-02 22:45:39 -07:00
1 parent a85e29abb5
commit 2ba1015ef2
6 files changed
+240 -33

No files matched your search

+48 -2
View File
@@ -42,6 +42,45 @@ assertEqual(picker.filteredPosition(images, 2, 'dark'), 1, 'image picker compute
assertEqual(picker.selectedFilteredPosition(images, 2, 'dark'), 0, 'image picker selected filtered position falls back when selected is hidden')
assertEqual(picker.nextSelectedIndexForFilter(images, 0, 'dark'), 1, 'image picker moves selection to first match when filter hides current item')
class WindowModel {
constructor() { this.items = []; this.insertions = 0 }
get count() { return this.items.length }
get(i) { return this.items[i] }
remove(i) { this.items.splice(i, 1) }
insert(i, item) { this.items.splice(i, 0, { ...item }); this.insertions++ }
move(from, to) { this.items.splice(to, 0, this.items.splice(from, 1)[0]) }
setProperty(i, key, value) { this.items[i][key] = value }
}
for (const count of [0, 1, 200, 530, 10000]) {
const collection = Array.from({ length: count }, (_, i) => ({ filePath: `/themes/theme-${i}.png` }))
const indices = picker.matchingIndices(collection, '')
const model = new WindowModel()
for (const selected of [0, Math.min(1, count - 1), Math.floor(count / 2), count - 1]) {
const window = picker.visibleWindow(indices, selected, 16)
picker.syncWindow(model, window)
assertDeepEqual(model.items, window, `carousel window reconciles ${count} images at ${selected}`)
assert(model.count <= 33, `carousel bounds delegates for ${count} images`)
if (count) assert(window.some(item => item.imageIndex === selected && item.relativeIndex === 0), `carousel includes selection in ${count} images`)
}
const matches = picker.matchingIndices(collection, 'theme-19')
picker.syncWindow(model, picker.visibleWindow(matches, matches[0], 16))
assert(model.items.every(item => collection[item.imageIndex].filePath.includes('theme-19')), `carousel filters ${count} images`)
picker.syncWindow(model, picker.visibleWindow([], 0, 16))
assertEqual(model.count, 0, `carousel clears ${count} images when there are no matches`)
}
const windowModel = new WindowModel()
const allIndices = Array.from({ length: 200 }, (_, i) => i)
picker.syncWindow(windowModel, picker.visibleWindow(allIndices, 100, 16))
const retained = windowModel.get(17)
picker.syncWindow(windowModel, picker.visibleWindow(allIndices, 101, 16))
assertEqual(windowModel.get(16), retained, 'carousel retains overlapping delegates when navigating')
assertEqual(windowModel.insertions, 34, 'carousel creates only one new delegate for an adjacent selection')
for (const radius of [1, 8, 16]) {
assertEqual(picker.visibleWindow(allIndices, 100, radius).length, radius * 2 + 1, `carousel scales its window to radius ${radius}`)
}
const imagePickerQml = fs.readFileSync(path.join(root, 'shell/plugins/image-picker/ImagePicker.qml'), 'utf8')
assert(
/function preloadRows[\s\S]*if \(opened \|\| requestActive\) return/.test(imagePickerQml),
@@ -74,7 +113,14 @@ assert(
'image picker parks on OverlayWindow and takes the keyboard once images load'
)
assert(
/source: item\.sourceActivated && item\.thumbnailPath \? Util\.fileUrl\(item\.thumbnailPath\) : ""[\s\S]*asynchronous: false/.test(imagePickerQml),
'image picker loads activated thumbnails synchronously to avoid carousel flicker'
/model: visibleImages/.test(imagePickerQml) &&
/sourceSize: Qt\.size\(root\.expandedWidth, root\.expandedHeight\)/.test(imagePickerQml) &&
/asynchronous: true\s*cache: false/.test(imagePickerQml),
'image picker renders its window with bounded asynchronous decoding'
)
assert(
imagePickerQml.includes('(item.selected || root.neighborImagesEnabled)') &&
/onStatusChanged: if \(item.selected && \(status === Image.Ready \|\| status === Image.Error\)\) root.neighborImagesEnabled = true/.test(imagePickerQml),
'image picker prioritizes the selected preview and releases neighbors on success or failure'
)
JS