speed up rendering w/ virtualization

This commit is contained in:
2026-06-16 20:32:55 -07:00
parent 991c60df65
commit 128db9b427
2 changed files with 67 additions and 8 deletions
+40 -6
View File
@@ -45,6 +45,9 @@ class OrgTodoListView extends ItemView {
this.filterText = "";
this.activeTags = new Set();
this.showDone = false;
this.ROW_H = 28; // fixed row height in px — MUST match CSS
this.BUFFER = 5; // extra rows above/below viewport, hides scroll seams
this.filtered = [];
}
getViewType() {
@@ -210,9 +213,8 @@ class OrgTodoListView extends ItemView {
}
renderList() {
this.listEl.empty();
const filtered = this.tasks.filter((t) => {
// 1. Filter (cheap — pure in-memory).
this.filtered = this.tasks.filter((t) => {
if (!this.showDone && t.done) return false;
if (this.activeTags.size > 0) {
for (const tag of this.activeTags) if (!t.tags.includes(tag)) return false;
@@ -224,13 +226,45 @@ class OrgTodoListView extends ItemView {
return true;
});
if (filtered.length === 0) {
this.listEl.empty();
if (this.filtered.length === 0) {
this.listEl.createEl("div", { text: "No matching tasks.", cls: "org-todo-empty" });
return;
}
for (const t of filtered) {
const row = this.listEl.createDiv({ cls: "org-todo-row" });
// 2. A tall spacer establishes the full scrollable height...
this.spacerEl = this.listEl.createDiv({ cls: "org-todo-spacer" });
this.spacerEl.style.height = this.filtered.length * this.ROW_H + "px";
// 3. ...and an absolutely-positioned layer holds the handful of visible rows.
this.rowsEl = this.spacerEl.createDiv({ cls: "org-todo-rows" });
// Repaint visible rows on scroll. Registered once per renderList; the old
// listEl (and its listener) is discarded by empty() above.
this.listEl.onscroll = () => this.renderVisible();
this.renderVisible();
}
// Paints only the rows currently in the viewport, recycling on scroll.
renderVisible() {
if (!this.filtered || this.filtered.length === 0) return;
const scrollTop = this.listEl.scrollTop;
const viewportH = this.listEl.clientHeight;
const first = Math.max(0, Math.floor(scrollTop / this.ROW_H) - this.BUFFER);
const visibleCount = Math.ceil(viewportH / this.ROW_H) + this.BUFFER * 2;
const last = Math.min(this.filtered.length, first + visibleCount);
// Offset the rows layer so the painted slice sits at the right scroll position.
this.rowsEl.style.transform = `translateY(${first * this.ROW_H}px)`;
this.rowsEl.empty();
for (let i = first; i < last; i++) {
const t = this.filtered[i];
const row = this.rowsEl.createDiv({ cls: "org-todo-row" });
const box = row.createEl("input", { type: "checkbox" });
box.checked = t.done;