nandi/frqpublic Fork 0
1528bb0
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Land a Flutter jump on the line it aimed at

`message-row` has marked its row `:scroll-here` since the overview grew a
"go to message"; the Flutter backend never read the mark, so a jump opened
the room and left the view at the end of it.

The row is not a widget that can be found afterwards — the tree is rebuilt
from the top whenever a cell fires — but the build that paints it has its
BuildContext, so it records it, and the scroll around it reads it back after
the frame and ensureVisibles onto it. A third of the way down rather than at
the top edge: a line arrived at reads as an answer to the ones above it.

The end-ward jump stands down when a jump is pending, because `jump-tick`
renames the viewport for both gestures and the end is the wrong one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-12T07:53:05-07:00 Browse files
1528bb0 parent: cc17757
modified flutter/src/frq/hiccup.cljd +50 -2
@@ -189,6 +189,17 @@
189189 ;; showing it, and it has to survive the rebuild that replaces one.
190190 (defonce ^:private scroll-marks (atom {}))
191191
192+;; Where a jump is aiming, once the frame that paints it has a context to
193+;; aim at.
194+;;
195+;; `message-row` marks one row `:scroll-here` while a jump is on it, and a
196+;; row is not a widget this backend can find afterwards the tree is rebuilt
197+;; from the top every time a cell fires, so there is no handle on it that
198+;; outlives the build. The build itself has one: the marked row's own
199+;; BuildContext, recorded here as it is built and read back by the scroll
200+;; around it after the frame.
201+(defonce ^:private jump-context (atom nil))
202+
192203 (defn- end-ward!
193204 "Put `k` at the end after this frame, when it should be.
194205
@@ -221,7 +232,12 @@
221232 (.addPostFrameCallback
222233 (.-instance m/WidgetsBinding)
223234 (fn [_]
224- (when (.-hasClients ctrl)
235+ ;; Not when a jump is pending: being taken to a line and being taken
236+ ;; to the end are the same gesture as far as `jump-tick` is
237+ ;; concerned — the screen renames the viewport either way — and the
238+ ;; end is the wrong one of the two. By now the marked row has been
239+ ;; built, so this knows which gesture it was.
240+ (when (and (nil? @jump-context) (.-hasClients ctrl))
225241 (.jumpTo ctrl (.-maxScrollExtent (.-position ctrl)))))))))
226242
227243 (defonce ^:private scroll-watch
@@ -264,6 +280,29 @@
264280 (when f (f said)))))
265281 nil))))))
266282
283+(defn- here-ward!
284+ "Bring the row a jump is aiming at into view, when one was built.
285+
286+ After the frame, for the reason `end-ward!` is: the row is built inside
287+ this scroll and so does not exist yet when the scroll is, and a context
288+ with no render object behind it cannot be scrolled to.
289+
290+ Aimed a third of the way down rather than at the top edge, because a line
291+ arrived at reads as an answer to the lines above it, and pinned to the top
292+ it has none of them."
293+ []
294+ (.addPostFrameCallback
295+ (.-instance m/WidgetsBinding)
296+ (fn [_]
297+ (when-let [^m/BuildContext c @jump-context]
298+ ;; Cleared here rather than when the jump does: `jump-to` comes off a
299+ ;; frame or two later and this has to have happened by then, and a
300+ ;; context kept past the frame that made it is a context to a widget
301+ ;; that may be gone.
302+ (reset! jump-context nil)
303+ (when (.-mounted c)
304+ (m/Scrollable.ensureVisible c .alignment 0.3))))))
305+
267306 (defn- fills-column?
268307 "Whether a node takes the height its column has left over.
269308
@@ -417,6 +456,10 @@
417456 (f/widget
418457 :context ctx
419458 (let [p (props node)
459+ ;; The row a jump is aiming at, saying so as it is built. Here and
460+ ;; not in an arm of the `case` below, because the mark is a property
461+ ;; any tag may carry — it happens to be a `:vbox` today.
462+ _ (when (:scroll-here p) (reset! jump-context ctx))
420463 kids (children (body node))
421464 ;; One column builder for every container, so the rule about what
422465 ;; fills is applied in one place: children that take what is left are
@@ -785,8 +828,13 @@
785828 ;; "ScrollController attached to multiple scroll views", which took
786829 ;; the whole screen.
787830 :managed [ctrl (m/ScrollController)]
831+ ;; `here-ward!` after `end-ward!`, so the callback that aims at a
832+ ;; line is queued behind the one that aims at the end: the end's
833+ ;; runs first, sees the pending jump and stands down, and then this
834+ ;; one clears it.
788835 :let [_ (watch-end! k ctrl on-change)
789- _ (end-ward! k ctrl token)]
836+ _ (end-ward! k ctrl token)
837+ _ (here-ward!)]
790838 (m/SingleChildScrollView
791839 .controller ctrl
792840 .child (col (dbl (:spacing p) 0.0) (body node)))))
@@ -189,6 +189,17 @@
189 ;; showing it, and it has to survive the rebuild that replaces one.189 ;; showing it, and it has to survive the rebuild that replaces one.
190 (defonce ^:private scroll-marks (atom {}))190 (defonce ^:private scroll-marks (atom {}))
191 191
192+;; Where a jump is aiming, once the frame that paints it has a context to
193+;; aim at.
194+;;
195+;; `message-row` marks one row `:scroll-here` while a jump is on it, and a
196+;; row is not a widget this backend can find afterwards the tree is rebuilt
197+;; from the top every time a cell fires, so there is no handle on it that
198+;; outlives the build. The build itself has one: the marked row's own
199+;; BuildContext, recorded here as it is built and read back by the scroll
200+;; around it after the frame.
201+(defonce ^:private jump-context (atom nil))
202+
192 (defn- end-ward!203 (defn- end-ward!
193 "Put `k` at the end after this frame, when it should be.204 "Put `k` at the end after this frame, when it should be.
194 205
@@ -221,7 +232,12 @@
221 (.addPostFrameCallback232 (.addPostFrameCallback
222 (.-instance m/WidgetsBinding)233 (.-instance m/WidgetsBinding)
223 (fn [_]234 (fn [_]
224- (when (.-hasClients ctrl)235+ ;; Not when a jump is pending: being taken to a line and being taken
236+ ;; to the end are the same gesture as far as `jump-tick` is
237+ ;; concerned — the screen renames the viewport either way — and the
238+ ;; end is the wrong one of the two. By now the marked row has been
239+ ;; built, so this knows which gesture it was.
240+ (when (and (nil? @jump-context) (.-hasClients ctrl))
225 (.jumpTo ctrl (.-maxScrollExtent (.-position ctrl)))))))))241 (.jumpTo ctrl (.-maxScrollExtent (.-position ctrl)))))))))
226 242
227 (defonce ^:private scroll-watch243 (defonce ^:private scroll-watch
@@ -264,6 +280,29 @@
264 (when f (f said)))))280 (when f (f said)))))
265 nil))))))281 nil))))))
266 282
283+(defn- here-ward!
284+ "Bring the row a jump is aiming at into view, when one was built.
285+
286+ After the frame, for the reason `end-ward!` is: the row is built inside
287+ this scroll and so does not exist yet when the scroll is, and a context
288+ with no render object behind it cannot be scrolled to.
289+
290+ Aimed a third of the way down rather than at the top edge, because a line
291+ arrived at reads as an answer to the lines above it, and pinned to the top
292+ it has none of them."
293+ []
294+ (.addPostFrameCallback
295+ (.-instance m/WidgetsBinding)
296+ (fn [_]
297+ (when-let [^m/BuildContext c @jump-context]
298+ ;; Cleared here rather than when the jump does: `jump-to` comes off a
299+ ;; frame or two later and this has to have happened by then, and a
300+ ;; context kept past the frame that made it is a context to a widget
301+ ;; that may be gone.
302+ (reset! jump-context nil)
303+ (when (.-mounted c)
304+ (m/Scrollable.ensureVisible c .alignment 0.3))))))
305+
267 (defn- fills-column?306 (defn- fills-column?
268 "Whether a node takes the height its column has left over.307 "Whether a node takes the height its column has left over.
269 308
@@ -417,6 +456,10 @@
417 (f/widget456 (f/widget
418 :context ctx457 :context ctx
419 (let [p (props node)458 (let [p (props node)
459+ ;; The row a jump is aiming at, saying so as it is built. Here and
460+ ;; not in an arm of the `case` below, because the mark is a property
461+ ;; any tag may carry — it happens to be a `:vbox` today.
462+ _ (when (:scroll-here p) (reset! jump-context ctx))
420 kids (children (body node))463 kids (children (body node))
421 ;; One column builder for every container, so the rule about what464 ;; One column builder for every container, so the rule about what
422 ;; fills is applied in one place: children that take what is left are465 ;; fills is applied in one place: children that take what is left are
@@ -785,8 +828,13 @@
785 ;; "ScrollController attached to multiple scroll views", which took828 ;; "ScrollController attached to multiple scroll views", which took
786 ;; the whole screen.829 ;; the whole screen.
787 :managed [ctrl (m/ScrollController)]830 :managed [ctrl (m/ScrollController)]
831+ ;; `here-ward!` after `end-ward!`, so the callback that aims at a
832+ ;; line is queued behind the one that aims at the end: the end's
833+ ;; runs first, sees the pending jump and stands down, and then this
834+ ;; one clears it.
788 :let [_ (watch-end! k ctrl on-change)835 :let [_ (watch-end! k ctrl on-change)
789- _ (end-ward! k ctrl token)]836+ _ (end-ward! k ctrl token)
837+ _ (here-ward!)]
790 (m/SingleChildScrollView838 (m/SingleChildScrollView
791 .controller ctrl839 .controller ctrl
792 .child (col (dbl (:spacing p) 0.0) (body node)))))840 .child (col (dbl (:spacing p) 0.0) (body node)))))