From 967ca97e84838f2bc68f0eff347c868e84f18d5e Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Jun 2026 10:04:25 -0400 Subject: [PATCH] feat: scroll vertically to the clip's track when jumping via link/jump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit goto! with follow? already recentred horizontally on the playhead. Now it also scrolls the tracks container vertically so the clip under the landing frame is brought into view — for link chips, annotation jump buttons, and script highlights. No-op when the tracks already fit. Accounts for the pinned annotation-lane header height in the scroll math. Co-Authored-By: Claude Opus 4.8 --- tl/src/tl/views.cljs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tl/src/tl/views.cljs b/tl/src/tl/views.cljs index 6e990d3..7efd112 100644 --- a/tl/src/tl/views.cljs +++ b/tl/src/tl/views.cljs @@ -12,6 +12,7 @@ (defonce video-el (atom nil)) (defonce raf (atom nil)) (defonce scroll-el (atom nil)) +(defonce body-scroll-el (atom nil)) ; the vertical (tracks) scroll container (defonce playhead-el (atom nil)) ; the single full-height playhead overlay (defonce play (atom nil)) ; {:ctx :segs :fps :idx} while playing, else nil (defonce active-insert! (atom nil)) ; the live content-editor's (insert! link) fn @@ -174,6 +175,23 @@ (rf/reg-fx :player/pause (fn [_] (when-let [v @video-el] (.pause v)))) (rf/reg-fx :player/seek (fn [secs] (when (and @video-el secs) (set! (.-currentTime @video-el) secs)))) +(defn- scroll-to-seg-track! + "Scroll the tracks vertically so the clip under local frame `local` is visible. + No-op when there's no vertical overflow. The annotation lanes are a pinned + header of height `lane-h`, so the clip rows live below that in scroll space." + [segs local] + (when-let [el @body-scroll-el] + (let [tracks @(rf/subscribe [::subs/tracks]) + row-h @(rf/subscribe [::subs/row-h]) + anns @(rf/subscribe [::subs/annotations]) + lane-h (max 18 (* 18 (count anns))) + track (:track (nth segs (seg-at segs local) nil)) + i (first (keep-indexed (fn [idx t] (when (= (:id t) track) idx)) tracks))] + (when i + (let [target (+ lane-h (* i row-h) (/ row-h 2))] + (.scrollTo el #js {:top (max 0 (- target (/ (.-clientHeight el) 2))) + :behavior "smooth"})))))) + (defn- goto! "Move the playhead to local frame `local` and seek the video to match." ([local] (goto! local false)) @@ -186,7 +204,8 @@ (seek-video! fps (scene/local->source segs local)) (when @play (swap! play assoc :idx (seg-at segs local))) ; keep playback in sync (when follow? - (r/after-render #(follow! fps zoom local)))))) + (r/after-render #(do (follow! fps zoom local) + (scroll-to-seg-track! segs local))))))) (defn video-monitor [] [:video {:src @(rf/subscribe [::subs/clip-url]) :controls true :preload "auto" @@ -322,7 +341,8 @@ ^{:key (str "lbl-" (:id a))} [:div.ann-bar-label {:style {:top (* i 18) :left (+ 4 (px lo fps zoom))}} (:name a)])]]] - [:div.timeline-body-scroll {:style {:padding-top lane-h}} + [:div.timeline-body-scroll {:ref (fn [n] (reset! body-scroll-el n)) + :style {:padding-top lane-h}} [:div.gutter-tracks (for [t tracks] ^{:key (:id t)}