diff --git a/LANGUAGE.md b/LANGUAGE.md index 3efda29a..9a1221d9 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -446,14 +446,20 @@ applies to both: Each reports with `on-change` and `event.value`, and plays `sound="..."` (or "click") through the program's `ui_sounds`. +- **Popovers, tooltips and bars.** + - `
` 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. + - `title="..."` shows a tooltip (`.ui-tooltip`) after the pointer rests for half a second. + - `` and `` fill (`.ui-fill`) as far as their value. - **Natives** are for what markup cannot say. `ui_native("minimap", measure, draw)` makes `` an element the program draws itself, laid out and styled like any other. It reports with `ui_fire(n, "change", value)` and reads its attributes with `ui_attr` / `ui_attr_on`. - **Renderers.** `ui_backend(b)` takes any renderer with `rect`, `text` and `measure`, and uses `round`, `ring`, `image`, `nine`, `clip`, `scale` and `now` when it has them. - `import "ludic.ui/render3d.ludic"` and `ui_render3d()` draw with render3d's overlay: - - images by path, cells of an atlas named with `ui_atlas(prefix, texture, cols, names)` as - ``, and nine-slices for `border-image`; + Importing `ludic.ui/render3d.ludic` makes render3d's overlay the renderer: + - images by path, and cells of an atlas named with `ui_atlas(prefix, texture, cols, rows, + names)` as `` (or `prefix:12`); + - nine-slices for `border-image`; - a scale from the screen's height (`ui_render3d_scale` for the player's interface size). `ui_translator(fn)`, `ui_sounds(fn)` and `ui_clock(fn)` hand the runtime the program's diff --git a/changes/defaults-views-templates.md b/changes/defaults-views-templates.md index 2cd6b82f..e5a1e6ed 100644 --- a/changes/defaults-views-templates.md +++ b/changes/defaults-views-templates.md @@ -48,6 +48,9 @@ The runtime is a UI framework, not only a template engine: - more CSS: `rgba()`/`#rrggbbaa`, `border-radius`, `outline`, `box-shadow`, `background-image`, `border-image`, group `opacity`, `@keyframes` / `animation` / `transition`; - HTML mixed content, and boolean attributes; +- `popover` (a top layer that keeps the pointer and keys, with light dismissal), `title` tooltips, + and `` / ``; +- importing `ludic.ui/render3d.ludic` installs the backend, and atlases take rows; - hooks for the program's language, sounds and clock. render3d gains `tex_width` / `tex_height`, and the XML reader keeps text runs among elements in @@ -63,3 +66,6 @@ Also: - An index is evaluated before the slice's elements are read. A `xs[f()]` whose `f` grew `xs` read stale memory. - A runtime error names the file its expression is in, not the program's. + +Two declarations with one name (a package's private global and a program's, say) are reported as +such before type checking. They used to surface as a page of type errors about the wrong type. diff --git a/examples/library/ui_popover.ludic b/examples/library/ui_popover.ludic new file mode 100644 index 00000000..ef1af5af --- /dev/null +++ b/examples/library/ui_popover.ludic @@ -0,0 +1,59 @@ +# ui_popover.ludic — HTML's popover and progress in ludic.ui: a button opens a popover of verbs, the +# keyboard stays inside it, a press outside or Esc closes it, and a progress bar fills to its value. +import "ludic.ui" +import "ui_popover_parts/Verbs.ludic" +program UiPopover { + numbers float + function frame(i: UiInput) -> UiNode { + ui_input(i) + ui_show("Verbs", null, 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes("Verbs", null) + ui_place(root, 0.0, 0.0, 400.0, 400.0) + return root + } + function click(x: float, y: float) -> void { + 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) + } + function said(root: UiNode) -> string { + let page = root.children[0] + return page.children[len(page.children) - 1].text + } + entry { + ui_backend(new UiBackend) + var root = frame(new UiInput) + let cell = root.children[0].children[0] + click(cell.x + 2.0, cell.y + 2.0) + root = frame(new UiInput) + let tab = new UiInput + tab.tab = true + frame(tab) + frame(tab) + root = frame(tab) + let enter = new UiInput + enter.enter = true + root = frame(enter) + root = frame(new UiInput) + let first = said(root) + click(cell.x + 2.0, cell.y + 2.0) + root = frame(new UiInput) + let opened = said(root) + click(390.0, 390.0) + root = frame(new UiInput) + let outside = said(root) + click(cell.x + 2.0, cell.y + 2.0) + let esc = new UiInput + esc.escape = true + frame(esc) + root = frame(new UiInput) + let bar = root.children[0].children[1] + print(`{first} | {opened} | {outside} | {said(root)} | fill {int(bar.children[0].cw)} of {int(bar.cw)}`) + } +} diff --git a/examples/library/ui_popover_parts/Verbs.lss b/examples/library/ui_popover_parts/Verbs.lss new file mode 100644 index 00000000..ae965783 --- /dev/null +++ b/examples/library/ui_popover_parts/Verbs.lss @@ -0,0 +1,3 @@ +.page { width: 300px; height: 200px; gap: 4px } +.verbs { top: 40px; left: 10px; width: 120px } +progress { width: 200px } diff --git a/examples/library/ui_popover_parts/Verbs.ludic b/examples/library/ui_popover_parts/Verbs.ludic new file mode 100644 index 00000000..090f1929 --- /dev/null +++ b/examples/library/ui_popover_parts/Verbs.ludic @@ -0,0 +1,6 @@ +# Verbs.ludic - a grid cell that opens a popover of verbs +component Verbs { + state open: bool = false + state used: string = "none" + state health: int = 30 +} diff --git a/examples/library/ui_popover_parts/Verbs.xml b/examples/library/ui_popover_parts/Verbs.xml new file mode 100644 index 00000000..34ec8bb4 --- /dev/null +++ b/examples/library/ui_popover_parts/Verbs.xml @@ -0,0 +1,11 @@ +
+ + +
+ + +
+
+ +

{used} {open}

+
diff --git a/packages/ludic.ui/controls_build.ludic b/packages/ludic.ui/controls_build.ludic index 2a606691..0b0a80c6 100644 --- a/packages/ludic.ui/controls_build.ludic +++ b/packages/ludic.ui/controls_build.ludic @@ -21,7 +21,21 @@ function ct_part(n: UiNode, cls: string, text: string) -> UiNode { } return p } +# and : a track with a fill as far as the value +function ct_build_bar(n: UiNode) -> void { + n.dir = UI_ROW + let lo = Value.as_float(ui_attr(n, "min")) + var hi = Value.as_float(ui_attr(n, "max")) + if hi <= lo { hi = lo + 1.0 } + let fill = ct_part(n, "ui-fill", "") + fill.w_pct = Math.clamp((Value.as_float(ui_attr(n, "value")) - lo) / (hi - lo), 0.0, 1.0) * 100.0 + push(n.children, fill) +} function ct_build(n: UiNode) -> void { + if n.tag == "progress" or n.tag == "meter" { + ct_build_bar(n) + return + } n.dir = UI_ROW n.focusable = true let t = ui_attr_text(n, "type") diff --git a/packages/ludic.ui/draw_order.ludic b/packages/ludic.ui/draw_order.ludic index 189f995c..15a4d7b5 100644 --- a/packages/ludic.ui/draw_order.ludic +++ b/packages/ludic.ui/draw_order.ludic @@ -29,20 +29,20 @@ function fr_clip_pop() -> void { function fr_children(n: UiNode) -> void { var positioned = 0 for i in 0 .. len(n.children) { - if n.children[i].pos == 0 { fr_draw(n.children[i]) } else { positioned += 1 } + if n.children[i].popover { push(pop_list, n.children[i]) } else if n.children[i].pos == 0 { fr_draw(n.children[i]) } else { positioned += 1 } } if positioned == 0 { return } var lo = 1000000 var hi = -1000000 for i in 0 .. len(n.children) { - if n.children[i].pos != 0 { + if n.children[i].pos != 0 and not n.children[i].popover { lo = Math.min(lo, n.children[i].z) hi = Math.max(hi, n.children[i].z) } } for z in lo .. hi + 1 { for i in 0 .. len(n.children) { - if n.children[i].pos != 0 and n.children[i].z == z { fr_draw(n.children[i]) } + if n.children[i].pos != 0 and not n.children[i].popover and n.children[i].z == z { fr_draw(n.children[i]) } } } } diff --git a/packages/ludic.ui/elem.ludic b/packages/ludic.ui/elem.ludic index eb6ef24b..4d7120e2 100644 --- a/packages/ludic.ui/elem.ludic +++ b/packages/ludic.ui/elem.ludic @@ -1,6 +1,6 @@ # elem.ludic - an element made from its template: what kind of node its tag is, its id, classes # and attributes (kept for selectors, applied after every rule), its text, and its children -const EL_BOXES: string = " col div section header footer nav main article aside ul ol li form box " +const EL_BOXES: string = " col div progress meter section header footer nav main article aside ul ol li form box " const EL_TEXTS: string = " text span p label h1 h2 h3 h4 h5 h6 strong em small b i a option " function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode { ui_at = k.where @@ -27,7 +27,7 @@ function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode { bd_kids(k.kids, e, key, n.children) if k.text != null { el_text(n, ui_tr(ui_eval_text(k.text, e))) } el_inline(n) - if k.tag == "input" or k.tag == "select" { ct_build(n) } + if k.tag == "input" or k.tag == "select" or k.tag == "progress" or k.tag == "meter" { ct_build(n) } return n } function el_kind(n: UiNode, tag: string) -> void { @@ -61,6 +61,12 @@ function el_keep(n: UiNode, key: string, v: Val) -> void { for i in 0 .. len(parts) { if len(parts[i]) > 0 { push(n.classes, parts[i]) } } + } else if key == "popover" { + n.popover = true + n.pos = 2 + push(n.akeys, key) + push(n.avals, s) + push(n.vals, v) } else if key == "tabindex" { n.focusable = s != "-1" } else if key == "hidden" { n.hidden = el_flag(v) } else if key == "disabled" { n.enabled = not el_flag(v) } else if key == "src" { n.src = s } else { if key == "enabled" { n.enabled = el_flag(v) } push(n.akeys, key) diff --git a/packages/ludic.ui/focus.ludic b/packages/ludic.ui/focus.ludic index 107adbca..7c9acfb1 100644 --- a/packages/ludic.ui/focus.ludic +++ b/packages/ludic.ui/focus.ludic @@ -27,7 +27,9 @@ function fc_move(step: int) -> void { # the keys, after the tree is laid out: moving, then acting on what has the focus function fc_keys(root: UiNode) -> void { fc_list = new []UiNode - fc_collect(root) + let top = pp_top(root) + if top != null and in_now.escape { ui_fire(top, "close", Value.null()) } + if top != null { fc_collect(top) } else { fc_collect(root) } let i = in_now if fc_find() < 0 { fc_autofocus() } let at = fc_find() diff --git a/packages/ludic.ui/frame.ludic b/packages/ludic.ui/frame.ludic index f78adc93..a909a33f 100644 --- a/packages/ludic.ui/frame.ludic +++ b/packages/ludic.ui/frame.ludic @@ -14,7 +14,11 @@ export function ui_show(screen: string, view: UiView, x: float, y: float, w: flo sc_update(root) fr_pointer(root) fc_keys(root) - if ui_be != null and ui_be.text != null { fr_draw(root) } + if ui_be != null and ui_be.text != null { + fr_draw(root) + pp_draw_top() + tt_draw(root) + } dv_after(screen, root) lc_run(screen) nt_fired_run() diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic index a106124d..dd32cff5 100644 --- a/packages/ludic.ui/index.ludic +++ b/packages/ludic.ui/index.ludic @@ -19,6 +19,8 @@ import "controls_build.ludic" import "input.ludic" import "focus.ludic" import "scroll.ludic" +import "popover.ludic" +import "tooltip.ludic" import "life.ludic" import "tpl.ludic" import "tpl_lines.ludic" diff --git a/packages/ludic.ui/link.ludic b/packages/ludic.ui/link.ludic index 28dba679..194b94d3 100644 --- a/packages/ludic.ui/link.ludic +++ b/packages/ludic.ui/link.ludic @@ -1,6 +1,6 @@ # link.ludic - each tag in a file that is not an element names a component: the file's own, an # unnamed import's exported one, or `as:Name` from the import called `as` -const LK_OWN: string = " row button input select textarea spacer scroll img hr if else each slot state style component screen import let fragment template provide #text " +const LK_OWN: string = " row button input select textarea progress meter spacer scroll img hr if else each slot state style component screen import let fragment template provide #text " function lib_link(f: UiFile, t: UiTpl) -> void { t.sheet = f.sheet if not lk_element(t.tag) { diff --git a/packages/ludic.ui/node.ludic b/packages/ludic.ui/node.ludic index 6fe7e884..cecd9108 100644 --- a/packages/ludic.ui/node.ludic +++ b/packages/ludic.ui/node.ludic @@ -103,5 +103,6 @@ export property UiNode { anim_loop: bool = false anim_alt: bool = false tr_dur: float = 0.0 # transition: how long a change of opacity takes + popover: bool = false # drawn on the top layer; a press outside it or Esc closes it } diff --git a/packages/ludic.ui/pointer.ludic b/packages/ludic.ui/pointer.ludic index 8e94ee95..2b21a7e4 100644 --- a/packages/ludic.ui/pointer.ludic +++ b/packages/ludic.ui/pointer.ludic @@ -9,9 +9,14 @@ function fr_pointer(root: UiNode) -> void { let i = in_now fr_down_now = i.down let keys = new []string - fr_under(root, i.x, i.y, keys) + let top = pp_top(root) + var scope = root + if top != null { scope = top } + fr_under(scope, i.x, i.y, keys) fr_hover = keys - let hit = fr_hit(root, i.x, i.y) + tt_track(scope, i.x, i.y) + if top != null and pp_pointer(top) { return } + let hit = fr_hit(scope, i.x, i.y) if i.down and not in_was_down { fr_press_key = "" if hit != null { diff --git a/packages/ludic.ui/popover.ludic b/packages/ludic.ui/popover.ludic new file mode 100644 index 00000000..0ba0a3f4 --- /dev/null +++ b/packages/ludic.ui/popover.ludic @@ -0,0 +1,32 @@ +# popover.ludic - HTML's popover: `
` 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")