feat(ui): a scrollbar held where it was taken, a popover that keeps the mouse, easing that lands

- the scrollbar is .ui-scrollbar with a .ui-thumb, styled by the default sheet or a theme; a press
  on the thumb holds it at the point it was taken and the pointer moves it in proportion, a press on
  the track jumps the thumb's middle there and holds it, and neither presses what is under the bar
  (it used to centre the thumb on the pointer and press the row beneath as well);
- while a popover is up only scroll boxes inside it take the pointer, and a press outside it that
  closes it forgets any press in progress;
- a transition ends exactly on its value (it stopped a rounding error short, at every frame rate).

Tests: ui_popover_mouse, ui_scrollbar, ui_ease (60/120/180/240 Hz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 21:39:28 +03:00
parent 9f59000f5b
commit a4c7a756f0
19 changed files with 299 additions and 29 deletions

View file

@ -0,0 +1,63 @@
# scroll_bar.ludic - a scroll box's bar: .ui-scrollbar, the track down its right edge, and its
# .ui-thumb, styled by the default sheet or a theme as the box's own parts. A press on the thumb holds
# it where it was taken; a press on the track jumps the thumb's middle to the pointer and holds it there.
var sb_x: float = 0.0
var sb_w: float = 0.0
var sb_ty: float = 0.0
var sb_th: float = 0.0
var sb_bar: UiNode = null
var sc_grab: float = 0.0 # how far below the thumb's top the pointer holds it
# the bar's parts, styled as descendants of the box, and where they are at this offset
function sb_geo(n: UiNode, off: float, room: float, most: float) -> void {
let bar = ct_part(n, "ui-scrollbar", "")
bar.tag = "div"
bar.parent = n
push(bar.children, ct_part(n, "ui-thumb", ""))
cs_tree(bar)
sb_bar = bar
sb_w = ui_px(6.0)
if bar.w >= 0.0 { sb_w = bar.w }
sb_x = n.x + n.cw - sb_w - bar.mr
var least = ui_px(24.0)
if len(bar.children) > 0 and bar.children[0].min_h > 0.0 { least = bar.children[0].min_h }
sb_th = Math.min(Math.max(n.ch * room / (room + most), least), n.ch)
sb_ty = n.y
if most > 0.0 { sb_ty = n.y + (n.ch - sb_th) * Math.clamp(off, 0.0, most) / most }
}
# the pointer on the bar: taken on a press over it, moved while held, let go on release
function sb_input(n: UiNode, off: float, room: float, most: float) -> float {
let i = in_now
if most <= 0.0 or room <= 0.0 { return off }
if i.down and not in_was_down and sc_drag == "" {
sb_geo(n, off, room, most)
let reach = Math.min(sb_x, n.x + n.cw - ui_px(14.0))
let over = i.x >= reach and i.y >= n.y and i.x < n.x + n.cw and i.y < n.y + n.ch
if over and not sb_bar.hidden {
sc_drag = n.key
sc_grab = sb_th / 2.0
if i.y >= sb_ty and i.y < sb_ty + sb_th { sc_grab = i.y - sb_ty }
}
}
if sc_drag != n.key { return off }
sb_geo(n, off, room, most)
let span = n.ch - sb_th
if span <= 0.0 { return off }
return (i.y - sc_grab - n.y) / span * most
}
# the track, then the thumb over it; scrollbar-color on the box wins over the parts' backgrounds
function sc_bar(n: UiNode) -> void {
let room = n.ch - ly_vin(n)
let most = n.content_h - ly_vin(n) - room
if most <= 0.0 or ui_be.rect == null { return }
sb_geo(n, n.scroll_y, room, most)
let bar = sb_bar
if bar.hidden { return }
var track = bar.bg
if n.bar_track != -2 { track = n.bar_track }
if track >= 0 { fr_box(sb_x, n.y, sb_w, n.ch, bar.radius, track, bar.bg_a * fr_ga) }
if len(bar.children) == 0 { return }
let t = bar.children[0]
var thumb = t.bg
if n.bar_c >= 0 { thumb = n.bar_c }
if thumb >= 0 { fr_box(sb_x, sb_ty, sb_w, sb_th, t.radius, thumb, t.bg_a * fr_ga) }
}