( node: DOMElement, output: Output, offsetX: number, offsetY: number, hasRemovedChild: boolean, prevScreen: Screen | undefined, scrollTopY: number, scrollBottomY: number, inheritedBackgroundColor: Color | undefined, // When true (DECSTBM fast path), culled children keep their cache — // the blit+shift put stable rows in next.screen so stale cache is // never read. Avoids walking O(total_children * subtree_depth) per frame. preserveCulledCache = false, )
| 1375 | // viewport later they don't emit a stale clear for a position now occupied |
| 1376 | // by a sibling. |
| 1377 | function renderScrolledChildren( |
| 1378 | node: DOMElement, |
| 1379 | output: Output, |
| 1380 | offsetX: number, |
| 1381 | offsetY: number, |
| 1382 | hasRemovedChild: boolean, |
| 1383 | prevScreen: Screen | undefined, |
| 1384 | scrollTopY: number, |
| 1385 | scrollBottomY: number, |
| 1386 | inheritedBackgroundColor: Color | undefined, |
| 1387 | // When true (DECSTBM fast path), culled children keep their cache — |
| 1388 | // the blit+shift put stable rows in next.screen so stale cache is |
| 1389 | // never read. Avoids walking O(total_children * subtree_depth) per frame. |
| 1390 | preserveCulledCache = false, |
| 1391 | ): void { |
| 1392 | let seenDirtyChild = false |
| 1393 | // Track cumulative height shift of dirty children iterated so far. When |
| 1394 | // zero, a clean child's yogaTop is unchanged (no sibling above it grew), |
| 1395 | // so cached.top is fresh and the cull check skips yoga. Bottom-append |
| 1396 | // has the dirty child last → all prior clean children hit cache → |
| 1397 | // O(dirty) not O(mounted). Middle-growth leaves shift non-zero after |
| 1398 | // the dirty child → subsequent children yoga-read (needed for correct |
| 1399 | // culling since their yogaTop shifted). |
| 1400 | let cumHeightShift = 0 |
| 1401 | for (const childNode of node.childNodes) { |
| 1402 | const childElem = childNode as DOMElement |
| 1403 | const cy = childElem.yogaNode |
| 1404 | if (cy) { |
| 1405 | const cached = nodeCache.get(childElem) |
| 1406 | let top: number |
| 1407 | let height: number |
| 1408 | if ( |
| 1409 | cached?.top !== undefined && |
| 1410 | !childElem.dirty && |
| 1411 | cumHeightShift === 0 |
| 1412 | ) { |
| 1413 | top = cached.top |
| 1414 | height = cached.height |
| 1415 | } else { |
| 1416 | top = cy.getComputedTop() |
| 1417 | height = cy.getComputedHeight() |
| 1418 | if (childElem.dirty) { |
| 1419 | cumHeightShift += height - (cached ? cached.height : 0) |
| 1420 | } |
| 1421 | // Refresh cached top so next frame's cumShift===0 path stays |
| 1422 | // correct. For culled children with preserveCulledCache=true this |
| 1423 | // is the ONLY refresh point — without it, a middle-growth frame |
| 1424 | // leaves stale tops that misfire next frame. |
| 1425 | if (cached) cached.top = top |
| 1426 | } |
| 1427 | const bottom = top + height |
| 1428 | if (bottom <= scrollTopY || top >= scrollBottomY) { |
| 1429 | // Culled — outside visible window. Drop stale cache entries from |
| 1430 | // the subtree so when this child re-enters it doesn't fire clears |
| 1431 | // at positions now occupied by siblings. The viewport-clear on |
| 1432 | // scroll-change handles the visible-area repaint. |
| 1433 | if (!preserveCulledCache) dropSubtreeCache(childElem) |
| 1434 | continue |
no test coverage detected