diff --git a/LANGUAGE.md b/LANGUAGE.md index 999d883d..8b02aed9 100644 --- a/LANGUAGE.md +++ b/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 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 - 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 ``) takes the wheel, has a scrollbar that can be 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 @@ -482,7 +484,8 @@ applies to both: `unit=" s"` after it; - ``, 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`; - ``: 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 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; - 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 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"` @@ -568,6 +575,10 @@ applies to both: 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 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: ``, `

Hi there

`. A boolean attribute present with no value is true, as in HTML (`" + 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}`) + } +} diff --git a/examples/library/ui_submit.ludic b/examples/library/ui_submit.ludic new file mode 100644 index 00000000..491f7de8 --- /dev/null +++ b/examples/library/ui_submit.ludic @@ -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 = "" + 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}'`) + } +} diff --git a/examples/library/ui_zoom.ludic b/examples/library/ui_zoom.ludic new file mode 100644 index 00000000..923a8a1a --- /dev/null +++ b/examples/library/ui_zoom.ludic @@ -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 = "" + const BODY: string = "

a

b

" + entry { + ui_backend(new UiBackend) + ui_load_text("" + STYLE + BODY + "", "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)}`) + } +} diff --git a/packages/ludic.ui/attr_box.ludic b/packages/ludic.ui/attr_box.ludic index 613f41ed..06dca9a3 100644 --- a/packages/ludic.ui/attr_box.ludic +++ b/packages/ludic.ui/attr_box.ludic @@ -23,7 +23,7 @@ var el_s3: float = 0.0 # top right bottom left out of 1 to 4 lengths function el_sides(v: Val) -> void { 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)) for i in 0 .. len(words) { 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) 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) if s == "fill" { return UI_FILL } if ca_is(s) { return el_calc_len(s) } diff --git a/packages/ludic.ui/attr_edge.ludic b/packages/ludic.ui/attr_edge.ludic index 254e27f5..d4a324fb 100644 --- a/packages/ludic.ui/attr_edge.ludic +++ b/packages/ludic.ui/attr_edge.ludic @@ -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))]) 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_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])) } } # box-shadow: 6px 10px 0 rgba(0, 0, 0, 0.45) - x, y, a blur this renderer draws sharp, the colour diff --git a/packages/ludic.ui/attr_fit.ludic b/packages/ludic.ui/attr_fit.ludic index ddb88bdd..cde3db3d 100644 --- a/packages/ludic.ui/attr_fit.ludic +++ b/packages/ludic.ui/attr_fit.ludic @@ -1,5 +1,5 @@ -# attr_fit.ludic - aspect-ratio (16 / 9, or 1.5) and object-fit (fill, contain, cover, none, -# scale-down) for pictures and natives, and a list split at its top-level commas +# attr_fit.ludic - aspect-ratio (16 / 9, or 1.5), object-fit (fill, contain, cover, none, scale-down) +# 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 { if key == "aspect-ratio" { let parts = lss_split(s, 47) @@ -12,7 +12,7 @@ function el_fit_props(n: UiNode, key: string, s: string) -> bool { 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 } nx(n).fit_obj = k - } else { return false } + } else if key == "zoom" { el_zoom(n, s) } else { return false } return true } # "a, rgba(1, 2, 3, 0.5) 40%, b" -> its three parts diff --git a/packages/ludic.ui/calc.ludic b/packages/ludic.ui/calc.ludic index 99fa4041..10849836 100644 --- a/packages/ludic.ui/calc.ludic +++ b/packages/ludic.ui/calc.ludic @@ -6,6 +6,8 @@ var ca_i: int = 0 var ca_bad: string = "" 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_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) != "" } # calc(...) worked out into ca_px and ca_pct; false (with the reason said) when it cannot be function ca_eval(s: string) -> bool { @@ -21,7 +23,9 @@ function ca_eval(s: string) -> bool { } ca_px = v.px 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 } property CaVal { @@ -63,6 +67,6 @@ function ca_len(a: CaVal) -> CaVal { if not a.bare { return a } let r = new CaVal r.bare = false - r.px = ui_px(a.n) + r.px = el_px(a.n) return r } diff --git a/packages/ludic.ui/calc_terms.ludic b/packages/ludic.ui/calc_terms.ludic index 833cf460..d5c7cd5f 100644 --- a/packages/ludic.ui/calc_terms.ludic +++ b/packages/ludic.ui/calc_terms.ludic @@ -1,6 +1,6 @@ # 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 -# of a /), as CSS's does +# calc(), a minus in front; a product needs a plain number on one side, and a quotient a plain number +# 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 { var a = ca_factor() while ca_bad == "" { @@ -9,8 +9,12 @@ function ca_product() -> CaVal { let div = ca_s[ca_i] == 47 ca_i += 1 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 { - ca_bad = "a plain number after /" + ca_bad = "a plain number, or a length under a length, after /" return a } if div and b.n == 0.0 { @@ -24,6 +28,12 @@ function ca_product() -> CaVal { } 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 { let r = new CaVal r.bare = a.bare diff --git a/packages/ludic.ui/cascade.ludic b/packages/ludic.ui/cascade.ludic index 7649781d..b91a631d 100644 --- a/packages/ludic.ui/cascade.ludic +++ b/packages/ludic.ui/cascade.ludic @@ -16,10 +16,12 @@ function cs_tree(n: UiNode) -> void { n.children = shown 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 { cs_pass = 1 cs_styles(n) + if zm_own(n) != 1.0 { cs_styles(n) } cs_pass = 0 cs_styles(n) an_apply(n) @@ -39,7 +41,7 @@ function cs_styles(n: UiNode) -> void { } } 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 { for i in 0 .. len(sh.rules) { let r = sh.rules[i] diff --git a/packages/ludic.ui/controls.ludic b/packages/ludic.ui/controls.ludic index 73192673..eefd6c5f 100644 --- a/packages/ludic.ui/controls.ludic +++ b/packages/ludic.ui/controls.ludic @@ -23,7 +23,7 @@ function ct_sound(n: UiNode) -> string { function ct_activate(n: UiNode) -> void { if not n.enabled { return } 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_fire(n, "press", Value.null()) } diff --git a/packages/ludic.ui/controls_parts.ludic b/packages/ludic.ui/controls_parts.ludic index ddd6eeae..1205681d 100644 --- a/packages/ludic.ui/controls_parts.ludic +++ b/packages/ludic.ui/controls_parts.ludic @@ -44,3 +44,9 @@ function ct_fixed(f: float, places: int) -> string { if neg and whole != 0 { s = "-" + 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("")) } +} diff --git a/packages/ludic.ui/frame.ludic b/packages/ludic.ui/frame.ludic index 053b1a82..bfe61f2f 100644 --- a/packages/ludic.ui/frame.ludic +++ b/packages/ludic.ui/frame.ludic @@ -14,7 +14,9 @@ export function ui_show(screen: string, view: UiView, x: float, y: float, w: flo sc_update(root) fr_pointer(root) fc_keys(root) + hd_tick(root) tt_track(root, in_now.x, in_now.y) + fb_keep(screen, root) if ui_be != null and ui_be.text != null { fr_draw(root) pp_draw_top() diff --git a/packages/ludic.ui/frame_box.ludic b/packages/ludic.ui/frame_box.ludic new file mode 100644 index 00000000..2b67eb92 --- /dev/null +++ b/packages/ludic.ui/frame_box.ludic @@ -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 +} diff --git a/packages/ludic.ui/hold.ludic b/packages/ludic.ui/hold.ludic new file mode 100644 index 00000000..e1601db3 --- /dev/null +++ b/packages/ludic.ui/hold.ludic @@ -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 +} diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic index 5ce0a015..b2722b0e 100644 --- a/packages/ludic.ui/index.ludic +++ b/packages/ludic.ui/index.ludic @@ -63,6 +63,7 @@ import "ua.ludic" import "anim.ludic" import "transition.ludic" import "units.ludic" +import "zoom.ludic" import "style.ludic" import "style_rule.ludic" import "media.ludic" @@ -81,6 +82,8 @@ import "layout_fit.ludic" import "layout_run.ludic" import "layout_wrap.ludic" import "frame.ludic" +import "frame_box.ludic" +import "hold.ludic" import "pointer.ludic" import "pointer_pick.ludic" import "pointer_events.ludic" diff --git a/packages/ludic.ui/layout.ludic b/packages/ludic.ui/layout.ludic index 23745069..395fe941 100644 --- a/packages/ludic.ui/layout.ludic +++ b/packages/ludic.ui/layout.ludic @@ -2,7 +2,7 @@ # 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. 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 } n.rfg = fg n.rfg_a = ly_fg_a diff --git a/packages/ludic.ui/native.ludic b/packages/ludic.ui/native.ludic index df99e122..ce3839b0 100644 --- a/packages/ludic.ui/native.ludic +++ b/packages/ludic.ui/native.ludic @@ -65,7 +65,7 @@ function nt_run(n: UiNode, name: string, value: Val) -> void { for i in 0 .. len(n.on_names) { if n.on_names[i] == name { 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, "value", value) let e = env_new(n.env) diff --git a/packages/ludic.ui/node_more.ludic b/packages/ludic.ui/node_more.ludic index fa664376..7b871915 100644 --- a/packages/ludic.ui/node_more.ludic +++ b/packages/ludic.ui/node_more.ludic @@ -29,6 +29,7 @@ export property UiNodeX { grad: UiGradient = null # background: linear-gradient(...) 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 + zoom: float = 1.0 # zoom: this element and everything inside it, larger or smaller } function nx(n: UiNode) -> UiNodeX { if n.ex == null { n.ex = new UiNodeX } diff --git a/packages/ludic.ui/units.ludic b/packages/ludic.ui/units.ludic index 3fdab19e..403d2d52 100644 --- a/packages/ludic.ui/units.ludic +++ b/packages/ludic.ui/units.ludic @@ -10,21 +10,21 @@ var ly_vp_h: float = 0.0 function el_unit(s: string) -> float { let f = el_num(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 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) } 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] == "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) function cs_fs(n: UiNode) -> float { if n.size > 0.0 { return n.size } - if n.parent != null { return cs_fs(n.parent) } - return ui_px(16.0) + if n.parent != null { return cs_fs(n.parent) * zm_own(n) } + return ui_px(16.0) * zm_own(n) } function cs_var_set(n: UiNode, name: string, value: string) -> void { if n.var_names == null { diff --git a/packages/ludic.ui/zoom.ludic b/packages/ludic.ui/zoom.ludic new file mode 100644 index 00000000..3fdd0eae --- /dev/null +++ b/packages/ludic.ui/zoom.ludic @@ -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) +} diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index 3dde4476..0d32788c 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -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_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_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_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)")