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:
parent
9f59000f5b
commit
a4c7a756f0
19 changed files with 299 additions and 29 deletions
|
|
@ -23,6 +23,7 @@ import "controls_number.ludic"
|
|||
import "input.ludic"
|
||||
import "focus.ludic"
|
||||
import "scroll.ludic"
|
||||
import "scroll_bar.ludic"
|
||||
import "popover.ludic"
|
||||
import "popover_anchor.ludic"
|
||||
import "tooltip.ludic"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# pointer.ludic - the pointer: what it is over is :hover (the element and all around it, as in CSS),
|
||||
# a press on a control focuses it and makes it :active, and letting go over it is a click. The
|
||||
# control being dragged (a range's thumb, a scrollbar) keeps the pointer until it is let go.
|
||||
# pointer.ludic - the pointer: what it is over is :hover (and all around it, as in CSS), a press on a
|
||||
# control focuses it and makes it :active, letting go over it is a click. A dragged thumb or scrollbar
|
||||
# keeps the pointer until let go; a press that takes a scrollbar or closes a popover presses nothing else.
|
||||
var fr_hover: []string = new []string
|
||||
var fr_press_key: string = ""
|
||||
var fr_down_now: bool = false
|
||||
|
|
@ -16,6 +16,7 @@ function fr_pointer(root: UiNode) -> void {
|
|||
fr_hover = keys
|
||||
tt_track(scope, i.x, i.y)
|
||||
if top != null and pp_pointer(top) { return }
|
||||
if sc_drag != "" { return }
|
||||
let hit = fr_hit(scope, i.x, i.y)
|
||||
if i.down and not in_was_down {
|
||||
ct_key_pressed_away(hit)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ function pp_pointer(top: UiNode) -> bool {
|
|||
let i = in_now
|
||||
if not (i.down and not in_was_down) { return false }
|
||||
if i.x >= top.x and i.y >= top.y and i.x < top.x + top.cw and i.y < top.y + top.ch { return false }
|
||||
fr_press_key = ""
|
||||
ui_fire(top, "close", Value.null())
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
# scroll.ludic - a box with overflow (or <scroll>) scrolls itself: the wheel over it, a scrollbar
|
||||
# thumb dragged, and the focused control pulled into view. Its content is shifted by how far it is
|
||||
# scroll.ludic - a box with overflow (or <scroll>) scrolls itself: the wheel over it, its scrollbar
|
||||
# held and dragged, and the focused control pulled into view. Its content is shifted by how far it is
|
||||
# scrolled and clipped to it, and the offset is kept by its key from frame to frame.
|
||||
var sc_keys: []string = new []string
|
||||
var sc_offs: []float = new []float
|
||||
var sc_drag: string = ""
|
||||
var sc_scope: UiNode = null # the popover that has the pointer this frame, if one is up
|
||||
function sc_get(key: string) -> float {
|
||||
for i in 0 .. len(sc_keys) {
|
||||
if sc_keys[i] == key { return sc_offs[i] }
|
||||
|
|
@ -21,7 +22,11 @@ function sc_put(key: string, v: float) -> void {
|
|||
push(sc_offs, v)
|
||||
}
|
||||
# every scroll box, outermost first: its offset (from input, when `live`), then its content moved
|
||||
function sc_update(n: UiNode) -> void { sc_walk(n, true) }
|
||||
function sc_update(n: UiNode) -> void {
|
||||
sc_scope = pp_top(n)
|
||||
if not in_now.down { sc_drag = "" }
|
||||
sc_walk(n, true)
|
||||
}
|
||||
function sc_apply(n: UiNode) -> void { sc_walk(n, false) }
|
||||
function sc_walk(n: UiNode, live: bool) -> void {
|
||||
if n.kind == UI_SCROLL {
|
||||
|
|
@ -38,17 +43,25 @@ function sc_walk(n: UiNode, live: bool) -> void {
|
|||
}
|
||||
for k in 0 .. len(n.children) { sc_walk(n.children[k], live) }
|
||||
}
|
||||
# the wheel over it, its thumb dragged, the keyboard's focus pulled into view
|
||||
# the wheel over it, its bar held and dragged (scroll_bar.ludic), the keyboard's focus pulled into
|
||||
# view; while a popover is up, only a box inside it answers the pointer
|
||||
function sc_input(n: UiNode, off0: float, room: float, most: float) -> float {
|
||||
var off = off0
|
||||
if sc_scope != null and not sc_within(n, sc_scope) { return sc_into_view(n, off, room) }
|
||||
let i = in_now
|
||||
let over = i.x >= n.x and i.y >= n.y and i.x < n.x + n.cw and i.y < n.y + n.ch
|
||||
if over and i.wheel != 0.0 { off = off - i.wheel * ui_px(40.0) }
|
||||
if i.down and not in_was_down and over and i.x >= n.x + n.cw - ui_px(14.0) and most > 0.0 { sc_drag = n.key }
|
||||
if not i.down { sc_drag = "" }
|
||||
if sc_drag == n.key and room > 0.0 { off = (i.y - n.y) / room * (most + room) - room / 2.0 }
|
||||
off = sb_input(n, off, room, most)
|
||||
return sc_into_view(n, off, room)
|
||||
}
|
||||
function sc_within(n: UiNode, top: UiNode) -> bool {
|
||||
var at = n
|
||||
while at != null {
|
||||
if at == top { return true }
|
||||
at = at.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
# the focused control, moved by the keyboard, is brought into the box's view
|
||||
function sc_into_view(n: UiNode, off: float, room: float) -> float {
|
||||
if not fc_visible or fc_key == "" or not lc_mine(fc_key, n.key) { return off }
|
||||
|
|
@ -59,22 +72,6 @@ function sc_into_view(n: UiNode, off: float, room: float) -> float {
|
|||
if top + f.ch > off + room { return top + f.ch - room + ui_px(8.0) }
|
||||
return off
|
||||
}
|
||||
# the scrollbar: a thumb on a track down the box's right edge, when there is more than it shows
|
||||
function sc_bar(n: UiNode) -> void {
|
||||
let room = n.ch - ly_vin(n)
|
||||
let total = n.content_h - ly_vin(n)
|
||||
if total <= room or ui_be.rect == null { return }
|
||||
let w = ui_px(6.0)
|
||||
let x = n.x + n.cw - w - ui_px(2.0)
|
||||
var track = n.bar_track
|
||||
if track == -2 { track = 3355443 }
|
||||
var thumb = n.bar_c
|
||||
if thumb < 0 { thumb = 10066329 }
|
||||
if track >= 0 { ui_be.rect(x, n.y, w, n.ch, track, 0.4 * fr_ga) }
|
||||
let th = Math.max(n.ch * room / total, ui_px(24.0))
|
||||
let ty = n.y + (n.ch - th) * n.scroll_y / (total - room)
|
||||
ui_be.rect(x, ty, w, th, thumb, 0.85 * fr_ga)
|
||||
}
|
||||
# ui_place moved every box by its stored offset; the frame takes that back before moving it again
|
||||
function sc_undo(n: UiNode) -> void {
|
||||
if n.kind == UI_SCROLL and n.scroll_y != 0.0 {
|
||||
|
|
|
|||
63
packages/ludic.ui/scroll_bar.ludic
Normal file
63
packages/ludic.ui/scroll_bar.ludic
Normal 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) }
|
||||
}
|
||||
|
|
@ -27,5 +27,6 @@ function tr_apply(n: UiNode) -> void {
|
|||
}
|
||||
function tr_value(i: int, now: float, dur: float) -> float {
|
||||
let t = Math.clamp((now - tr_at[i]) / dur, 0.0, 1.0)
|
||||
if t >= 1.0 { return tr_to[i] }
|
||||
return tr_from[i] + (tr_to[i] - tr_from[i]) * t
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# ua.ludic - what every element looks like before a program styles it, as a browser's own sheet
|
||||
# does: headings sized, buttons and controls usable and plain, a focus ring. A theme overrides any
|
||||
# of it, with the same selectors or with the variables it reads.
|
||||
const UA_LSS: string = "h1 { font-size: 32 } h2 { font-size: 26 } h3 { font-size: 22 } h4 { font-size: 19 } h5 { font-size: 16 } h6 { font-size: 14 } small { font-size: 13 } hr { height: 1; width: fill; background: #808080; margin: 4 0 } scroll { width: fill; height: fill } img { width: 24; height: 24 } button { align-items: center; justify-content: center; gap: 8; padding: 6 14; background: #3a3f46; border-radius: 4 } button:hover { background: #4a5058 } button:disabled { opacity: 0.4 } :focus-visible { outline: 2px solid #ffcc55; outline-offset: 2 } input, select { align-items: center; gap: 10; padding: 6 10 } .ui-label { flex-grow: 1 } .ui-labels { flex-grow: 1; gap: 2 } .ui-labels .ui-label { flex-grow: 0 } .ui-note { font-size: 13; color: #9aa0a6 } .ui-track { flex-direction: row; align-items: center; width: 44; height: 22; padding: 3; background: #555a60; border-radius: 11 } .ui-knob { width: 16; height: 16; background: #eeeeee; border-radius: 8 } input:checked .ui-track { justify-content: end; background: #d9a54a } input[type=range] .ui-track { width: 200; height: 8; padding: 0; border-radius: 4 } .ui-fill { height: fill; background: #d9a54a; justify-content: end; align-items: center; border-radius: 4 } .ui-thumb { width: 14; height: 14; background: #eeeeee; border-radius: 7; margin: 0 -7 } .ui-value { min-width: 40 } .ui-field { width: 200; padding: 4 8; background: #202428; border: 1px solid #555a60 } .ui-caret { width: 2; height: 1.2em; background: #eeeeee } .ui-prev, .ui-next { color: #aaaaaa } progress, meter { width: 160; height: 10; background: #202428; border-radius: 5 } progress .ui-fill, meter .ui-fill { justify-content: start; border-radius: 5 } [popover] { position: absolute; z-index: 900 } .ui-tooltip { position: fixed; padding: 6 10; background: #202428; border: 1px solid #555a60; font-size: 15; z-index: 1000 }"
|
||||
const UA_LSS: string = "h1 { font-size: 32 } h2 { font-size: 26 } h3 { font-size: 22 } h4 { font-size: 19 } h5 { font-size: 16 } h6 { font-size: 14 } small { font-size: 13 } hr { height: 1; width: fill; background: #808080; margin: 4 0 } scroll { width: fill; height: fill } img { width: 24; height: 24 } button { align-items: center; justify-content: center; gap: 8; padding: 6 14; background: #3a3f46; border-radius: 4 } button:hover { background: #4a5058 } button:disabled { opacity: 0.4 } :focus-visible { outline: 2px solid #ffcc55; outline-offset: 2 } input, select { align-items: center; gap: 10; padding: 6 10 } .ui-label { flex-grow: 1 } .ui-labels { flex-grow: 1; gap: 2 } .ui-labels .ui-label { flex-grow: 0 } .ui-note { font-size: 13; color: #9aa0a6 } .ui-track { flex-direction: row; align-items: center; width: 44; height: 22; padding: 3; background: #555a60; border-radius: 11 } .ui-knob { width: 16; height: 16; background: #eeeeee; border-radius: 8 } input:checked .ui-track { justify-content: end; background: #d9a54a } input[type=range] .ui-track { width: 200; height: 8; padding: 0; border-radius: 4 } .ui-fill { height: fill; background: #d9a54a; justify-content: end; align-items: center; border-radius: 4 } .ui-thumb { width: 14; height: 14; background: #eeeeee; border-radius: 7; margin: 0 -7 } .ui-value { min-width: 40 } .ui-field { width: 200; padding: 4 8; background: #202428; border: 1px solid #555a60 } .ui-caret { width: 2; height: 1.2em; background: #eeeeee } .ui-prev, .ui-next { color: #aaaaaa } progress, meter { width: 160; height: 10; background: #202428; border-radius: 5 } progress .ui-fill, meter .ui-fill { justify-content: start; border-radius: 5 } .ui-scrollbar { width: 6; margin: 0 2 0 0; background: rgba(51, 51, 51, 0.4); border-radius: 3 } .ui-scrollbar .ui-thumb { min-height: 24; margin: 0; background: rgba(153, 153, 153, 0.85); border-radius: 3 } [popover] { position: absolute; z-index: 900 } .ui-tooltip { position: fixed; padding: 6 10; background: #202428; border: 1px solid #555a60; font-size: 15; z-index: 1000 }"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue