Walk only the rows a scroll area can show
A channel's backlog is every message it has, and jvui walked all of them every frame whether or not they were on screen - several times over after any change, while the layout settled. A long channel was slow to answer a click. A box that would land wholly outside the clip is now folded into its column at the size it reported last time, without walking it: c/skip-box!, which glimmer-jvui asks before walking each container child. It only does that with a size it can trust - a box whose last walk reported what it reported before, with nothing inside it still settling, in a non-wrapping column, taking no share of the slack, at the width it was measured against. A row carrying :scroll-here is always walked, since a jump is to somewhere off screen. Headless, message-shaped rows: a settled frame at 3000 rows goes from about 760ms to about 21ms, and the settle-and-paint after a change from several seconds to about 110ms. What is left after a change is the reconciler's re-render, not the walk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b48e404 parent: e0d9029 modified
glimmer-backends/glimmer-jvui/src/glimmer_jvui/core.clj +18 -1 | @@ -176,6 +176,11 @@ | ||
| 176 | 176 | (and over? (not was)) (do (swap! hovering conj id) (fire! n :on-hover)) |
| 177 | 177 | (and was (not over?)) (do (swap! hovering disj id) (fire! n :on-unhover)))))) |
| 178 | 178 | |
| 179 | +(def ^:private skippable | |
| 180 | + "The tags whose jvui box is keyed by the node's own key, so `c/skip-box!` | |
| 181 | + can find what that box remembered." | |
| 182 | + #{:vbox :box :hbox :card :frame}) | |
| 183 | + | |
| 179 | 184 | (defn- emit-children! |
| 180 | 185 | "The body a container is walked with. |
| 181 | 186 | |
| @@ -187,7 +192,19 @@ | ||
| 187 | 192 | (let [here? (:scroll-here (:props @n))] |
| 188 | 193 | (fn [_id rect] |
| 189 | 194 | (when here? (w/reveal! rect)) |
| 190 | - (doseq [c (:children @n)] (emit! c))))) | |
| 195 | + (doseq [c (:children @n)] | |
| 196 | + (let [{:keys [tag props key]} @c] | |
| 197 | + ;; A container scrolled out of sight is not walked at all, only | |
| 198 | + ;; counted at the size it had — see `c/skip-box!`. A backlog is | |
| 199 | + ;; thousands of rows and a window shows twenty; walking the rest | |
| 200 | + ;; every frame was what made a long channel slow to answer a click. | |
| 201 | + ;; | |
| 202 | + ;; Never the one a jump is aiming at: it is off screen by | |
| 203 | + ;; definition, and skipping it would skip the ask to be shown. | |
| 204 | + (when-not (and (contains? skippable tag) | |
| 205 | + (not (:scroll-here props)) | |
| 206 | + (c/skip-box! key)) | |
| 207 | + (emit! c))))))) | |
| 191 | 208 | |
| 192 | 209 | (defn- emit! |
| 193 | 210 | "Render one node, and through it everything below it. |
| @@ -176,6 +176,11 @@ | |||
| 176 | (and over? (not was)) (do (swap! hovering conj id) (fire! n :on-hover)) | 176 | (and over? (not was)) (do (swap! hovering conj id) (fire! n :on-hover)) |
| 177 | (and was (not over?)) (do (swap! hovering disj id) (fire! n :on-unhover)))))) | 177 | (and was (not over?)) (do (swap! hovering disj id) (fire! n :on-unhover)))))) |
| 178 | 178 | ||
| 179 | +(def ^:private skippable | ||
| 180 | + "The tags whose jvui box is keyed by the node's own key, so `c/skip-box!` | ||
| 181 | + can find what that box remembered." | ||
| 182 | + #{:vbox :box :hbox :card :frame}) | ||
| 183 | + | ||
| 179 | (defn- emit-children! | 184 | (defn- emit-children! |
| 180 | "The body a container is walked with. | 185 | "The body a container is walked with. |
| 181 | 186 | ||
| @@ -187,7 +192,19 @@ | |||
| 187 | (let [here? (:scroll-here (:props @n))] | 192 | (let [here? (:scroll-here (:props @n))] |
| 188 | (fn [_id rect] | 193 | (fn [_id rect] |
| 189 | (when here? (w/reveal! rect)) | 194 | (when here? (w/reveal! rect)) |
| 190 | - (doseq [c (:children @n)] (emit! c))))) | 195 | + (doseq [c (:children @n)] |
| 196 | + (let [{:keys [tag props key]} @c] | ||
| 197 | + ;; A container scrolled out of sight is not walked at all, only | ||
| 198 | + ;; counted at the size it had — see `c/skip-box!`. A backlog is | ||
| 199 | + ;; thousands of rows and a window shows twenty; walking the rest | ||
| 200 | + ;; every frame was what made a long channel slow to answer a click. | ||
| 201 | + ;; | ||
| 202 | + ;; Never the one a jump is aiming at: it is off screen by | ||
| 203 | + ;; definition, and skipping it would skip the ask to be shown. | ||
| 204 | + (when-not (and (contains? skippable tag) | ||
| 205 | + (not (:scroll-here props)) | ||
| 206 | + (c/skip-box! key)) | ||
| 207 | + (emit! c))))))) | ||
| 191 | 208 | ||
| 192 | (defn- emit! | 209 | (defn- emit! |
| 193 | "Render one node, and through it everything below it. | 210 | "Render one node, and through it everything below it. |
modified
jvui/src/jvui/core.clj +62 -4 | @@ -84,7 +84,10 @@ | ||
| 84 | 84 | |
| 85 | 85 | (defn refresh! |
| 86 | 86 | "Ask for another layout pass before anything is drawn." |
| 87 | - [] (upd! assoc :refresh? true)) | |
| 87 | + ;; Counted as well as flagged: the flag says the pass as a whole moved, and | |
| 88 | + ;; the count lets a box ask whether anything moved INSIDE it — see | |
| 89 | + ;; `:stable?` in `box*`. | |
| 90 | + [] (upd! (fn [s] (-> s (assoc :refresh? true) (update :changes (fnil inc 0)))))) | |
| 88 | 91 | |
| 89 | 92 | (defn unsettle! |
| 90 | 93 | "Say that the tree changed under us, so the next frame settles before it |
| @@ -412,6 +415,10 @@ | ||
| 412 | 415 | [(max (first min-size) (first remembered)) |
| 413 | 416 | (max (second min-size) (second remembered))] |
| 414 | 417 | remembered) |
| 418 | + ;; What the parent had to give across, before this box takes its | |
| 419 | + ;; share — remembered so `skip-box!` can tell a size that still holds | |
| 420 | + ;; from one the window has since rewrapped. | |
| 421 | + parent-w (nth (:rect (top)) 2) | |
| 415 | 422 | outer (rect-for [(+ (first want) (* 2 margin)) |
| 416 | 423 | (+ (second want) (* 2 margin))] |
| 417 | 424 | expand gravity) |
| @@ -450,7 +457,15 @@ | ||
| 450 | 457 | ;; on a line of its own. |
| 451 | 458 | :wrap (boolean (and wrap (= dir :horizontal))) |
| 452 | 459 | :offset (or offset [0.0 0.0])}) |
| 453 | - (let [result (when body (body id box-rect)) | |
| 460 | + (let [;; Whether anything under this box asked for another pass while it | |
| 461 | + ;; was walked: then the sizes it is about to report are still | |
| 462 | + ;; moving, which is what `:stable?` below keeps track of. Only what | |
| 463 | + ;; is INSIDE it counts — a change earlier in the pass, in some other | |
| 464 | + ;; row, says nothing about this one, and counting it would walk the | |
| 465 | + ;; whole of a long list again after every change. | |
| 466 | + changes-before (:changes (ui) 0) | |
| 467 | + result (when body (body id box-rect)) | |
| 468 | + moved-inside? (not= changes-before (:changes (ui) 0)) | |
| 454 | 469 | f (pop-frame!)] |
| 455 | 470 | (when clip? (set-clip! outer-clip)) |
| 456 | 471 | ;; A box measures its cross axis by its widest child and its own axis by |
| @@ -472,13 +487,56 @@ | ||
| 472 | 487 | ;; scroll, so both numbers are kept. |
| 473 | 488 | reported (if (and fixed min-size) min-size computed)] |
| 474 | 489 | (when (not= reported (:min-size prev)) (refresh!)) |
| 475 | - (data! id {:min-size reported | |
| 490 | + (data! id {;; A size `skip-box!` may fold in without walking: the box | |
| 491 | + ;; reported what it reported last time, and nothing inside | |
| 492 | + ;; it was still settling. A box measured on its first walk | |
| 493 | + ;; is the case this is for — it reports nothing, and skipped | |
| 494 | + ;; at that size it would never be walked to learn better. | |
| 495 | + :stable? (and (not moved-inside?) | |
| 496 | + (= reported (:min-size prev))) | |
| 497 | + :min-size reported | |
| 476 | 498 | :content-min computed |
| 477 | 499 | :n (ctr f N) :expanders (ctr f EXPANDERS) |
| 478 | 500 | :along (ctr f ALONG) |
| 479 | - :rect box-rect :content (:rect f)}) | |
| 501 | + :rect box-rect :content (:rect f) | |
| 502 | + :margin margin :expand expand :parent-w parent-w}) | |
| 480 | 503 | result)))) |
| 481 | 504 | |
| 505 | +(defn skip-box! | |
| 506 | + "Count the box the next child keyed `key` would be, without walking it, when | |
| 507 | + it would land wholly outside the clip. Answers true when it did. | |
| 508 | + | |
| 509 | + A scrolled list is where this pays: thousands of rows, twenty on screen, | |
| 510 | + and a walk of every one of them every frame — several, after any change, | |
| 511 | + while the layout settles. A row out of sight is folded into its column at | |
| 512 | + the size it reported the last time it was walked, so the column comes out | |
| 513 | + as tall as ever and every row after it lands where it would have. | |
| 514 | + | |
| 515 | + Only where that remembered size can be trusted: a box that has been walked | |
| 516 | + before, in a column that does not wrap, taking no share of the column's | |
| 517 | + slack, and given the same width it was measured against — a window that | |
| 518 | + has been resized has rewrapped every line, and a row skipped at its old | |
| 519 | + height would put everything under it in the wrong place. What it cannot | |
| 520 | + see is a box whose contents changed while it was out of sight; that one | |
| 521 | + comes back at its new size the first walk it is on screen." | |
| 522 | + [key] | |
| 523 | + (let [{:keys [dir rect offset wrap] :as f} (top) | |
| 524 | + id (child-id (:id f) (ctr f N) key) | |
| 525 | + {:keys [min-size margin expand parent-w stable?]} (data id)] | |
| 526 | + (when (and stable? min-size parent-w | |
| 527 | + (= dir :vertical) (not wrap) | |
| 528 | + (not (and (not= expand :cross) (expands? expand :vertical))) | |
| 529 | + (== (double parent-w) (double (nth rect 2)))) | |
| 530 | + (let [[_ ry _ _] rect | |
| 531 | + [_ oy] (or offset [0.0 0.0]) | |
| 532 | + y (+ ry oy (ctr f CURSOR)) | |
| 533 | + h (+ (double (second min-size)) (* 2.0 (double margin))) | |
| 534 | + [_ cy _ ch] (clip-rect)] | |
| 535 | + ;; Strictly outside: a box that touches the edge is still walked. | |
| 536 | + (when (or (< (+ y h) cy) (> y (+ cy ch))) | |
| 537 | + (tally! h h (+ (double (first min-size)) (* 2.0 (double margin))) false) | |
| 538 | + true))))) | |
| 539 | + | |
| 482 | 540 | (defn draw-pass? |
| 483 | 541 | "True on the walk that paints. Unlike `drawing?` this does not also ask for |
| 484 | 542 | a painter, so a headless walk answers it — which is what lets an event |
| @@ -84,7 +84,10 @@ | |||
| 84 | 84 | ||
| 85 | (defn refresh! | 85 | (defn refresh! |
| 86 | "Ask for another layout pass before anything is drawn." | 86 | "Ask for another layout pass before anything is drawn." |
| 87 | - [] (upd! assoc :refresh? true)) | 87 | + ;; Counted as well as flagged: the flag says the pass as a whole moved, and |
| 88 | + ;; the count lets a box ask whether anything moved INSIDE it — see | ||
| 89 | + ;; `:stable?` in `box*`. | ||
| 90 | + [] (upd! (fn [s] (-> s (assoc :refresh? true) (update :changes (fnil inc 0)))))) | ||
| 88 | 91 | ||
| 89 | (defn unsettle! | 92 | (defn unsettle! |
| 90 | "Say that the tree changed under us, so the next frame settles before it | 93 | "Say that the tree changed under us, so the next frame settles before it |
| @@ -412,6 +415,10 @@ | |||
| 412 | [(max (first min-size) (first remembered)) | 415 | [(max (first min-size) (first remembered)) |
| 413 | (max (second min-size) (second remembered))] | 416 | (max (second min-size) (second remembered))] |
| 414 | remembered) | 417 | remembered) |
| 418 | + ;; What the parent had to give across, before this box takes its | ||
| 419 | + ;; share — remembered so `skip-box!` can tell a size that still holds | ||
| 420 | + ;; from one the window has since rewrapped. | ||
| 421 | + parent-w (nth (:rect (top)) 2) | ||
| 415 | outer (rect-for [(+ (first want) (* 2 margin)) | 422 | outer (rect-for [(+ (first want) (* 2 margin)) |
| 416 | (+ (second want) (* 2 margin))] | 423 | (+ (second want) (* 2 margin))] |
| 417 | expand gravity) | 424 | expand gravity) |
| @@ -450,7 +457,15 @@ | |||
| 450 | ;; on a line of its own. | 457 | ;; on a line of its own. |
| 451 | :wrap (boolean (and wrap (= dir :horizontal))) | 458 | :wrap (boolean (and wrap (= dir :horizontal))) |
| 452 | :offset (or offset [0.0 0.0])}) | 459 | :offset (or offset [0.0 0.0])}) |
| 453 | - (let [result (when body (body id box-rect)) | 460 | + (let [;; Whether anything under this box asked for another pass while it |
| 461 | + ;; was walked: then the sizes it is about to report are still | ||
| 462 | + ;; moving, which is what `:stable?` below keeps track of. Only what | ||
| 463 | + ;; is INSIDE it counts — a change earlier in the pass, in some other | ||
| 464 | + ;; row, says nothing about this one, and counting it would walk the | ||
| 465 | + ;; whole of a long list again after every change. | ||
| 466 | + changes-before (:changes (ui) 0) | ||
| 467 | + result (when body (body id box-rect)) | ||
| 468 | + moved-inside? (not= changes-before (:changes (ui) 0)) | ||
| 454 | f (pop-frame!)] | 469 | f (pop-frame!)] |
| 455 | (when clip? (set-clip! outer-clip)) | 470 | (when clip? (set-clip! outer-clip)) |
| 456 | ;; A box measures its cross axis by its widest child and its own axis by | 471 | ;; A box measures its cross axis by its widest child and its own axis by |
| @@ -472,13 +487,56 @@ | |||
| 472 | ;; scroll, so both numbers are kept. | 487 | ;; scroll, so both numbers are kept. |
| 473 | reported (if (and fixed min-size) min-size computed)] | 488 | reported (if (and fixed min-size) min-size computed)] |
| 474 | (when (not= reported (:min-size prev)) (refresh!)) | 489 | (when (not= reported (:min-size prev)) (refresh!)) |
| 475 | - (data! id {:min-size reported | 490 | + (data! id {;; A size `skip-box!` may fold in without walking: the box |
| 491 | + ;; reported what it reported last time, and nothing inside | ||
| 492 | + ;; it was still settling. A box measured on its first walk | ||
| 493 | + ;; is the case this is for — it reports nothing, and skipped | ||
| 494 | + ;; at that size it would never be walked to learn better. | ||
| 495 | + :stable? (and (not moved-inside?) | ||
| 496 | + (= reported (:min-size prev))) | ||
| 497 | + :min-size reported | ||
| 476 | :content-min computed | 498 | :content-min computed |
| 477 | :n (ctr f N) :expanders (ctr f EXPANDERS) | 499 | :n (ctr f N) :expanders (ctr f EXPANDERS) |
| 478 | :along (ctr f ALONG) | 500 | :along (ctr f ALONG) |
| 479 | - :rect box-rect :content (:rect f)}) | 501 | + :rect box-rect :content (:rect f) |
| 502 | + :margin margin :expand expand :parent-w parent-w}) | ||
| 480 | result)))) | 503 | result)))) |
| 481 | 504 | ||
| 505 | +(defn skip-box! | ||
| 506 | + "Count the box the next child keyed `key` would be, without walking it, when | ||
| 507 | + it would land wholly outside the clip. Answers true when it did. | ||
| 508 | + | ||
| 509 | + A scrolled list is where this pays: thousands of rows, twenty on screen, | ||
| 510 | + and a walk of every one of them every frame — several, after any change, | ||
| 511 | + while the layout settles. A row out of sight is folded into its column at | ||
| 512 | + the size it reported the last time it was walked, so the column comes out | ||
| 513 | + as tall as ever and every row after it lands where it would have. | ||
| 514 | + | ||
| 515 | + Only where that remembered size can be trusted: a box that has been walked | ||
| 516 | + before, in a column that does not wrap, taking no share of the column's | ||
| 517 | + slack, and given the same width it was measured against — a window that | ||
| 518 | + has been resized has rewrapped every line, and a row skipped at its old | ||
| 519 | + height would put everything under it in the wrong place. What it cannot | ||
| 520 | + see is a box whose contents changed while it was out of sight; that one | ||
| 521 | + comes back at its new size the first walk it is on screen." | ||
| 522 | + [key] | ||
| 523 | + (let [{:keys [dir rect offset wrap] :as f} (top) | ||
| 524 | + id (child-id (:id f) (ctr f N) key) | ||
| 525 | + {:keys [min-size margin expand parent-w stable?]} (data id)] | ||
| 526 | + (when (and stable? min-size parent-w | ||
| 527 | + (= dir :vertical) (not wrap) | ||
| 528 | + (not (and (not= expand :cross) (expands? expand :vertical))) | ||
| 529 | + (== (double parent-w) (double (nth rect 2)))) | ||
| 530 | + (let [[_ ry _ _] rect | ||
| 531 | + [_ oy] (or offset [0.0 0.0]) | ||
| 532 | + y (+ ry oy (ctr f CURSOR)) | ||
| 533 | + h (+ (double (second min-size)) (* 2.0 (double margin))) | ||
| 534 | + [_ cy _ ch] (clip-rect)] | ||
| 535 | + ;; Strictly outside: a box that touches the edge is still walked. | ||
| 536 | + (when (or (< (+ y h) cy) (> y (+ cy ch))) | ||
| 537 | + (tally! h h (+ (double (first min-size)) (* 2.0 (double margin))) false) | ||
| 538 | + true))))) | ||
| 539 | + | ||
| 482 | (defn draw-pass? | 540 | (defn draw-pass? |
| 483 | "True on the walk that paints. Unlike `drawing?` this does not also ask for | 541 | "True on the walk that paints. Unlike `drawing?` this does not also ask for |
| 484 | a painter, so a headless walk answers it — which is what lets an event | 542 | a painter, so a headless walk answers it — which is what lets an event |
modified
jvui/test/jvui/tests.clj +45 -0 | @@ -413,6 +413,50 @@ | ||
| 413 | 413 | (check! (> (w/scroll-offset "reveal") off) |
| 414 | 414 | "and the wheel moves it again once nothing asks")))) |
| 415 | 415 | |
| 416 | +(defn- check-offscreen-rows-are-skipped! [] | |
| 417 | + ;; A channel's backlog is thousands of rows in a viewport that shows twenty. | |
| 418 | + ;; Walking all of them every frame is what made a long channel slow, so a | |
| 419 | + ;; row out of sight is counted at its remembered size and not walked — and | |
| 420 | + ;; the list must come out exactly as tall, with every row that comes back | |
| 421 | + ;; into view exactly where it would have been. | |
| 422 | + (let [cx (ctx) walked (atom #{}) ys (atom {}) | |
| 423 | + render (fn [] | |
| 424 | + (w/scroll* {:height 100 :scroll-key "cull"} | |
| 425 | + (fn [_ _] | |
| 426 | + ;; As glimmer-jvui walks a container's children: ask | |
| 427 | + ;; first, and walk only what was not skipped. | |
| 428 | + (doseq [i (range 200)] | |
| 429 | + (when-not (c/skip-box! i) | |
| 430 | + (c/box* {:dir :vertical :key i} | |
| 431 | + (fn [_ _] | |
| 432 | + (swap! walked conj i) | |
| 433 | + (swap! ys assoc i | |
| 434 | + (second (w/label (str "row " i) | |
| 435 | + {:expand :none})))))))))) | |
| 436 | + content (fn [] | |
| 437 | + (->> (vals (:data @cx)) | |
| 438 | + (filter #(= [0.0 100.0] (:min-size %))) | |
| 439 | + first :content-min second))] | |
| 440 | + (dotimes [_ 3] (frame! cx render)) | |
| 441 | + (let [full (content) | |
| 442 | + y30 (get @ys 30)] | |
| 443 | + (reset! walked #{}) | |
| 444 | + (frame! cx render) | |
| 445 | + (check! (< (count @walked) 20) | |
| 446 | + (str "only the rows near the viewport are walked: " (count @walked))) | |
| 447 | + (check! (close? full (content)) | |
| 448 | + (str "and the list is as tall as it was: " full " -> " (content))) | |
| 449 | + (frame! cx render [{:kind :motion :x 50 :y 50} | |
| 450 | + {:kind :wheel :dx 0.0 :dy -15.0}]) | |
| 451 | + (reset! walked #{}) | |
| 452 | + (frame! cx render [{:kind :motion :x 50 :y 50}]) | |
| 453 | + (let [off (w/scroll-offset "cull")] | |
| 454 | + (check! (contains? @walked 30) | |
| 455 | + (str "a row scrolled into view is walked again: offset=" off)) | |
| 456 | + (check! (close? (get @ys 30) (- y30 off)) | |
| 457 | + (str "at the place it would have had: " (get @ys 30) | |
| 458 | + " vs " (- y30 off))))))) | |
| 459 | + | |
| 416 | 460 | ;; --- theme ------------------------------------------------------------------- |
| 417 | 461 | |
| 418 | 462 | (defn- check-theme! [] |
| @@ -535,6 +579,7 @@ | ||
| 535 | 579 | ["tab moves focus" check-tab-moves-focus!] |
| 536 | 580 | ["a viewport scrolls" check-scroll!] |
| 537 | 581 | ["a row asks to be seen" check-reveal!] |
| 582 | + ["rows out of sight are skipped" check-offscreen-rows-are-skipped!] | |
| 538 | 583 | ["the theme mixes" check-theme!]]) |
| 539 | 584 | |
| 540 | 585 | (defn -main [& _] |
| @@ -413,6 +413,50 @@ | |||
| 413 | (check! (> (w/scroll-offset "reveal") off) | 413 | (check! (> (w/scroll-offset "reveal") off) |
| 414 | "and the wheel moves it again once nothing asks")))) | 414 | "and the wheel moves it again once nothing asks")))) |
| 415 | 415 | ||
| 416 | +(defn- check-offscreen-rows-are-skipped! [] | ||
| 417 | + ;; A channel's backlog is thousands of rows in a viewport that shows twenty. | ||
| 418 | + ;; Walking all of them every frame is what made a long channel slow, so a | ||
| 419 | + ;; row out of sight is counted at its remembered size and not walked — and | ||
| 420 | + ;; the list must come out exactly as tall, with every row that comes back | ||
| 421 | + ;; into view exactly where it would have been. | ||
| 422 | + (let [cx (ctx) walked (atom #{}) ys (atom {}) | ||
| 423 | + render (fn [] | ||
| 424 | + (w/scroll* {:height 100 :scroll-key "cull"} | ||
| 425 | + (fn [_ _] | ||
| 426 | + ;; As glimmer-jvui walks a container's children: ask | ||
| 427 | + ;; first, and walk only what was not skipped. | ||
| 428 | + (doseq [i (range 200)] | ||
| 429 | + (when-not (c/skip-box! i) | ||
| 430 | + (c/box* {:dir :vertical :key i} | ||
| 431 | + (fn [_ _] | ||
| 432 | + (swap! walked conj i) | ||
| 433 | + (swap! ys assoc i | ||
| 434 | + (second (w/label (str "row " i) | ||
| 435 | + {:expand :none})))))))))) | ||
| 436 | + content (fn [] | ||
| 437 | + (->> (vals (:data @cx)) | ||
| 438 | + (filter #(= [0.0 100.0] (:min-size %))) | ||
| 439 | + first :content-min second))] | ||
| 440 | + (dotimes [_ 3] (frame! cx render)) | ||
| 441 | + (let [full (content) | ||
| 442 | + y30 (get @ys 30)] | ||
| 443 | + (reset! walked #{}) | ||
| 444 | + (frame! cx render) | ||
| 445 | + (check! (< (count @walked) 20) | ||
| 446 | + (str "only the rows near the viewport are walked: " (count @walked))) | ||
| 447 | + (check! (close? full (content)) | ||
| 448 | + (str "and the list is as tall as it was: " full " -> " (content))) | ||
| 449 | + (frame! cx render [{:kind :motion :x 50 :y 50} | ||
| 450 | + {:kind :wheel :dx 0.0 :dy -15.0}]) | ||
| 451 | + (reset! walked #{}) | ||
| 452 | + (frame! cx render [{:kind :motion :x 50 :y 50}]) | ||
| 453 | + (let [off (w/scroll-offset "cull")] | ||
| 454 | + (check! (contains? @walked 30) | ||
| 455 | + (str "a row scrolled into view is walked again: offset=" off)) | ||
| 456 | + (check! (close? (get @ys 30) (- y30 off)) | ||
| 457 | + (str "at the place it would have had: " (get @ys 30) | ||
| 458 | + " vs " (- y30 off))))))) | ||
| 459 | + | ||
| 416 | ;; --- theme ------------------------------------------------------------------- | 460 | ;; --- theme ------------------------------------------------------------------- |
| 417 | 461 | ||
| 418 | (defn- check-theme! [] | 462 | (defn- check-theme! [] |
| @@ -535,6 +579,7 @@ | |||
| 535 | ["tab moves focus" check-tab-moves-focus!] | 579 | ["tab moves focus" check-tab-moves-focus!] |
| 536 | ["a viewport scrolls" check-scroll!] | 580 | ["a viewport scrolls" check-scroll!] |
| 537 | ["a row asks to be seen" check-reveal!] | 581 | ["a row asks to be seen" check-reveal!] |
| 582 | + ["rows out of sight are skipped" check-offscreen-rows-are-skipped!] | ||
| 538 | ["the theme mixes" check-theme!]]) | 583 | ["the theme mixes" check-theme!]]) |
| 539 | 584 | ||
| 540 | (defn -main [& _] | 585 | (defn -main [& _] |