feat(ui): ui_scale and ui_box, on-submit, zoom (and a length over a length in calc), on-hold
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3d2a103dfb
commit
ec9a650e65
24 changed files with 323 additions and 23 deletions
15
LANGUAGE.md
15
LANGUAGE.md
|
|
@ -455,7 +455,9 @@ applies to both:
|
||||||
go to that element until it is let go, wherever the pointer is. A scroll box leaves the wheel to
|
go to that element until it is let go, wherever the pointer is. A scroll box leaves the wheel to
|
||||||
an element inside it that takes `on-wheel`.
|
an element inside it that takes `on-wheel`.
|
||||||
- `on-down` and `on-up` say when a button goes down and comes up again, by the pointer (released
|
- `on-down` and `on-up` say when a button goes down and comes up again, by the pointer (released
|
||||||
anywhere) or by Enter or the pad's A held on it; it is `:active` in between.
|
anywhere) or by Enter or the pad's A held on it; it is `:active` in between. `on-hold` runs
|
||||||
|
every frame it is held, with `event.dt` (seconds since the last frame) and `event.t` (how long
|
||||||
|
it has been held), on the ui clock.
|
||||||
- A scroll box (`overflow: auto` or `<scroll>`) takes the wheel, has a scrollbar that can be
|
- 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.
|
dragged (`scrollbar-color`), clips what it holds and pulls the focused control into view.
|
||||||
- `scroll-top="{px}"` holds a scroll box at that offset every frame; the wheel, the bar and the
|
- `scroll-top="{px}"` holds a scroll box at that offset every frame; the wheel, the bar and the
|
||||||
|
|
@ -482,7 +484,8 @@ applies to both:
|
||||||
`unit=" s"` after it;
|
`unit=" s"` after it;
|
||||||
- `<select label value>` with `<option value>`s;
|
- `<select label value>` with `<option value>`s;
|
||||||
- `<input type="text" label value maxlength>`, which takes typing, and Backspace takes a letter
|
- `<input type="text" label value maxlength>`, which takes typing, and Backspace takes a letter
|
||||||
back;
|
back. Enter (or the pad's A) on it runs `on-submit` with `event.value`; the focus stays, and so
|
||||||
|
does the text unless it says `clear-on-submit`;
|
||||||
- `<input type="number" label min max step value>`: typed digits (a minus when min allows one, a
|
- `<input type="number" label min max step value>`: typed digits (a minus when min allows one, a
|
||||||
point when step has a fraction) replace the value, Enter or leaving the field commits them held
|
point when step has a fraction) replace the value, Enter or leaving the field commits them held
|
||||||
between min and max, and the left and right arrows step it (`decimals`, `unit` as a range's);
|
between min and max, and the left and right arrows step it (`decimals`, `unit` as a range's);
|
||||||
|
|
@ -533,6 +536,10 @@ applies to both:
|
||||||
- nine-slices for `border-image`, sliced by the texture's own width and height;
|
- nine-slices for `border-image`, sliced by the texture's own width and height;
|
||||||
- a scale from the screen's height (`ui_render3d_scale` for the player's interface size).
|
- a scale from the screen's height (`ui_render3d_scale` for the player's interface size).
|
||||||
|
|
||||||
|
`ui_scale()` is the scale the interface is drawn at (screen pixels per design pixel), and
|
||||||
|
`ui_box("id")` where an element (by id or key) was laid out when its screen was last shown, as a
|
||||||
|
`UiBox` of `x`, `y`, `w` and `h` in screen pixels, or null when there is none.
|
||||||
|
|
||||||
`ui_translator(fn)`, `ui_sounds(fn)` and `ui_clock(fn)` hand the runtime the program's
|
`ui_translator(fn)`, `ui_sounds(fn)` and `ui_clock(fn)` hand the runtime the program's
|
||||||
language, sounds and time. `import "ludic.ui/screen.ludic"` is the 2D screen's renderer.
|
language, sounds and time. `import "ludic.ui/screen.ludic"` is the 2D screen's renderer.
|
||||||
Every text a template shows is translated, except inside `translate="no"` (and `translate="yes"`
|
Every text a template shows is translated, except inside `translate="no"` (and `translate="yes"`
|
||||||
|
|
@ -568,6 +575,10 @@ applies to both:
|
||||||
name 0.25s [infinite] [alternate]` animate numeric properties from when the element first
|
name 0.25s [infinite] [alternate]` animate numeric properties from when the element first
|
||||||
appeared, and `transition: opacity 0.2s` eases a changed opacity. Both run on the clock, in
|
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.
|
float, and land exactly on their end values whatever the frame rate.
|
||||||
|
- `zoom: 0.8` (or `80%`, or `calc()` / `min()`) draws an element and everything inside it that
|
||||||
|
much larger or smaller - its lengths, its text and its layout; zooms multiply. In `calc()` a
|
||||||
|
length over a length is a plain number, so `zoom: min(1, calc(100vw / 1800px))` shrinks a HUD
|
||||||
|
for a narrow window.
|
||||||
- **Mixed content.** Text beside elements keeps its place: `<button><img src="icon:arrow"/>Resume
|
- **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
|
</button>`, `<p>Hi <b>there</b></p>`. A boolean attribute present with no value is true, as in
|
||||||
HTML (`<button autofocus>`).
|
HTML (`<button autofocus>`).
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,10 @@ What a game's screens found missing, now in `ludic.ui`:
|
||||||
through a renderer with no `rect` no longer crashes;
|
through a renderer with no `rect` no longer crashes;
|
||||||
- a component with no stylesheet of its own reads a theme's `:root` variables from around it (it
|
- a component with no stylesheet of its own reads a theme's `:root` variables from around it (it
|
||||||
did; now a test says so);
|
did; now a test says so);
|
||||||
|
- `ui_scale()` and `ui_box("id")`, the scale and an element's laid-out box, for a host;
|
||||||
|
- `on-submit` on a text field (Enter or A; the focus and text stay unless `clear-on-submit`);
|
||||||
|
- `zoom` on any element, and a length over a length in `calc()` is a plain number;
|
||||||
|
- `on-hold` every frame a button is held, with `event.dt` and `event.t`;
|
||||||
- a transition lands exactly on its end value (it had stopped a rounding error short of it, at every
|
- a transition lands exactly on its end value (it had stopped a rounding error short of it, at every
|
||||||
frame rate).
|
frame rate).
|
||||||
|
|
||||||
|
|
|
||||||
19
examples/library/ui_box.ludic
Normal file
19
examples/library/ui_box.ludic
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# ui_box.ludic — what a host reads from outside the interface: ui_scale(), the scale it is drawn at
|
||||||
|
# (the renderer's), and ui_box("id"), where an element was laid out when its screen was last shown,
|
||||||
|
# in screen pixels (null for one that is not there) - to frame a 3D view beside a panel.
|
||||||
|
import "ludic.ui"
|
||||||
|
program UiBox {
|
||||||
|
numbers float
|
||||||
|
const PAGE: string = "<ui><style>.side { width: 100px; height: 60px; margin: 10px }</style><screen name='b'><p>title</p><div id='panel' class='side'/></screen></ui>"
|
||||||
|
function scale() -> float { return 1.5 }
|
||||||
|
entry {
|
||||||
|
let b = new UiBackend
|
||||||
|
b.scale = fn scale
|
||||||
|
ui_backend(b)
|
||||||
|
ui_load_text(PAGE, "b.xml")
|
||||||
|
ui_show("b", null, 20.0, 30.0, 800.0, 600.0)
|
||||||
|
let p = ui_box("panel")
|
||||||
|
let none = ui_box("nothing")
|
||||||
|
print(`scale {ui_scale()} | panel {int(p.x)},{int(p.y)} {int(p.w)}x{int(p.h)} | missing {none == null}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
examples/library/ui_hold.ludic
Normal file
59
examples/library/ui_hold.ludic
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# ui_hold.ludic — on-hold fires every frame a button is held, by the pointer (still when it is dragged
|
||||||
|
# off, until let go) or by Enter or the pad's A on the focus, with event.dt and event.t on the ui clock,
|
||||||
|
# the same at 60 and 240 Hz; on-down and on-up still mark the ends.
|
||||||
|
import "ludic.ui"
|
||||||
|
program UiHold {
|
||||||
|
numbers float
|
||||||
|
var now: float = 0.0
|
||||||
|
var held: float = 0.0
|
||||||
|
var frames: int = 0
|
||||||
|
var last_t: float = 0.0
|
||||||
|
var ends: string = ""
|
||||||
|
view Craft {
|
||||||
|
on tick(dt: float, t: float) {
|
||||||
|
held = held + dt
|
||||||
|
frames = frames + 1
|
||||||
|
last_t = t
|
||||||
|
}
|
||||||
|
on down() { ends = ends + "d" }
|
||||||
|
on up() { ends = ends + "u" }
|
||||||
|
}
|
||||||
|
const PAGE: string = "<ui><screen name='h'><button on-hold='tick(event.dt, event.t)' on-down='down()' on-up='up()'>Make</button></screen></ui>"
|
||||||
|
function clock() -> float { return now }
|
||||||
|
function frame(i: UiInput) -> UiNode {
|
||||||
|
ui_input(i)
|
||||||
|
ui_show("h", view_craft(), 0.0, 0.0, 400.0, 400.0)
|
||||||
|
let r: UiNode = ui_nodes("h", view_craft())
|
||||||
|
ui_place(r, 0.0, 0.0, 400.0, 400.0)
|
||||||
|
return r.children[0]
|
||||||
|
}
|
||||||
|
# held for `secs` at `hz`, by the pointer (off the button after the first frame) or by A
|
||||||
|
function hold(b: UiNode, secs: float, hz: int, pad: bool) -> string {
|
||||||
|
held = 0.0
|
||||||
|
frames = 0
|
||||||
|
let n = int(secs * float(hz) + 0.5)
|
||||||
|
for k in 0 .. n + 1 {
|
||||||
|
let i = new UiInput
|
||||||
|
i.pad_a = pad
|
||||||
|
i.down = not pad
|
||||||
|
i.x = b.x + 5.0
|
||||||
|
i.y = b.y + 5.0
|
||||||
|
if k > 0 and not pad { i.x = 390.0 }
|
||||||
|
frame(i)
|
||||||
|
now = now + 1.0 / float(hz)
|
||||||
|
}
|
||||||
|
frame(new UiInput)
|
||||||
|
now = now + 1.0 / float(hz)
|
||||||
|
return `{frames} frames {int(Math.round(held * 1000.0))} ms t {int(Math.round(last_t * 1000.0))}`
|
||||||
|
}
|
||||||
|
entry {
|
||||||
|
ui_backend(new UiBackend)
|
||||||
|
ui_clock(fn clock)
|
||||||
|
ui_load_text(PAGE, "h.xml")
|
||||||
|
let b = frame(new UiInput)
|
||||||
|
let slow = hold(b, 0.5, 60, false)
|
||||||
|
let fast = hold(b, 0.5, 240, false)
|
||||||
|
let pad = hold(b, 0.25, 60, true)
|
||||||
|
print(`{slow} | {fast} | pad {pad} | {ends}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
45
examples/library/ui_submit.ludic
Normal file
45
examples/library/ui_submit.ludic
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# ui_submit.ludic — on-submit on a text field: Enter (or the pad's A) while it has the focus sends what
|
||||||
|
# it holds with event.value; the focus stays, and so does the text, unless clear-on-submit empties it.
|
||||||
|
import "ludic.ui"
|
||||||
|
program UiSubmit {
|
||||||
|
numbers float
|
||||||
|
var said: string = ""
|
||||||
|
var chat: string = ""
|
||||||
|
var name: string = ""
|
||||||
|
view Chat {
|
||||||
|
chat = chat
|
||||||
|
name = name
|
||||||
|
on set_chat(v: string) { chat = v }
|
||||||
|
on set_name(v: string) { name = v }
|
||||||
|
on send(v: string) { said = said + "[" + v + "]" }
|
||||||
|
}
|
||||||
|
const PAGE: string = "<ui><screen name='c'><input type='text' label='Say' value='{chat}' clear-on-submit on-change='set_chat(event.value)' on-submit='send(event.value)'/><input type='text' label='Name' value='{name}' on-change='set_name(event.value)' on-submit='send(event.value)'/></screen></ui>"
|
||||||
|
function frame(i: UiInput) -> void {
|
||||||
|
ui_input(i)
|
||||||
|
ui_show("c", view_chat(), 0.0, 0.0, 400.0, 400.0)
|
||||||
|
}
|
||||||
|
function press(tab: bool, enter: bool, pad: bool, typed: string) -> void {
|
||||||
|
let i = new UiInput
|
||||||
|
i.tab = tab
|
||||||
|
i.enter = enter
|
||||||
|
i.pad_a = pad
|
||||||
|
i.typed = typed
|
||||||
|
frame(i)
|
||||||
|
frame(new UiInput)
|
||||||
|
}
|
||||||
|
entry {
|
||||||
|
ui_backend(new UiBackend)
|
||||||
|
ui_load_text(PAGE, "c.xml")
|
||||||
|
frame(new UiInput)
|
||||||
|
press(true, false, false, "")
|
||||||
|
press(false, false, false, "hi")
|
||||||
|
press(false, true, false, "")
|
||||||
|
let after = `{said} chat '{chat}'`
|
||||||
|
press(false, false, false, "yo")
|
||||||
|
press(false, false, true, "")
|
||||||
|
press(true, false, false, "")
|
||||||
|
press(false, false, false, "Ada")
|
||||||
|
press(false, true, false, "")
|
||||||
|
print(`{after} | {said} chat '{chat}' name '{name}'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
examples/library/ui_zoom.ludic
Normal file
21
examples/library/ui_zoom.ludic
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# ui_zoom.ludic — zoom scales an element and everything inside it: its lengths, its text and its
|
||||||
|
# layout, and zooms multiply; zoom takes calc() and min(), with a length over a length a plain number,
|
||||||
|
# so a HUD can shrink to a narrow window (zoom: min(1, calc(100vw / 800px))).
|
||||||
|
import "ludic.ui"
|
||||||
|
program UiZoom {
|
||||||
|
numbers float
|
||||||
|
const STYLE: string = "<style>.box { width: 100px; padding: 10px }\n.half { zoom: 0.5 }\n.twice { zoom: 200% }\n.fit { zoom: min(1, calc(100vw / 800px)) }\np { height: 20px }\n.em { width: 2em }</style>"
|
||||||
|
const BODY: string = "<screen name='z'><div class='box'><p>a</p></div><div class='box half'><p>b</p><div class='twice'><div class='box'/></div></div><div class='fit'><div class='box'/><div class='em'/></div></screen>"
|
||||||
|
entry {
|
||||||
|
ui_backend(new UiBackend)
|
||||||
|
ui_load_text("<ui>" + STYLE + BODY + "</ui>", "z.xml")
|
||||||
|
ui_show("z", null, 0.0, 0.0, 400.0, 600.0)
|
||||||
|
let r: UiNode = ui_nodes("z", null)
|
||||||
|
ui_place(r, 0.0, 0.0, 400.0, 600.0)
|
||||||
|
let plain = r.children[0]
|
||||||
|
let half = r.children[1]
|
||||||
|
let inner = half.children[1].children[0]
|
||||||
|
let fit = r.children[2]
|
||||||
|
print(`plain {int(plain.cw)} pad {int(plain.pt)} text {int(plain.children[0].rsize)} h {int(plain.children[0].ch)} | half {int(half.cw)} pad {int(half.pt)} text {int(half.children[0].rsize)} h {int(half.children[0].ch)} | half of twice {int(inner.cw)} | fit {int(fit.children[0].cw)} em {int(fit.children[1].cw)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ var el_s3: float = 0.0
|
||||||
# top right bottom left out of 1 to 4 lengths
|
# top right bottom left out of 1 to 4 lengths
|
||||||
function el_sides(v: Val) -> void {
|
function el_sides(v: Val) -> void {
|
||||||
let parts = new []float
|
let parts = new []float
|
||||||
if vl_num(v) { push(parts, ui_px(Value.as_float(v))) } else {
|
if vl_num(v) { push(parts, el_px(Value.as_float(v))) } else {
|
||||||
let words = el_words(vl_text(v))
|
let words = el_words(vl_text(v))
|
||||||
for i in 0 .. len(words) {
|
for i in 0 .. len(words) {
|
||||||
if len(words[i]) > 0 { push(parts, el_word_len(words[i])) }
|
if len(words[i]) > 0 { push(parts, el_word_len(words[i])) }
|
||||||
|
|
@ -39,7 +39,7 @@ function el_sides(v: Val) -> void {
|
||||||
}
|
}
|
||||||
# a length: a number, 10px, 1.5em, 2rem, 50vw, calc(...), fill, fit or auto (a percentage is el_pct's)
|
# a length: a number, 10px, 1.5em, 2rem, 50vw, calc(...), fill, fit or auto (a percentage is el_pct's)
|
||||||
function el_len(v: Val) -> float {
|
function el_len(v: Val) -> float {
|
||||||
if vl_num(v) { return ui_px(Value.as_float(v)) }
|
if vl_num(v) { return el_px(Value.as_float(v)) }
|
||||||
let s = vl_text(v)
|
let s = vl_text(v)
|
||||||
if s == "fill" { return UI_FILL }
|
if s == "fill" { return UI_FILL }
|
||||||
if ca_is(s) { return el_calc_len(s) }
|
if ca_is(s) { return el_calc_len(s) }
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ function el_border_image(n: UiNode, s: string) -> void {
|
||||||
n.bi_src = el_url(s[0..Math.min(close + 1, len(s))])
|
n.bi_src = el_url(s[0..Math.min(close + 1, len(s))])
|
||||||
let rest = lss_split(s[Math.min(close + 1, len(s))..len(s)], 47)
|
let rest = lss_split(s[Math.min(close + 1, len(s))..len(s)], 47)
|
||||||
n.bi_slice = el_num(tpl_words(rest[0]))
|
n.bi_slice = el_num(tpl_words(rest[0]))
|
||||||
n.bi_width = ui_px(n.bi_slice)
|
n.bi_width = el_px(n.bi_slice)
|
||||||
if len(rest) > 1 { n.bi_width = el_unit(tpl_words(rest[1])) }
|
if len(rest) > 1 { n.bi_width = el_unit(tpl_words(rest[1])) }
|
||||||
}
|
}
|
||||||
# box-shadow: 6px 10px 0 rgba(0, 0, 0, 0.45) - x, y, a blur this renderer draws sharp, the colour
|
# box-shadow: 6px 10px 0 rgba(0, 0, 0, 0.45) - x, y, a blur this renderer draws sharp, the colour
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# attr_fit.ludic - aspect-ratio (16 / 9, or 1.5) and object-fit (fill, contain, cover, none,
|
# attr_fit.ludic - aspect-ratio (16 / 9, or 1.5), object-fit (fill, contain, cover, none, scale-down)
|
||||||
# scale-down) for pictures and natives, and a list split at its top-level commas
|
# for pictures and natives, zoom (zoom.ludic), and a list split at its top-level commas
|
||||||
function el_fit_props(n: UiNode, key: string, s: string) -> bool {
|
function el_fit_props(n: UiNode, key: string, s: string) -> bool {
|
||||||
if key == "aspect-ratio" {
|
if key == "aspect-ratio" {
|
||||||
let parts = lss_split(s, 47)
|
let parts = lss_split(s, 47)
|
||||||
|
|
@ -12,7 +12,7 @@ function el_fit_props(n: UiNode, key: string, s: string) -> bool {
|
||||||
var k = 0
|
var k = 0
|
||||||
if w == "contain" { k = 1 } else if w == "cover" { k = 2 } else if w == "none" { k = 3 } else if w == "scale-down" { k = 4 }
|
if w == "contain" { k = 1 } else if w == "cover" { k = 2 } else if w == "none" { k = 3 } else if w == "scale-down" { k = 4 }
|
||||||
nx(n).fit_obj = k
|
nx(n).fit_obj = k
|
||||||
} else { return false }
|
} else if key == "zoom" { el_zoom(n, s) } else { return false }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
# "a, rgba(1, 2, 3, 0.5) 40%, b" -> its three parts
|
# "a, rgba(1, 2, 3, 0.5) 40%, b" -> its three parts
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ var ca_i: int = 0
|
||||||
var ca_bad: string = ""
|
var ca_bad: string = ""
|
||||||
var ca_px: float = 0.0 # the result: its lengths, its percentage, and whether it had units
|
var ca_px: float = 0.0 # the result: its lengths, its percentage, and whether it had units
|
||||||
var ca_pct: float = 0.0
|
var ca_pct: float = 0.0
|
||||||
|
var ca_num: float = 0.0 # a plain number's own value, and whether it was one
|
||||||
|
var ca_bare: bool = false
|
||||||
function ca_is(s: string) -> bool { return ca_fn_at(s, 0) != "" }
|
function ca_is(s: string) -> bool { return ca_fn_at(s, 0) != "" }
|
||||||
# calc(...) worked out into ca_px and ca_pct; false (with the reason said) when it cannot be
|
# calc(...) worked out into ca_px and ca_pct; false (with the reason said) when it cannot be
|
||||||
function ca_eval(s: string) -> bool {
|
function ca_eval(s: string) -> bool {
|
||||||
|
|
@ -21,7 +23,9 @@ function ca_eval(s: string) -> bool {
|
||||||
}
|
}
|
||||||
ca_px = v.px
|
ca_px = v.px
|
||||||
ca_pct = v.pct
|
ca_pct = v.pct
|
||||||
if v.bare { ca_px = ui_px(v.n) }
|
ca_num = v.n
|
||||||
|
ca_bare = v.bare
|
||||||
|
if v.bare { ca_px = el_px(v.n) }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
property CaVal {
|
property CaVal {
|
||||||
|
|
@ -63,6 +67,6 @@ function ca_len(a: CaVal) -> CaVal {
|
||||||
if not a.bare { return a }
|
if not a.bare { return a }
|
||||||
let r = new CaVal
|
let r = new CaVal
|
||||||
r.bare = false
|
r.bare = false
|
||||||
r.px = ui_px(a.n)
|
r.px = el_px(a.n)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# calc_terms.ludic - calc()'s products and its terms: a number with its unit, a bracket, a nested
|
# calc_terms.ludic - calc()'s products and its terms: a number with its unit, a bracket, a nested
|
||||||
# calc(), a minus in front; a product or a quotient needs a plain number on one side (on the right
|
# calc(), a minus in front; a product needs a plain number on one side, and a quotient a plain number
|
||||||
# of a /), as CSS's does
|
# or a length on its right (a length over a length is a plain number, as CSS's typed division makes)
|
||||||
function ca_product() -> CaVal {
|
function ca_product() -> CaVal {
|
||||||
var a = ca_factor()
|
var a = ca_factor()
|
||||||
while ca_bad == "" {
|
while ca_bad == "" {
|
||||||
|
|
@ -9,8 +9,12 @@ function ca_product() -> CaVal {
|
||||||
let div = ca_s[ca_i] == 47
|
let div = ca_s[ca_i] == 47
|
||||||
ca_i += 1
|
ca_i += 1
|
||||||
let b = ca_factor()
|
let b = ca_factor()
|
||||||
|
if div and not b.bare and not a.bare and a.pct == 0.0 and b.pct == 0.0 and b.px != 0.0 {
|
||||||
|
a = ca_ratio(a.px / b.px)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if div and not b.bare {
|
if div and not b.bare {
|
||||||
ca_bad = "a plain number after /"
|
ca_bad = "a plain number, or a length under a length, after /"
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
if div and b.n == 0.0 {
|
if div and b.n == 0.0 {
|
||||||
|
|
@ -24,6 +28,12 @@ function ca_product() -> CaVal {
|
||||||
}
|
}
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
# a length over a length is a plain number (100vw / 1800px)
|
||||||
|
function ca_ratio(k: float) -> CaVal {
|
||||||
|
let r = new CaVal
|
||||||
|
r.n = k
|
||||||
|
return r
|
||||||
|
}
|
||||||
function ca_scale(a: CaVal, k: float) -> CaVal {
|
function ca_scale(a: CaVal, k: float) -> CaVal {
|
||||||
let r = new CaVal
|
let r = new CaVal
|
||||||
r.bare = a.bare
|
r.bare = a.bare
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,12 @@ function cs_tree(n: UiNode) -> void {
|
||||||
n.children = shown
|
n.children = shown
|
||||||
cs_order(n)
|
cs_order(n)
|
||||||
}
|
}
|
||||||
# twice: the font size first, from every rule, so an em anywhere reads the size the element ends with
|
# twice: the font size (and zoom) first, from every rule, so an em anywhere reads the size the
|
||||||
|
# element ends with (again when it zooms, so its own lengths are zoomed)
|
||||||
function cs_node(n: UiNode) -> void {
|
function cs_node(n: UiNode) -> void {
|
||||||
cs_pass = 1
|
cs_pass = 1
|
||||||
cs_styles(n)
|
cs_styles(n)
|
||||||
|
if zm_own(n) != 1.0 { cs_styles(n) }
|
||||||
cs_pass = 0
|
cs_pass = 0
|
||||||
cs_styles(n)
|
cs_styles(n)
|
||||||
an_apply(n)
|
an_apply(n)
|
||||||
|
|
@ -39,7 +41,7 @@ function cs_styles(n: UiNode) -> void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var cs_pass: int = 0
|
var cs_pass: int = 0
|
||||||
function cs_pass_ok(key: string) -> bool { return cs_pass == 0 or at_alias(key) == "size" }
|
function cs_pass_ok(key: string) -> bool { return cs_pass == 0 or at_alias(key) == "size" or key == "zoom" }
|
||||||
function cs_sheet(n: UiNode, sh: UiSheet) -> void {
|
function cs_sheet(n: UiNode, sh: UiSheet) -> void {
|
||||||
for i in 0 .. len(sh.rules) {
|
for i in 0 .. len(sh.rules) {
|
||||||
let r = sh.rules[i]
|
let r = sh.rules[i]
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ function ct_sound(n: UiNode) -> string {
|
||||||
function ct_activate(n: UiNode) -> void {
|
function ct_activate(n: UiNode) -> void {
|
||||||
if not n.enabled { return }
|
if not n.enabled { return }
|
||||||
let t = ct_type(n)
|
let t = ct_type(n)
|
||||||
if t == "checkbox" { ct_change(n, vl_bool(not ui_attr_on(n, "checked"))) } else if t == "radio" { ct_change(n, ui_attr(n, "value")) } else if t == "select" { ct_step(n, 1) } else if t == "key" { ct_key_start(n) } else if t == "number" { ct_num_commit(n) } else if t == "" {
|
if t == "checkbox" { ct_change(n, vl_bool(not ui_attr_on(n, "checked"))) } else if t == "radio" { ct_change(n, ui_attr(n, "value")) } else if t == "select" { ct_step(n, 1) } else if t == "key" { ct_key_start(n) } else if t == "number" { ct_num_commit(n) } else if t == "text" { ct_submit(n) } else if t == "" {
|
||||||
ui_sound(ct_sound(n))
|
ui_sound(ct_sound(n))
|
||||||
ui_fire(n, "press", Value.null())
|
ui_fire(n, "press", Value.null())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,3 +44,9 @@ function ct_fixed(f: float, places: int) -> string {
|
||||||
if neg and whole != 0 { s = "-" + s }
|
if neg and whole != 0 { s = "-" + s }
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
# Enter (or A) on a text field: on-submit with what it holds; the focus stays, and the text too
|
||||||
|
# unless it says clear-on-submit
|
||||||
|
function ct_submit(n: UiNode) -> void {
|
||||||
|
ui_fire(n, "submit", ui_attr(n, "value"))
|
||||||
|
if ui_attr_on(n, "clear-on-submit") { ui_fire(n, "change", Value.str("")) }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ export function ui_show(screen: string, view: UiView, x: float, y: float, w: flo
|
||||||
sc_update(root)
|
sc_update(root)
|
||||||
fr_pointer(root)
|
fr_pointer(root)
|
||||||
fc_keys(root)
|
fc_keys(root)
|
||||||
|
hd_tick(root)
|
||||||
tt_track(root, in_now.x, in_now.y)
|
tt_track(root, in_now.x, in_now.y)
|
||||||
|
fb_keep(screen, root)
|
||||||
if ui_be != null and ui_be.text != null {
|
if ui_be != null and ui_be.text != null {
|
||||||
fr_draw(root)
|
fr_draw(root)
|
||||||
pp_draw_top()
|
pp_draw_top()
|
||||||
|
|
|
||||||
44
packages/ludic.ui/frame_box.ludic
Normal file
44
packages/ludic.ui/frame_box.ludic
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# frame_box.ludic - what a host asks of the interface from outside it: the scale it is drawn at, and
|
||||||
|
# where an element was laid out the last time its screen was shown (a 3D view framed beside a panel)
|
||||||
|
export property UiBox {
|
||||||
|
x: float = 0.0
|
||||||
|
y: float = 0.0
|
||||||
|
w: float = 0.0
|
||||||
|
h: float = 0.0
|
||||||
|
}
|
||||||
|
var fb_roots: []UiNode = new []UiNode
|
||||||
|
# the screen pixels per design pixel: the renderer's scale (render3d's includes ui_render3d_scale)
|
||||||
|
export function ui_scale() -> float { return ui_px(1.0) }
|
||||||
|
function fb_keep(screen: string, root: UiNode) -> void {
|
||||||
|
for i in 0 .. len(fb_roots) {
|
||||||
|
if fb_roots[i].key == screen {
|
||||||
|
fb_roots[i] = root
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
push(fb_roots, root)
|
||||||
|
}
|
||||||
|
# the box of the element with that id (or key) on any screen shown, in screen pixels; null if none
|
||||||
|
export function ui_box(id: string) -> UiBox {
|
||||||
|
for i in 0 .. len(fb_roots) {
|
||||||
|
let n = fb_find(fb_roots[i], id)
|
||||||
|
if n != null {
|
||||||
|
let b = new UiBox
|
||||||
|
b.x = n.x
|
||||||
|
b.y = n.y
|
||||||
|
b.w = n.cw
|
||||||
|
b.h = n.ch
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
function fb_find(n: UiNode, id: string) -> UiNode {
|
||||||
|
if n.id == id or n.key == id { return n }
|
||||||
|
if n.children == null { return null }
|
||||||
|
for i in 0 .. len(n.children) {
|
||||||
|
let f = fb_find(n.children[i], id)
|
||||||
|
if f != null { return f }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
28
packages/ludic.ui/hold.ludic
Normal file
28
packages/ludic.ui/hold.ludic
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# hold.ludic - on-hold: every frame a control is held down - by the pointer (wherever it goes until it
|
||||||
|
# is let go), or by Enter or the pad's A on the focus - with event.dt, the seconds since the last
|
||||||
|
# frame, and event.t, how long it has been held, on the ui clock
|
||||||
|
var hd_key: string = ""
|
||||||
|
var hd_t0: float = 0.0
|
||||||
|
var hd_last: float = 0.0
|
||||||
|
function hd_tick(root: UiNode) -> void {
|
||||||
|
var key = fr_key_press
|
||||||
|
if fr_press_key != "" and in_now.down { key = fr_press_key }
|
||||||
|
if key == "" {
|
||||||
|
hd_key = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let now = an_now()
|
||||||
|
if key != hd_key {
|
||||||
|
hd_key = key
|
||||||
|
hd_t0 = now
|
||||||
|
hd_last = now
|
||||||
|
}
|
||||||
|
let n = fr_node(root, key)
|
||||||
|
if n != null {
|
||||||
|
let v = Value.object()
|
||||||
|
Value.put(v, "dt", Value.float(now - hd_last))
|
||||||
|
Value.put(v, "t", Value.float(now - hd_t0))
|
||||||
|
ui_fire(n, "hold", v)
|
||||||
|
}
|
||||||
|
hd_last = now
|
||||||
|
}
|
||||||
|
|
@ -63,6 +63,7 @@ import "ua.ludic"
|
||||||
import "anim.ludic"
|
import "anim.ludic"
|
||||||
import "transition.ludic"
|
import "transition.ludic"
|
||||||
import "units.ludic"
|
import "units.ludic"
|
||||||
|
import "zoom.ludic"
|
||||||
import "style.ludic"
|
import "style.ludic"
|
||||||
import "style_rule.ludic"
|
import "style_rule.ludic"
|
||||||
import "media.ludic"
|
import "media.ludic"
|
||||||
|
|
@ -81,6 +82,8 @@ import "layout_fit.ludic"
|
||||||
import "layout_run.ludic"
|
import "layout_run.ludic"
|
||||||
import "layout_wrap.ludic"
|
import "layout_wrap.ludic"
|
||||||
import "frame.ludic"
|
import "frame.ludic"
|
||||||
|
import "frame_box.ludic"
|
||||||
|
import "hold.ludic"
|
||||||
import "pointer.ludic"
|
import "pointer.ludic"
|
||||||
import "pointer_pick.ludic"
|
import "pointer_pick.ludic"
|
||||||
import "pointer_events.ludic"
|
import "pointer_events.ludic"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# measures its content at that width (so a wrapping row knows how tall it is), then places each
|
# measures its content at that width (so a wrapping row knows how tall it is), then places each
|
||||||
# child. A size is the border box: padding and border inside it, margin outside it.
|
# child. A size is the border box: padding and border inside it, margin outside it.
|
||||||
function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void {
|
function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void {
|
||||||
n.rsize = size
|
n.rsize = size * zm_own(n)
|
||||||
if n.size > 0.0 { n.rsize = n.size }
|
if n.size > 0.0 { n.rsize = n.size }
|
||||||
n.rfg = fg
|
n.rfg = fg
|
||||||
n.rfg_a = ly_fg_a
|
n.rfg_a = ly_fg_a
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ function nt_run(n: UiNode, name: string, value: Val) -> void {
|
||||||
for i in 0 .. len(n.on_names) {
|
for i in 0 .. len(n.on_names) {
|
||||||
if n.on_names[i] == name {
|
if n.on_names[i] == name {
|
||||||
var ev = Value.object()
|
var ev = Value.object()
|
||||||
if lk_in(PE_NAMES, name) and Value.kind(value) == 6 { ev = value }
|
if (lk_in(PE_NAMES, name) or name == "hold") and Value.kind(value) == 6 { ev = value }
|
||||||
Value.put(ev, "name", Value.str(name))
|
Value.put(ev, "name", Value.str(name))
|
||||||
Value.put(ev, "value", value)
|
Value.put(ev, "value", value)
|
||||||
let e = env_new(n.env)
|
let e = env_new(n.env)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ export property UiNodeX {
|
||||||
grad: UiGradient = null # background: linear-gradient(...)
|
grad: UiGradient = null # background: linear-gradient(...)
|
||||||
ratio: float = 0.0 # aspect-ratio, width over height; 0 none
|
ratio: float = 0.0 # aspect-ratio, width over height; 0 none
|
||||||
fit_obj: int = 0 # object-fit: 0 fill, 1 contain, 2 cover, 3 none, 4 scale-down
|
fit_obj: int = 0 # object-fit: 0 fill, 1 contain, 2 cover, 3 none, 4 scale-down
|
||||||
|
zoom: float = 1.0 # zoom: this element and everything inside it, larger or smaller
|
||||||
}
|
}
|
||||||
function nx(n: UiNode) -> UiNodeX {
|
function nx(n: UiNode) -> UiNodeX {
|
||||||
if n.ex == null { n.ex = new UiNodeX }
|
if n.ex == null { n.ex = new UiNodeX }
|
||||||
|
|
|
||||||
|
|
@ -10,21 +10,21 @@ var ly_vp_h: float = 0.0
|
||||||
function el_unit(s: string) -> float {
|
function el_unit(s: string) -> float {
|
||||||
let f = el_num(s)
|
let f = el_num(s)
|
||||||
let n = len(s)
|
let n = len(s)
|
||||||
if n > 3 and s[n - 3..n] == "rem" { return f * ui_px(16.0) }
|
if n > 3 and s[n - 3..n] == "rem" { return f * el_px(16.0) }
|
||||||
if n > 2 and s[n - 2..n] == "em" {
|
if n > 2 and s[n - 2..n] == "em" {
|
||||||
if el_cur == null { return f * ui_px(16.0) }
|
if el_cur == null { return f * el_px(16.0) }
|
||||||
if el_key == "size" and el_cur.parent != null { return f * cs_fs(el_cur.parent) }
|
if el_key == "size" and el_cur.parent != null { return f * cs_fs(el_cur.parent) }
|
||||||
return f * cs_fs(el_cur)
|
return f * cs_fs(el_cur)
|
||||||
}
|
}
|
||||||
if n > 2 and s[n - 2..n] == "vw" { return f * ly_vp_w / 100.0 }
|
if n > 2 and s[n - 2..n] == "vw" { return f * ly_vp_w / 100.0 }
|
||||||
if n > 2 and s[n - 2..n] == "vh" { return f * ly_vp_h / 100.0 }
|
if n > 2 and s[n - 2..n] == "vh" { return f * ly_vp_h / 100.0 }
|
||||||
return ui_px(f)
|
return el_px(f)
|
||||||
}
|
}
|
||||||
# the font size an element has so far in its cascade (its own, else its parent's)
|
# the font size an element has so far in its cascade (its own, else its parent's)
|
||||||
function cs_fs(n: UiNode) -> float {
|
function cs_fs(n: UiNode) -> float {
|
||||||
if n.size > 0.0 { return n.size }
|
if n.size > 0.0 { return n.size }
|
||||||
if n.parent != null { return cs_fs(n.parent) }
|
if n.parent != null { return cs_fs(n.parent) * zm_own(n) }
|
||||||
return ui_px(16.0)
|
return ui_px(16.0) * zm_own(n)
|
||||||
}
|
}
|
||||||
function cs_var_set(n: UiNode, name: string, value: string) -> void {
|
function cs_var_set(n: UiNode, name: string, value: string) -> void {
|
||||||
if n.var_names == null {
|
if n.var_names == null {
|
||||||
|
|
|
||||||
37
packages/ludic.ui/zoom.ludic
Normal file
37
packages/ludic.ui/zoom.ludic
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# zoom.ludic - zoom: 0.8 (or 80%, or calc(), min() and the rest: min(1, calc(100vw / 1800px))) makes
|
||||||
|
# an element and everything inside it that much larger or smaller, its lengths, its text and its
|
||||||
|
# layout; zooms multiply down the tree. A length in px is zoomed as it is read (el_px).
|
||||||
|
function el_zoom(n: UiNode, s0: string) -> void {
|
||||||
|
let s = tpl_words(s0)
|
||||||
|
var z = 1.0
|
||||||
|
if ca_is(s) {
|
||||||
|
if ca_eval(s) {
|
||||||
|
z = ca_num
|
||||||
|
if not ca_bare { ui_err(`zoom: {s} - a zoom is a plain number (divide a length by a length)`) }
|
||||||
|
}
|
||||||
|
} else if len(s) > 1 and s[len(s) - 1] == 37 { z = el_num(s) / 100.0 } else if s != "normal" { z = el_num(s) }
|
||||||
|
if z <= 0.0 { z = 1.0 }
|
||||||
|
nx(n).zoom = z
|
||||||
|
}
|
||||||
|
function zm_own(n: UiNode) -> float {
|
||||||
|
if n.ex == null { return 1.0 }
|
||||||
|
return n.ex.zoom
|
||||||
|
}
|
||||||
|
# every zoom from this element out
|
||||||
|
function zm_all(n: UiNode) -> float {
|
||||||
|
var z = 1.0
|
||||||
|
var at = n
|
||||||
|
while at != null {
|
||||||
|
z = z * zm_own(at)
|
||||||
|
at = at.parent
|
||||||
|
}
|
||||||
|
return z
|
||||||
|
}
|
||||||
|
# a length of `v` design pixels on the element being styled: the renderer's scale and every zoom
|
||||||
|
# around it (its own too, except while its zoom itself is read)
|
||||||
|
function el_px(v: float) -> float {
|
||||||
|
if el_cur == null { return ui_px(v) }
|
||||||
|
if el_key == "zoom" and el_cur.parent != null { return ui_px(v) * zm_all(el_cur.parent) }
|
||||||
|
if el_key == "zoom" { return ui_px(v) }
|
||||||
|
return ui_px(v) * zm_all(el_cur)
|
||||||
|
}
|
||||||
|
|
@ -776,6 +776,10 @@ function cmd_dev_test() -> int {
|
||||||
controller_case("library/ui_keycap", "", "listening 16711680 | 257 Right click -2 | 258 Middle click -2 | 256 Left click -2 | pressed 0", "ui_keycap.ludic (L11: a key field takes the left, right and middle mouse buttons, and is :capturing while it listens)")
|
controller_case("library/ui_keycap", "", "listening 16711680 | 257 Right click -2 | 258 Middle click -2 | 256 Left click -2 | pressed 0", "ui_keycap.ludic (L11: a key field takes the left, right and middle mouse buttons, and is :capturing while it listens)")
|
||||||
controller_case("library/ui_focus_tip", "", "b 14,38 | Drop it 27,45 | Give it away 27,77 | b 14,38", "ui_focus_tip.ludic (L11: a title shows for the keyboard's focus after half a second, and goes back to the pointer when it moves)")
|
controller_case("library/ui_focus_tip", "", "b 14,38 | Drop it 27,45 | Give it away 27,77 | b 14,38", "ui_focus_tip.ludic (L11: a title shows for the keyboard's focus after half a second, and goes back to the pointer when it moves)")
|
||||||
controller_case("library/ui_theme", "", "accent #ff0000 gap 7", "ui_theme.ludic (L11: a component with no stylesheet reads the theme's :root variables from the screen around it)")
|
controller_case("library/ui_theme", "", "accent #ff0000 gap 7", "ui_theme.ludic (L11: a component with no stylesheet reads the theme's :root variables from the screen around it)")
|
||||||
|
controller_case("library/ui_box", "", "scale 1.5 | panel 35,75 150x90 | missing 1", "ui_box.ludic (L11: ui_scale and ui_box, the interface's scale and an element's laid-out box, read from outside)")
|
||||||
|
controller_case("library/ui_submit", "", "[hi] chat '' | [hi][yo][Ada] chat '' name 'Ada'", "ui_submit.ludic (L11: on-submit on a text field by Enter or A, clear-on-submit, the focus kept)")
|
||||||
|
controller_case("library/ui_zoom", "", "plain 100 pad 10 text 16 h 20 | half 50 pad 5 text 8 h 10 | half of twice 100 | fit 50 em 16", "ui_zoom.ludic (L11: zoom scales a subtree's lengths, text and layout, multiplies, and takes min() and a length over a length)")
|
||||||
|
controller_case("library/ui_hold", "", "31 frames 500 ms t 500 | 121 frames 500 ms t 500 | pad 16 frames 250 ms t 250 | dududu", "ui_hold.ludic (L11: on-hold every frame a button is held, by the pointer or A, with dt and t on the clock at any rate)")
|
||||||
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_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_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)")
|
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