Progressive week rendering for smoother operation.
This commit is contained in:
parent
c9f3104e8b
commit
70ffd2881f
@ -115,27 +115,42 @@ const contentHeight = computed(() => {
|
||||
|
||||
const visibleWeeks = ref([])
|
||||
let lastScrollRange = { startVW: null, endVW: null }
|
||||
let windowTimer = null
|
||||
let dataTimer = null
|
||||
const WINDOW_DEBOUNCE_MS = 30
|
||||
const DATA_DEBOUNCE_MS = 40
|
||||
let updating = 0 // 0 idle, 1 window incremental, 2 full rebuild
|
||||
function scheduleWindowUpdate(reason) {
|
||||
if (windowTimer) return
|
||||
windowTimer = setTimeout(() => {
|
||||
windowTimer = null
|
||||
const fn = () => updateVisibleWeeks(reason)
|
||||
if ('requestIdleCallback' in window) requestIdleCallback(fn, { timeout: 80 })
|
||||
else requestAnimationFrame(fn)
|
||||
}, WINDOW_DEBOUNCE_MS)
|
||||
if (updating !== 0) return
|
||||
updating = 1
|
||||
const run = () => {
|
||||
let complete = true
|
||||
try {
|
||||
complete = updateVisibleWeeks(reason)
|
||||
} finally {
|
||||
updating = 0
|
||||
}
|
||||
if (!complete) scheduleWindowUpdate('incremental-build')
|
||||
}
|
||||
if ('requestIdleCallback' in window) requestIdleCallback(run, { timeout: 16 })
|
||||
else requestAnimationFrame(run)
|
||||
}
|
||||
function scheduleDataRebuild(reason) {
|
||||
if (dataTimer) return
|
||||
dataTimer = setTimeout(() => {
|
||||
dataTimer = null
|
||||
const fn = () => rebuildVisibleWeeks(reason)
|
||||
if ('requestIdleCallback' in window) requestIdleCallback(fn, { timeout: 120 })
|
||||
else requestAnimationFrame(fn)
|
||||
}, DATA_DEBOUNCE_MS)
|
||||
if (updating === 2) return // already rebuilding
|
||||
const doRebuild = () => {
|
||||
updating = 2
|
||||
const run = () => {
|
||||
try {
|
||||
rebuildVisibleWeeks(reason)
|
||||
} finally {
|
||||
updating = 0
|
||||
}
|
||||
}
|
||||
if ('requestIdleCallback' in window) requestIdleCallback(run, { timeout: 60 })
|
||||
else requestAnimationFrame(run)
|
||||
}
|
||||
// If we're mid incremental window update, defer slightly to next frame
|
||||
if (updating === 1) {
|
||||
requestAnimationFrame(doRebuild)
|
||||
return
|
||||
}
|
||||
doRebuild()
|
||||
}
|
||||
|
||||
const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate })
|
||||
@ -177,6 +192,7 @@ const selectedDateRange = computed(() => {
|
||||
})
|
||||
|
||||
function updateVisibleWeeks(reason) {
|
||||
// Compute desired virtual week window with buffer
|
||||
const buffer = 4
|
||||
const currentScrollTop = viewport.value?.scrollTop ?? scrollTop.value
|
||||
const startIdx = Math.floor((currentScrollTop - buffer * rowHeight.value) / rowHeight.value)
|
||||
@ -185,34 +201,86 @@ function updateVisibleWeeks(reason) {
|
||||
)
|
||||
const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value)
|
||||
const endVW = Math.min(maxVirtualWeek.value, endIdx + minVirtualWeek.value)
|
||||
if (
|
||||
lastScrollRange.startVW === startVW &&
|
||||
lastScrollRange.endVW === endVW &&
|
||||
visibleWeeks.value.length
|
||||
)
|
||||
return
|
||||
|
||||
// Step 1: prune anything outside the desired window
|
||||
if (visibleWeeks.value.length) {
|
||||
while (visibleWeeks.value.length && visibleWeeks.value[0].virtualWeek < startVW)
|
||||
while (visibleWeeks.value.length && visibleWeeks.value[0].virtualWeek < startVW) {
|
||||
visibleWeeks.value.shift()
|
||||
}
|
||||
while (
|
||||
visibleWeeks.value.length &&
|
||||
visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek > endVW
|
||||
)
|
||||
) {
|
||||
visibleWeeks.value.pop()
|
||||
let needFirst = visibleWeeks.value[0]?.virtualWeek
|
||||
if (visibleWeeks.value.length === 0) needFirst = endVW + 1
|
||||
for (let vw = (needFirst ?? startVW) - 1; vw >= startVW; vw--)
|
||||
visibleWeeks.value.unshift(createWeek(vw))
|
||||
let needLast = visibleWeeks.value[visibleWeeks.value.length - 1]?.virtualWeek
|
||||
for (let vw = (needLast ?? startVW - 1) + 1; vw <= endVW; vw++)
|
||||
visibleWeeks.value.push(createWeek(vw))
|
||||
lastScrollRange = { startVW, endVW }
|
||||
return
|
||||
}
|
||||
const weeks = []
|
||||
for (let vw = startVW; vw <= endVW; vw++) weeks.push(createWeek(vw))
|
||||
visibleWeeks.value = weeks
|
||||
}
|
||||
|
||||
// Step 2: ensure no gaps; add at most one adjacent missing week each pass
|
||||
let added = false
|
||||
const len = visibleWeeks.value.length
|
||||
if (len === 0) {
|
||||
visibleWeeks.value.push(createWeek(startVW))
|
||||
added = true
|
||||
} else {
|
||||
// Sort defensively (should already be sorted)
|
||||
visibleWeeks.value.sort((a, b) => a.virtualWeek - b.virtualWeek)
|
||||
const firstVW = visibleWeeks.value[0].virtualWeek
|
||||
const lastVW = visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek
|
||||
if (firstVW > startVW) {
|
||||
// Add one week just before current first to close front gap gradually
|
||||
visibleWeeks.value.unshift(createWeek(firstVW - 1))
|
||||
added = true
|
||||
} else {
|
||||
// Look for first internal gap
|
||||
let gapInserted = false
|
||||
for (let i = 0; i < visibleWeeks.value.length - 1; i++) {
|
||||
const curVW = visibleWeeks.value[i].virtualWeek
|
||||
const nextVW = visibleWeeks.value[i + 1].virtualWeek
|
||||
if (nextVW - curVW > 1 && curVW < endVW) {
|
||||
// Insert the immediate missing week after curVW
|
||||
visibleWeeks.value.splice(i + 1, 0, createWeek(curVW + 1))
|
||||
added = true
|
||||
gapInserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!gapInserted && lastVW < endVW) {
|
||||
// Extend at end
|
||||
visibleWeeks.value.push(createWeek(lastVW + 1))
|
||||
added = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: assess coverage
|
||||
const firstAfter = visibleWeeks.value[0].virtualWeek
|
||||
const lastAfter = visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek
|
||||
const contiguous = (() => {
|
||||
for (let i = 0; i < visibleWeeks.value.length - 1; i++) {
|
||||
if (visibleWeeks.value[i + 1].virtualWeek !== visibleWeeks.value[i].virtualWeek + 1)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})()
|
||||
const coverageComplete =
|
||||
firstAfter <= startVW &&
|
||||
lastAfter >= endVW &&
|
||||
contiguous &&
|
||||
visibleWeeks.value.length === endVW - startVW + 1
|
||||
if (!coverageComplete) {
|
||||
// Incomplete; do not update lastScrollRange so subsequent runs keep adding
|
||||
return false
|
||||
}
|
||||
if (
|
||||
lastScrollRange.startVW === startVW &&
|
||||
lastScrollRange.endVW === endVW &&
|
||||
!added &&
|
||||
visibleWeeks.value.length
|
||||
) {
|
||||
return true
|
||||
}
|
||||
lastScrollRange = { startVW, endVW }
|
||||
return true
|
||||
}
|
||||
function rebuildVisibleWeeks(reason) {
|
||||
const buffer = 4
|
||||
@ -650,6 +718,8 @@ const handleHeaderYearChange = ({ scrollTop: st }) => {
|
||||
const maxScroll = contentHeight.value - viewportHeight.value
|
||||
const clamped = Math.max(0, Math.min(st, isFinite(maxScroll) ? maxScroll : st))
|
||||
setScrollTop(clamped, 'header-year-change')
|
||||
// Force a full rebuild so the new year range appears instantly
|
||||
scheduleDataRebuild('header-year-change')
|
||||
}
|
||||
|
||||
// Heuristic: rotate month label (180deg) only for predominantly Latin text.
|
||||
|
Loading…
x
Reference in New Issue
Block a user