` sits where its position puts it
+# (absolutely, in its parent) and is drawn on the top layer, over everything. While one is up the
+# pointer and the keyboard are its: a press outside it, or Esc, closes it (on-close), and Tab
+# walks only its own controls.
+var pop_list: []UiNode = new []UiNode
+function pp_find(n: UiNode, out: []UiNode) -> void {
+ if n.popover {
+ push(out, n)
+ return
+ }
+ for i in 0 .. len(n.children) { pp_find(n.children[i], out) }
+}
+# the topmost popover open in this tree, or null
+function pp_top(root: UiNode) -> UiNode {
+ let all = new []UiNode
+ pp_find(root, all)
+ if len(all) == 0 { return null }
+ return all[len(all) - 1]
+}
+# the pointer against the popover: inside it is as usual; a press outside closes it and is spent
+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 }
+ ui_fire(top, "close", Value.null())
+ return true
+}
+function pp_draw_top() -> void {
+ let list = pop_list
+ pop_list = new []UiNode
+ for i in 0 .. len(list) { fr_draw(list[i]) }
+}
diff --git a/packages/ludic.ui/render3d.ludic b/packages/ludic.ui/render3d.ludic
index 66ed3df5..9ecb198e 100644
--- a/packages/ludic.ui/render3d.ludic
+++ b/packages/ludic.ui/render3d.ludic
@@ -1,5 +1,5 @@
# render3d.ludic - ludic.ui drawn with ludic.render3d's overlay. A program that draws with render3d
-# imports this and calls ui_render3d() once; its templates then draw in the overlay pass, sized in
+# imports this, and its templates draw in the overlay pass (ui_render3d() installs it again), sized in
# design pixels of a 1080-line screen (ui_render3d_scale sets the player's own interface size).
#

a texture, loaded once
#

a cell of an atlas the program named with ui_atlas
@@ -12,7 +12,14 @@ var r3u_tex_ids: []int = new []int
var r3u_atlas: []string = new []string
var r3u_atlas_tex: []int = new []int
var r3u_atlas_cols: []int = new []int
+var r3u_atlas_rows: []int = new []int
var r3u_atlas_names: [][]string = new [][]string
+var r3u_installed: bool = r3u_install()
+# importing this file is enough: it is the renderer from the program's first frame
+function r3u_install() -> bool {
+ ui_render3d()
+ return true
+}
export function ui_render3d() -> void {
let b = new UiBackend
b.rect = fn r3u_rect
@@ -30,11 +37,13 @@ export function ui_render3d() -> void {
}
# the player's interface size, 1.0 as designed
export function ui_render3d_scale(s: float) -> void { r3u_user = s }
-# a grid atlas: `names` are its cells, row by row, `cols` to a row;

draws one
-export function ui_atlas(prefix: string, tex: int, cols: int, names: []string) -> void {
+# a grid atlas: `names` are its cells, row by row, `cols` across and `rows` down;

draws one (by its name, or by its number: "item:12")
+export function ui_atlas(prefix: string, tex: int, cols: int, rows: int, names: []string) -> void {
push(r3u_atlas, prefix)
push(r3u_atlas_tex, tex)
push(r3u_atlas_cols, cols)
+ push(r3u_atlas_rows, Math.max(rows, 1))
push(r3u_atlas_names, names)
}
function r3u_now() -> float { return float(Time.now_us() / 1000) / 1000.0 }
diff --git a/packages/ludic.ui/render3d_image.ludic b/packages/ludic.ui/render3d_image.ludic
index e833f3e7..ee95a98a 100644
--- a/packages/ludic.ui/render3d_image.ludic
+++ b/packages/ludic.ui/render3d_image.ludic
@@ -37,17 +37,19 @@ function r3u_cell(k: int, name: string, x: float, y: float, w: float, h: float,
for i in 0 .. len(names) {
if names[i] == name { idx = i }
}
+ if idx < 0 and len(name) > 0 and name[0] >= 48 and name[0] <= 57 { idx = int(el_num(name)) }
if idx < 0 {
ui_err(`{r3u_atlas[k]}:{name}: the atlas has no such picture`)
return
}
let cols = r3u_atlas_cols[k]
+ let rows = r3u_atlas_rows[k]
let cx = idx % cols
let cy = idx / cols
let u0 = float(cx) / float(cols)
- let v0 = float(cy) / float(cols)
+ let v0 = float(cy) / float(rows)
let s = Math.min(w, h)
- ov_sub(r3u_atlas_tex[k], int(x + (w - s) / 2.0), int(y + (h - s) / 2.0), int(s), int(s), u0, v0, u0 + 1.0 / float(cols), v0 + 1.0 / float(cols), r3u_r(c), r3u_g(c), r3u_b(c), a)
+ ov_sub(r3u_atlas_tex[k], int(x + (w - s) / 2.0), int(y + (h - s) / 2.0), int(s), int(s), u0, v0, u0 + 1.0 / float(cols), v0 + 1.0 / float(rows), r3u_r(c), r3u_g(c), r3u_b(c), a)
}
# border-image: the texture's `slice`-pixel corners kept, the rest stretched, drawn `dst` wide
function r3u_nine(src: string, slice: float, x: float, y: float, w: float, h: float, dst: float, c: int, a: float) -> void {
diff --git a/packages/ludic.ui/tooltip.ludic b/packages/ludic.ui/tooltip.ludic
new file mode 100644
index 00000000..9c24a466
--- /dev/null
+++ b/packages/ludic.ui/tooltip.ludic
@@ -0,0 +1,47 @@
+# tooltip.ludic - an element's title="..." shown beside the pointer once it has rested on the
+# element for half a second, in a box styled by .ui-tooltip (the default sheet's, or a theme's)
+var tt_key: string = ""
+var tt_since: float = 0.0
+var tt_text: string = ""
+var tt_x: float = 0.0
+var tt_y: float = 0.0
+function tt_track(scope: UiNode, x: float, y: float) -> void {
+ let n = tt_under(scope, x, y)
+ if n == null {
+ tt_key = ""
+ return
+ }
+ if n.key != tt_key {
+ tt_key = n.key
+ tt_since = an_now()
+ }
+ tt_text = ui_tr(ui_attr_text(n, "title"))
+ tt_x = x
+ tt_y = y
+}
+# the deepest element under the point that has a title
+function tt_under(n: UiNode, x: float, y: float) -> UiNode {
+ if x < n.x or y < n.y or x >= n.x + n.cw or y >= n.y + n.ch { return null }
+ var i = len(n.children) - 1
+ while i >= 0 {
+ let t = tt_under(n.children[i], x, y)
+ if t != null { return t }
+ i -= 1
+ }
+ if n.akeys != null and ui_attr_text(n, "title") != "" { return n }
+ return null
+}
+function tt_draw(root: UiNode) -> void {
+ if tt_key == "" or tt_text == "" or an_now() - tt_since < 0.5 { return }
+ let n = ct_part(root, "ui-tooltip", "")
+ n.tag = "div"
+ n.parent = root
+ n.children = new []UiNode
+ push(n.children, ct_part(root, "ui-tooltip-text", tt_text))
+ cs_tree(n)
+ ly_measure(n, ly_vp_w * 0.4, ui_px(16.0), root.rfg)
+ let x = Math.min(tt_x + ui_px(16.0), ly_vp_x + ly_vp_w - n.mw)
+ let y = Math.min(tt_y + ui_px(20.0), ly_vp_y + ly_vp_h - n.mh)
+ ly_place(n, x, y, n.mw, n.mh)
+ fr_draw(n)
+}
diff --git a/packages/ludic.ui/ua.ludic b/packages/ludic.ui/ua.ludic
index a70b4ca4..e00b4588 100644
--- a/packages/ludic.ui/ua.ludic
+++ b/packages/ludic.ui/ua.ludic
@@ -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-track { 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-right: -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 }"
+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-track { 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-right: -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 }"
diff --git a/selfhost/ludicc.seed.ll b/selfhost/ludicc.seed.ll
index 145fbd01..3c41ce39 100644
--- a/selfhost/ludicc.seed.ll
+++ b/selfhost/ludicc.seed.ll
@@ -12771,7 +12771,7 @@ declare void @hs_free(i32)
@.str12493 = private unnamed_addr constant [2 x i8] c" \00"
@.str12494 = private unnamed_addr constant [50 x i8] c" -Wl,/subsystem:windows -Wl,/entry:mainCRTStartup\00"
@.str12495 = private unnamed_addr constant [3 x i8] c" \22\00"
-@.str12496 = private unnamed_addr constant [55 x i8] c"build/cfree/selfhost.ludic:17806: index out of range: \00"
+@.str12496 = private unnamed_addr constant [55 x i8] c"build/cfree/selfhost.ludic:17808: index out of range: \00"
@.str12497 = private unnamed_addr constant [2 x i8] c"\22\00"
@.str12498 = private unnamed_addr constant [5 x i8] c" -o \00"
@.str12499 = private unnamed_addr constant [2 x i8] c"\22\00"
@@ -134235,6 +134235,8 @@ ifend89:
br label %ifend86
ifend86:
store i32 0, ptr @g_g_parsing
+ call void @fn_check_duplicate_fns()
+ call void @fn_check_duplicate_decls()
call void @fn_check_program()
call void @fn_emit_program()
%t230 = load i32, ptr %t3
diff --git a/selfhost/ludicc.win.seed.ll b/selfhost/ludicc.win.seed.ll
index 861c5e36..dd189847 100644
--- a/selfhost/ludicc.win.seed.ll
+++ b/selfhost/ludicc.win.seed.ll
@@ -13088,7 +13088,7 @@ entry:
@.str12493 = private unnamed_addr constant [2 x i8] c" \00"
@.str12494 = private unnamed_addr constant [50 x i8] c" -Wl,/subsystem:windows -Wl,/entry:mainCRTStartup\00"
@.str12495 = private unnamed_addr constant [3 x i8] c" \22\00"
-@.str12496 = private unnamed_addr constant [55 x i8] c"build/cfree/selfhost.ludic:17806: index out of range: \00"
+@.str12496 = private unnamed_addr constant [55 x i8] c"build/cfree/selfhost.ludic:17808: index out of range: \00"
@.str12497 = private unnamed_addr constant [2 x i8] c"\22\00"
@.str12498 = private unnamed_addr constant [5 x i8] c" -o \00"
@.str12499 = private unnamed_addr constant [2 x i8] c"\22\00"
@@ -134538,6 +134538,8 @@ ifend89:
br label %ifend86
ifend86:
store i32 0, ptr @g_g_parsing
+ call void @fn_check_duplicate_fns()
+ call void @fn_check_duplicate_decls()
call void @fn_check_program()
call void @fn_emit_program()
%t230 = load i32, ptr %t3
diff --git a/selfhost/main.ludic b/selfhost/main.ludic
index cdb5ae1e..6fac1896 100644
--- a/selfhost/main.ludic
+++ b/selfhost/main.ludic
@@ -203,6 +203,8 @@ entry {
else { g_windowed = has_target and has_systems() }
g_parsing = false # lowering errors now locate by statement, not token
+ check_duplicate_fns() # two declarations of one name: the cause, before its symptoms
+ check_duplicate_decls()
check_program() # L4: the types agree before anything is emitted
emit_program()
diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic
index 4aaf3490..3b6b1cad 100644
--- a/tools/ludic-cli/test.ludic
+++ b/tools/ludic-cli/test.ludic
@@ -759,6 +759,7 @@ function cmd_dev_test() -> int {
reject_case("rejected/component_no_template", "there is no Lonely.xml beside it", "a component needs its template beside it")
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)")
reject_case("rejected/view_untyped", "view Sums: say what type total is", "a view field that is not a bare global says its type")
reject_case("rejected/view_without_ui", "a view is shown by ludic.ui", "a view needs ludic.ui to show it")