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
|
|
@ -444,6 +444,9 @@ applies to both:
|
|||
it is let go.
|
||||
- A scroll box (`overflow: auto` or `<scroll>`) takes the wheel, has a scrollbar that can be
|
||||
dragged (`scrollbar-color`), clips what it holds and pulls the focused control into view.
|
||||
- The scrollbar is `.ui-scrollbar` with a `.ui-thumb`, styled like a control's parts. A press on
|
||||
the thumb holds it where it was taken and the pointer moves it in proportion; a press on the
|
||||
track jumps the thumb's middle there and holds it. A press on the bar presses nothing under it.
|
||||
- `:focus`, `:focus-visible` and `:focus-within` style the focus, and `outline` (which follows
|
||||
`border-radius`) draws the ring.
|
||||
- A test drives all of it with `ui_input(i)`.
|
||||
|
|
@ -474,6 +477,8 @@ applies to both:
|
|||
- **Popovers, tooltips and bars.**
|
||||
- `<div popover on-close="...">` sits absolutely in its parent and draws on the top layer. While
|
||||
it is up, the pointer and Tab stay inside it, and a press outside it or Esc closes it.
|
||||
A press on it goes to its own controls, never to what lies under it at the same pixels, and a
|
||||
press outside it only closes it.
|
||||
- `anchor="cell"` puts a popover beside the element with that id (the nearest, looking out from
|
||||
the popover), and a bare `anchor` beside the element before it. `placement` is `right` (the
|
||||
default), `left`, `bottom` or `top`, its margin on that side is the gap, and where it would
|
||||
|
|
@ -511,7 +516,8 @@ applies to both:
|
|||
`color` around it, as text does. An `<img>` with a `color` of its own is tinted by it.
|
||||
- Motion: `opacity` fades the element and everything inside it. `@keyframes` with `animation:
|
||||
name 0.25s [infinite] [alternate]` animate numeric properties from when the element first
|
||||
appeared, and `transition: opacity 0.2s` eases a changed opacity.
|
||||
appeared, and `transition: opacity 0.2s` eases a changed opacity. Both run on the clock, in
|
||||
float, and land exactly on their end values whatever the frame rate.
|
||||
- **Mixed content.** Text beside elements keeps its place: `<button><img src="icon:arrow"/>Resume
|
||||
</button>`, `<p>Hi <b>there</b></p>`. A boolean attribute present with no value is true, as in
|
||||
HTML (`<button autofocus>`).
|
||||
|
|
|
|||
|
|
@ -73,7 +73,13 @@ What a game's screens found missing, now in `ludic.ui`:
|
|||
- a component root that is itself a component takes every user's class, style and id, and the
|
||||
sheets that style it are weighed together by specificity;
|
||||
- a component's event may be called `set`; a `string` prop given a number reads it as text; two
|
||||
components of one name are an error naming both files.
|
||||
components of one name are an error naming both files;
|
||||
- the scrollbar is `.ui-scrollbar` and `.ui-thumb`: a press on the thumb holds it where it was taken,
|
||||
a press on the track jumps the thumb's middle there, and neither presses what is under the bar;
|
||||
- a popover's own controls take its presses whatever lies under it, a press outside only closes it,
|
||||
and while one is up the scroll boxes outside it do not take the pointer;
|
||||
- a transition lands exactly on its end value (it had stopped a rounding error short of it, at every
|
||||
frame rate).
|
||||
|
||||
render3d gains `tex_width` / `tex_height`, and the XML reader keeps text runs among elements in
|
||||
order (`mixed`).
|
||||
|
|
|
|||
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>
|
||||
|
|
@ -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 }"
|
||||
|
|
|
|||
|
|
@ -760,6 +760,9 @@ function cmd_dev_test() -> int {
|
|||
controller_case("library/ui_controls", "", "1 false 7 2 Adalove 70 1 false 8 2 Adalove 70; scrolled 40", "ui_controls.ludic (L11: focus and keys, the built-in controls, a range dragged, a box scrolled)")
|
||||
controller_case("library/ui_anim", "", "0 100 100 | 49 50 100 | 100 100 61", "ui_anim.ludic (L11: @keyframes and animation, looping and alternating, and a transition)")
|
||||
controller_case("library/ui_popover", "", "drop false | drop true | drop false | drop false | fill 60 of 200", "ui_popover.ludic (L11: a popover keeps the keys and closes on a press outside or Esc; progress fills)")
|
||||
controller_case("library/ui_popover_mouse", "", "over the cell 1 | opened 1 dropped 0 closed 0 true | opened 1 dropped 1 closed 0 false | opened 2 dropped 1 closed 1 false | opened 3 dropped 1 closed 2 false", "ui_popover_mouse.ludic (L11: a popover takes the mouse - its own button wins over the cell under it, and a press outside closes it and presses nothing)")
|
||||
controller_case("library/ui_scrollbar", "", "grab 0 moved 120 far 300 back 60 kept 60 | jump 270 | wheel 230 | pressed 1", "ui_scrollbar.ludic (L11: a scrollbar's thumb held where it was taken and dragged, a press on the track jumps there, the wheel, nothing under the bar pressed)")
|
||||
controller_case("library/ui_ease", "", "60: 1480 680 slid 1 landed 1 left 0; 120: 1480 680 slid 1 landed 1 left 0; 180: 1480 680 slid 1 landed 1 left 0; 240: 1480 680 slid 1 landed 1 left 0;", "ui_ease.ludic (L11: a transition and an animation at 60, 120, 180 and 240 Hz are the same half way and land exactly on their targets)")
|
||||
controller_case("library/ui_inputs", "", "count 12 12 32 0.5 0 | 20 32 0.5 0 | 19 32 0.5 0 | How many to carry | Space bar listening 1 19 9 0.5 0 | 19 9 0.5 0 | 19 0 0.5 0 | 60% 0.50 s | 19 0 0.6 9 | 7!", "ui_inputs.ludic (L11: number fields, key capture with Esc and Backspace, notes, range formats, an event named set, a string prop given a number)")
|
||||
controller_case("library/ui_boxes", "", "list 120 of 200 | text at 32 152 wide, 3 lines, row 60 | zero 0x0 | half 140x36 pad 4 2 | order abc at 16 | fill 98x8 of 200x12", "ui_boxes.ludic (L11: flex-shrink to the leftover room, text wrapping beside its siblings, zero sizes, calc(), order, the range track as a row)")
|
||||
controller_case("library/ui_look", "", "root div.in.i.m pad 5 margin 9 color #ff0000 text Hi 2,53 #102030 | text Hi 0,50 #eeeeee | opacity 0.25 | nine 40 #ffffff nine 40 #ff0000 of 2 image photo.png #ffffff | image icon:tent #00ff00 | text first line 29,205 #eeeeee | text second line 29,223 #eeeeee by id at 84,50 | flipped at 246,268 100x32", "ui_look.ludic (L11: nested components styled by specificity, text-shadow, ui_opacity, border-image tints, pictures, tooltip lines, anchored popovers)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue