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
55
examples/library/ui_ease.ludic
Normal file
55
examples/library/ui_ease.ludic
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# ui_ease.ludic — easing never depends on the frame rate: a transition and an animation stepped at
|
||||
# 60, 120, 180 and 240 Hz on the program's clock are at the same value half way (kept in float, never
|
||||
# cut to whole pixels) and land exactly on their targets when their time is up.
|
||||
import "ludic.ui"
|
||||
program UiEase {
|
||||
numbers float
|
||||
var dim: bool = false
|
||||
var now: float = 0.0
|
||||
view Ease {
|
||||
dim = dim
|
||||
}
|
||||
const STYLE: string = "<style>@keyframes slide { from { left: 0 } to { left: 37 } }\n.go { animation: slide 0.25s }\n.soft { transition: opacity 0.25s }\n.dim { opacity: 0.2 }</style>"
|
||||
const BODY: string = "<p class='go'>a</p><p class=\"soft {dim ? 'dim' : ''}\">b</p><p class='dim'>c</p></screen>"
|
||||
function clock() -> float { return now }
|
||||
function nodes(name: string) -> UiNode {
|
||||
ui_show(name, view_ease(), 0.0, 0.0, 200.0, 200.0)
|
||||
return ui_nodes(name, view_ease())
|
||||
}
|
||||
# one rate, on its own screen: the slide from its first frame, the fade from half a second in
|
||||
function at_rate(hz: int) -> string {
|
||||
let name = `r{hz}`
|
||||
dim = false
|
||||
var mid = ""
|
||||
var slid = false
|
||||
var landed = false
|
||||
var over = false
|
||||
for k in 0 .. hz + 1 {
|
||||
now = float(k) / float(hz)
|
||||
if k == hz / 2 { dim = true }
|
||||
let r = nodes(name)
|
||||
let go = r.children[0]
|
||||
let soft = r.children[1]
|
||||
let want = r.children[2].alpha
|
||||
if k == hz / 10 { mid = `{int(Math.round(go.left * 100.0))} ` }
|
||||
if k == hz / 2 + hz / 10 { mid = mid + `{int(Math.round(soft.alpha * 1000.0))}` }
|
||||
if k == hz / 4 { slid = go.left == 37.0 }
|
||||
if k == hz / 2 + hz / 4 { landed = soft.alpha == want }
|
||||
if k > hz / 2 + hz / 4 and soft.alpha != want { over = true }
|
||||
}
|
||||
return `{hz}: {mid} slid {slid} landed {landed} left {over}`
|
||||
}
|
||||
entry {
|
||||
ui_backend(new UiBackend)
|
||||
ui_clock(fn clock)
|
||||
var page = "<ui>" + STYLE
|
||||
for i in 0 .. 4 {
|
||||
let hz = 60 * (i + 1)
|
||||
page = page + `<screen name='r{hz}'>` + BODY
|
||||
}
|
||||
ui_load_text(page + "</ui>", "e.xml")
|
||||
var out = ""
|
||||
for i in 0 .. 4 { out = out + at_rate(60 * (i + 1)) + "; " }
|
||||
print(out)
|
||||
}
|
||||
}
|
||||
52
examples/library/ui_popover_mouse.ludic
Normal file
52
examples/library/ui_popover_mouse.ludic
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# ui_popover_mouse.ludic — a popover takes the mouse: its own button, at the same pixels as the grid
|
||||
# cell under it, is the one a click presses; a click outside it (on that cell, or on nothing) closes
|
||||
# it and presses nothing else.
|
||||
import "ludic.ui"
|
||||
import "ui_popover_mouse_parts/Grid.ludic"
|
||||
program UiPopoverMouse {
|
||||
numbers float
|
||||
function frame(i: UiInput) -> UiNode {
|
||||
ui_input(i)
|
||||
ui_show("Grid", null, 0.0, 0.0, 400.0, 400.0)
|
||||
let root: UiNode = ui_nodes("Grid", null)
|
||||
ui_place(root, 0.0, 0.0, 400.0, 400.0)
|
||||
return root
|
||||
}
|
||||
function click(x: float, y: float) -> string {
|
||||
let d = new UiInput
|
||||
d.x = x
|
||||
d.y = y
|
||||
d.down = true
|
||||
frame(d)
|
||||
let u = new UiInput
|
||||
u.x = x
|
||||
u.y = y
|
||||
frame(u)
|
||||
let root = frame(new UiInput)
|
||||
let page = root.children[0]
|
||||
return page.children[len(page.children) - 1].text
|
||||
}
|
||||
function find(n: UiNode, id: string) -> UiNode {
|
||||
if n.id == id { return n }
|
||||
for i in 0 .. len(n.children) {
|
||||
let f = find(n.children[i], id)
|
||||
if f != null { return f }
|
||||
}
|
||||
return null
|
||||
}
|
||||
entry {
|
||||
ui_backend(new UiBackend)
|
||||
frame(new UiInput)
|
||||
let opened = click(20.0, 20.0)
|
||||
let root = frame(new UiInput)
|
||||
let drop = find(root, "drop")
|
||||
let cell = find(root, "cell")
|
||||
let over = drop.x >= cell.x and drop.y >= cell.y and drop.x + drop.cw <= cell.x + cell.cw
|
||||
let dropped = click(drop.x + drop.cw / 2.0, drop.y + drop.ch / 2.0)
|
||||
click(20.0, 20.0)
|
||||
let on_cell = click(180.0, 90.0)
|
||||
click(20.0, 20.0)
|
||||
let on_nothing = click(390.0, 390.0)
|
||||
print(`over the cell {over} | {opened} | {dropped} | {on_cell} | {on_nothing}`)
|
||||
}
|
||||
}
|
||||
3
examples/library/ui_popover_mouse_parts/Grid.lss
Normal file
3
examples/library/ui_popover_mouse_parts/Grid.lss
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.page { width: 300px; height: 300px }
|
||||
.cell { width: 200px; height: 100px }
|
||||
.verbs { top: 10px; left: 10px; width: 120px }
|
||||
8
examples/library/ui_popover_mouse_parts/Grid.ludic
Normal file
8
examples/library/ui_popover_mouse_parts/Grid.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Grid.ludic - a grid cell with a popover of verbs laid over it, written BEFORE the cell, so the cell
|
||||
# comes later in the tree and would win any hit test that went by the order things are drawn in
|
||||
component Grid {
|
||||
state open: bool = false
|
||||
state opened: int = 0
|
||||
state dropped: int = 0
|
||||
state closed: int = 0
|
||||
}
|
||||
9
examples/library/ui_popover_mouse_parts/Grid.xml
Normal file
9
examples/library/ui_popover_mouse_parts/Grid.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<div class="page">
|
||||
<if test="{open}">
|
||||
<div popover class="verbs" on-close="set open = false; set closed = closed + 1">
|
||||
<button id="drop" on-click="set dropped = dropped + 1; set open = false">Drop</button>
|
||||
</div>
|
||||
</if>
|
||||
<button id="cell" class="cell" on-click="set opened = opened + 1; set open = true">Axe</button>
|
||||
<p>opened {opened} dropped {dropped} closed {closed} {open}</p>
|
||||
</div>
|
||||
52
examples/library/ui_scrollbar.ludic
Normal file
52
examples/library/ui_scrollbar.ludic
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# ui_scrollbar.ludic — a scroll box's bar held and dragged: a press on the thumb holds it where it
|
||||
# was taken, the pointer held moves it in proportion, letting go leaves it; a press on the track
|
||||
# jumps the thumb's middle there; the wheel still scrolls; and the rows under the bar are never pressed.
|
||||
import "ludic.ui"
|
||||
import "ui_scrollbar_parts/Long.ludic"
|
||||
program UiScrollbar {
|
||||
numbers float
|
||||
var bx: float = 0.0
|
||||
var by: float = 0.0
|
||||
function frame(i: UiInput) -> UiNode {
|
||||
ui_input(i)
|
||||
ui_show("Long", null, 0.0, 0.0, 400.0, 400.0)
|
||||
let root: UiNode = ui_nodes("Long", null)
|
||||
ui_place(root, 0.0, 0.0, 400.0, 400.0)
|
||||
return root.children[0]
|
||||
}
|
||||
# the pointer at (bx + x, by + y), held or not; how far the box is scrolled then
|
||||
function at(x: float, y: float, down: bool) -> int {
|
||||
let i = new UiInput
|
||||
i.x = bx + x
|
||||
i.y = by + y
|
||||
i.down = down
|
||||
return int(frame(i).children[0].scroll_y)
|
||||
}
|
||||
function wheel(x: float, y: float) -> int {
|
||||
let i = new UiInput
|
||||
i.x = bx + x
|
||||
i.y = by + y
|
||||
i.wheel = 1.0
|
||||
frame(i)
|
||||
return int(frame(new UiInput).children[0].scroll_y)
|
||||
}
|
||||
entry {
|
||||
ui_backend(new UiBackend)
|
||||
let list = frame(new UiInput).children[0]
|
||||
bx = list.x
|
||||
by = list.y
|
||||
let grab = at(195.0, 10.0, true)
|
||||
let moved = at(195.0, 40.0, true)
|
||||
let far = at(195.0, 300.0, true)
|
||||
let back = at(195.0, 25.0, true)
|
||||
let kept = at(195.0, 25.0, false)
|
||||
at(0.0 - bx - 1.0, 0.0 - by - 1.0, false)
|
||||
let jump = at(195.0, 80.0, true)
|
||||
at(195.0, 80.0, false)
|
||||
let wheeled = wheel(50.0, 50.0)
|
||||
at(50.0, 20.0, true)
|
||||
at(50.0, 20.0, false)
|
||||
let page = frame(new UiInput)
|
||||
print(`grab {grab} moved {moved} far {far} back {back} kept {kept} | jump {jump} | wheel {wheeled} | {page.children[1].text}`)
|
||||
}
|
||||
}
|
||||
3
examples/library/ui_scrollbar_parts/Long.lss
Normal file
3
examples/library/ui_scrollbar_parts/Long.lss
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.page { width: 300px; height: 300px }
|
||||
.list { overflow: auto; width: 200px; height: 100px }
|
||||
.row { width: fill; height: 40px; padding: 0; border-radius: 0 }
|
||||
5
examples/library/ui_scrollbar_parts/Long.ludic
Normal file
5
examples/library/ui_scrollbar_parts/Long.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Long.ludic - a box of ten rows, four times taller than it shows; each row is a button as wide as
|
||||
# the box, so the scrollbar sits over them
|
||||
component Long {
|
||||
state pressed: int = 0
|
||||
}
|
||||
4
examples/library/ui_scrollbar_parts/Long.xml
Normal file
4
examples/library/ui_scrollbar_parts/Long.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<div class="page">
|
||||
<div class="list"><each in="{range(10)}" as="r"><button class="row" on-click="set pressed = pressed + 1">{r}</button></each></div>
|
||||
<p>pressed {pressed}</p>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue