[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

✨ make scroll indicator draggable in demo

Extract thumb geometry into reusable helper so event handling can
hit-test the scrollbar. Left-click the thumb to drag, click the
track to jump (top-aligned). Drag cancels on resize or mouseup.

Taras Mankovski (Apr 11, 2026, 7:26 AM EDT) 80324f41 cbcb025a

+50 -11
+50 -11
demo/scroll.ts
··· 69 69 return " ".repeat(Math.max(0, pad)) + num + " │"; 70 70 } 71 71 72 + function thumbGeometry( 73 + v: Virtualizer, 74 + viewportHeight: number, 75 + ): { thumbPos: number; thumbSize: number } { 76 + let vp = v.resolveViewport(); 77 + let total = vp.totalEstimatedVisualRows; 78 + let current = vp.currentEstimatedVisualRow; 79 + let thumbSize = total > viewportHeight 80 + ? Math.max(1, Math.round(viewportHeight * viewportHeight / total)) 81 + : viewportHeight; 82 + let thumbPos = total > 1 83 + ? Math.round(current / Math.max(total - 1, 1) * (viewportHeight - thumbSize)) 84 + : 0; 85 + return { thumbPos, thumbSize }; 86 + } 87 + 72 88 function buildOps( 73 89 v: Virtualizer, 74 90 columns: number, ··· 79 95 let gw = gutterWidth(v); 80 96 let viewportHeight = rows - 1; 81 97 82 - // Scrollbar geometry 83 - let total = vp.totalEstimatedVisualRows; 84 - let current = vp.currentEstimatedVisualRow; 85 - let thumbSize = total > viewportHeight 86 - ? Math.max(1, Math.round(viewportHeight * viewportHeight / total)) 87 - : viewportHeight; 88 - let thumbPos = total > 1 89 - ? Math.round(current / Math.max(total - 1, 1) * (viewportHeight - thumbSize)) 90 - : 0; 98 + let { thumbPos, thumbSize } = thumbGeometry(v, viewportHeight); 91 99 92 100 // Root 93 101 ops.push(open("root", { ··· 166 174 // Status bar 167 175 let bottomLabel = vp.isAtBottom ? " BOTTOM" : ""; 168 176 let status = 169 - ` lines: ${v.lineCount} row ${current}/${total}${bottomLabel} j/k:scroll g/G:top/bottom q:quit`; 177 + ` lines: ${v.lineCount} row ${vp.currentEstimatedVisualRow}/${vp.totalEstimatedVisualRows}${bottomLabel} j/k:scroll g/G:top/bottom q:quit`; 170 178 ops.push( 171 179 open("status", { 172 180 layout: { ··· 240 248 event: InputEvent, 241 249 v: Virtualizer, 242 250 viewportRows: number, 251 + columns: number, 252 + drag: { active: boolean; offset: number }, 243 253 ): boolean { 244 254 if (event.type === "keydown" && event.ctrl && event.key === "c") return true; 245 255 if (event.type === "keydown" && event.key === "q") return true; ··· 281 291 v.scrollBy(event.direction === "down" ? 3 : -3); 282 292 } 283 293 294 + if ( 295 + event.type === "mousedown" && 296 + event.button === "left" && 297 + event.x === columns - 1 && 298 + event.y < viewportRows 299 + ) { 300 + let { thumbPos, thumbSize } = thumbGeometry(v, viewportRows); 301 + if (event.y >= thumbPos && event.y < thumbPos + thumbSize) { 302 + drag.active = true; 303 + drag.offset = event.y - thumbPos; 304 + } else { 305 + let fraction = event.y / Math.max(viewportRows - thumbSize, 1); 306 + v.scrollToFraction(Math.min(Math.max(fraction, 0), 1)); 307 + } 308 + } 309 + 310 + if (event.type === "mousemove" && drag.active) { 311 + let { thumbSize } = thumbGeometry(v, viewportRows); 312 + let fraction = (event.y - drag.offset) / Math.max(viewportRows - thumbSize, 1); 313 + v.scrollToFraction(Math.min(Math.max(fraction, 0), 1)); 314 + } 315 + 316 + if (event.type === "mouseup") { 317 + drag.active = false; 318 + } 319 + 284 320 return false; 285 321 } 286 322 ··· 332 368 Deno.stdout.writeSync(tty.revert); 333 369 }); 334 370 371 + let drag = { active: false, offset: 0 }; 372 + 335 373 // Initial render 336 374 Deno.stdout.writeSync(term.render(buildOps(v, columns, rows)).output); 337 375 338 376 for (let event of yield* each(input)) { 339 - let quit = handleEvent(event, v, viewportRows); 377 + let quit = handleEvent(event, v, viewportRows, columns, drag); 340 378 if (quit) break; 341 379 342 380 if (event.type === "resize") { 381 + drag.active = false; 343 382 columns = event.width; 344 383 rows = event.height; 345 384 viewportRows = rows - 1;