From 7d8f34de1712a5058df7502edfa529a64be5e669 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Thu, 24 Sep 2026 19:54:14 +0300 Subject: [PATCH] feat(ui): flex-shrink, text that wraps beside its siblings, calc(), order, and 0 as a size - flex-shrink: a line whose children want more room than it has takes it back from those that shrink, in proportion to shrink times size and never below a min size, a fixed size or the content. A scroll box shrinks (and scrolls), and a box in a column shrinks as far as the scroll boxes inside it let it, so a list in a column takes the room its siblings leave with no height. `flex: grow shrink`, and `flex-shrink` by name. - A row wider than its room measures its texts (and boxes without a width) again in the room the rest leave them, so a line wraps beside an icon. - calc() with + - * / and brackets over px, %, em, rem, vw, vh, plain numbers and var(); a width or height keeps its percentage, taken of the room it is given. Lengths lists (padding, margin) keep a calc()'s spaces. - `order` places a box's children without touching the tree. - width: 0 and height: 0 are 0: an unset size is UI_AUTO now, not 0. - A component root that is itself a component takes each user's class, style and id in turn (the outermost's id wins), and the rules of all their sheets are weighed by specificity together; a user's rule wins a tie. The parent's sheet used to override the child's whatever its specificity, and a middle component's sheet was lost. - The default sheet lays .ui-track out as a row, so a range's fill is as tall as its track and a checked box's knob goes right; a range's thumb is centred on the fill's end. - ui_boxes.ludic. Co-Authored-By: Claude Opus 5.5 --- examples/library/ui_boxes.ludic | 25 ++++++ examples/library/ui_boxes_parts/Boxes.lss | 14 +++ examples/library/ui_boxes_parts/Boxes.ludic | 5 ++ examples/library/ui_boxes_parts/Boxes.xml | 12 +++ packages/ludic.ui/attr.ludic | 21 ++--- packages/ludic.ui/attr_box.ludic | 7 +- packages/ludic.ui/attr_more.ludic | 94 +++++++++++++++++++++ packages/ludic.ui/build_class.ludic | 12 ++- packages/ludic.ui/calc.ludic | 69 +++++++++++++++ packages/ludic.ui/calc_terms.ludic | 76 +++++++++++++++++ packages/ludic.ui/cascade.ludic | 10 ++- packages/ludic.ui/cascade_merge.ludic | 53 ++++++++++++ packages/ludic.ui/index.ludic | 7 ++ packages/ludic.ui/layout.ludic | 60 ++----------- packages/ludic.ui/layout_box.ludic | 86 +++++++++++++++++++ packages/ludic.ui/layout_run.ludic | 15 ++-- packages/ludic.ui/layout_shrink.ludic | 75 ++++++++++++++++ packages/ludic.ui/layout_text.ludic | 2 +- packages/ludic.ui/layout_wrap.ludic | 8 +- packages/ludic.ui/node.ludic | 9 +- packages/ludic.ui/node_more.ludic | 40 +++++++++ packages/ludic.ui/ua.ludic | 2 +- tools/ludic-cli/test.ludic | 1 + 23 files changed, 609 insertions(+), 94 deletions(-) create mode 100644 examples/library/ui_boxes.ludic create mode 100644 examples/library/ui_boxes_parts/Boxes.lss create mode 100644 examples/library/ui_boxes_parts/Boxes.ludic create mode 100644 examples/library/ui_boxes_parts/Boxes.xml create mode 100644 packages/ludic.ui/attr_more.ludic create mode 100644 packages/ludic.ui/calc.ludic create mode 100644 packages/ludic.ui/calc_terms.ludic create mode 100644 packages/ludic.ui/cascade_merge.ludic create mode 100644 packages/ludic.ui/layout_box.ludic create mode 100644 packages/ludic.ui/layout_shrink.ludic create mode 100644 packages/ludic.ui/node_more.ludic diff --git a/examples/library/ui_boxes.ludic b/examples/library/ui_boxes.ludic new file mode 100644 index 00000000..62d9f399 --- /dev/null +++ b/examples/library/ui_boxes.ludic @@ -0,0 +1,25 @@ +# ui_boxes.ludic — ludic.ui's flex and sizes: a scroll box in a column shrinks to the room its +# siblings leave (flex-shrink) and scrolls what it cannot show; text in a row wraps in the room left +# after the icon beside it; width and height 0 are 0; calc() mixes a percentage with lengths; order +# rearranges a row; and a range's track is a row, its fill as tall as the track. +import "ludic.ui" +import "ui_boxes_parts/Boxes.ludic" +program UiBoxes { + numbers float + entry { + ui_backend(new UiBackend) + ui_show("Boxes", null, 0.0, 0.0, 400.0, 600.0) + let root: UiNode = ui_nodes("Boxes", null) + ui_place(root, 0.0, 0.0, 400.0, 600.0) + let page = root.children[0] + let list = page.children[0].children[1] + let line = page.children[1] + let text = line.children[1] + let zero = page.children[2] + let half = page.children[3] + let ord = page.children[4] + let track = page.children[5].children[0] + let fill = track.children[0] + print(`list {int(list.ch)} of {int(list.content_h)} | text at {int(text.x)} {int(text.cw)} wide, {len(text.lines)} lines, row {int(line.ch)} | zero {int(zero.cw)}x{int(zero.ch)} | half {int(half.cw)}x{int(half.ch)} pad {int(half.pt)} {int(half.pr)} | order {ord.children[0].text}{ord.children[1].text}{ord.children[2].text} at {int(ord.children[2].x)} | fill {int(fill.cw)}x{int(fill.ch)} of {int(track.cw)}x{int(track.ch)}`) + } +} diff --git a/examples/library/ui_boxes_parts/Boxes.lss b/examples/library/ui_boxes_parts/Boxes.lss new file mode 100644 index 00000000..30bfda09 --- /dev/null +++ b/examples/library/ui_boxes_parts/Boxes.lss @@ -0,0 +1,14 @@ +.page { width: 300px; gap: 4px } +.panel { height: 200px; gap: 10px } +.bar { height: 30px } +.list { overflow: auto } +.item { height: 20px } +.line { width: 200px; gap: 8px } +.line img { width: 24px; height: 24px } +.zero { width: 0; height: 0 } +.half { --gap: 10px; width: calc(50% - var(--gap)); height: calc(2 * var(--gap) + 1em); padding: calc(1em / 4) 2px } +.ordered { gap: 0 } +.a { order: 1 } +.b { order: 2 } +.c { order: 3 } +.ui-track { height: 12px; padding: 2px } diff --git a/examples/library/ui_boxes_parts/Boxes.ludic b/examples/library/ui_boxes_parts/Boxes.ludic new file mode 100644 index 00000000..e48029a5 --- /dev/null +++ b/examples/library/ui_boxes_parts/Boxes.ludic @@ -0,0 +1,5 @@ +# Boxes.ludic - a panel with a list that takes the room its header and footer leave, a line of text +# beside an icon, zero sizes, calc(), order, and a range's track +component Boxes { + state rows: int = 10 +} diff --git a/examples/library/ui_boxes_parts/Boxes.xml b/examples/library/ui_boxes_parts/Boxes.xml new file mode 100644 index 00000000..1f25e02c --- /dev/null +++ b/examples/library/ui_boxes_parts/Boxes.xml @@ -0,0 +1,12 @@ +
+
+

Head

+

{r}

+

Foot

+
+

A line long enough that it has to wrap beside the icon

+

hidden

+
+ cab + +
diff --git a/packages/ludic.ui/attr.ludic b/packages/ludic.ui/attr.ludic index 3f04417b..ae226792 100644 --- a/packages/ludic.ui/attr.ludic +++ b/packages/ludic.ui/attr.ludic @@ -1,7 +1,7 @@ # attr.ludic - what a property sets, by its CSS name or the short one (width or w, background or # bg, font-size or size, ...). An attribute on an element is the same property, applied last. -const AT_ALIAS: string = " width=w height=h min-width=min-w max-width=max-w min-height=min-h max-height=max-h padding=pad background=bg background-color=bg font-size=size opacity=alpha flex-grow=grow flex-direction=dir flex-wrap=wrap justify-content=justify align-items=align align-self=self row-gap=gap column-gap=gap text-align=talign " -const AT_UNDRAWN: string = " font-weight font-family font-style box-shadow text-shadow cursor transition animation line-height letter-spacing text-decoration " +const AT_ALIAS: string = " width=w height=h min-width=min-w max-width=max-w min-height=min-h max-height=max-h padding=pad background=bg background-color=bg font-size=size opacity=alpha flex-grow=grow flex-shrink=shrink flex-direction=dir flex-wrap=wrap justify-content=justify align-items=align align-self=self row-gap=gap column-gap=gap text-align=talign " +const AT_UNDRAWN: string = " font-weight font-family font-style box-shadow cursor transition animation line-height letter-spacing text-decoration " # a stylesheet's property: one that means nothing is said function el_attr(n: UiNode, key: string, v: Val) -> void { if el_apply(n, key, v) { return } @@ -17,7 +17,7 @@ function el_apply(n: UiNode, key: string, v0: Val) -> bool { return true } let v = cs_vars_in(n, v0) - return el_size(n, k, v) or el_box(n, k, v) or el_flex(n, k, v) or el_look(n, k, v) or el_place_prop(n, k, v) or el_edge(n, k, v) + return el_size(n, k, v) or el_box(n, k, v) or el_flex(n, k, v) or el_look(n, k, v) or el_place_prop(n, k, v) or el_edge(n, k, v) or el_more(n, k, v) } function at_alias(key: string) -> string { let pat = ` {key}=` @@ -32,13 +32,7 @@ function at_alias(key: string) -> string { return key } function el_size(n: UiNode, key: string, v: Val) -> bool { - if key == "w" { - n.w_pct = el_pct(v) - n.w = el_len(v) - } else if key == "h" { - n.h_pct = el_pct(v) - n.h = el_len(v) - } else if key == "min-w" { n.min_w = el_len(v) } else if key == "max-w" { n.max_w = el_len(v) } else if key == "min-h" { n.min_h = el_len(v) } else if key == "max-h" { n.max_h = el_len(v) } else if key == "gap" { n.gap = el_len(v) } else { return false } + if key == "w" or key == "h" { el_dim(n, key == "w", v) } else if key == "min-w" { n.min_w = el_len(v) } else if key == "max-w" { n.max_w = el_len(v) } else if key == "min-h" { n.min_h = el_len(v) } else if key == "max-h" { n.max_h = el_len(v) } else if key == "gap" { n.gap = el_len(v) } else { return false } return true } function el_flex(n: UiNode, key: string, v: Val) -> bool { @@ -46,7 +40,7 @@ function el_flex(n: UiNode, key: string, v: Val) -> bool { if key == "dir" { n.dir = UI_COL if s == "row" { n.dir = UI_ROW } - } else if key == "grow" or key == "flex" { n.grow = el_num(s) } else if key == "wrap" { n.wrap = s == "wrap" or (vl_truthy(v) and s != "false" and s != "nowrap") } else if key == "align" { n.align = el_place(v) } else if key == "self" { n.self = el_place(v) } else if key == "justify" { n.justify = el_place(v) } else if key == "talign" { n.talign = el_place(v) } else if key == "display" { n.hidden = s == "none" } else { return false } + } else if key == "grow" { n.grow = el_num(s) } else if key == "flex" { el_flex_short(n, s) } else if key == "wrap" { n.wrap = s == "wrap" or (vl_truthy(v) and s != "false" and s != "nowrap") } else if key == "align" { n.align = el_place(v) } else if key == "self" { n.self = el_place(v) } else if key == "justify" { n.justify = el_place(v) } else if key == "talign" { n.talign = el_place(v) } else if key == "display" { n.hidden = s == "none" } else { return false } return true } function el_look(n: UiNode, key: string, v: Val) -> bool { @@ -55,7 +49,10 @@ function el_look(n: UiNode, key: string, v: Val) -> bool { n.fg = ui_color(s) n.fg_a = cl_alpha } else if key == "bg" { - if len(s) > 4 and s[0..4] == "url(" { n.bg_img = el_url(s) } else { + if len(s) > 4 and s[0..4] == "url(" { n.bg_img = el_url(s) } else if tpl_words(s) == "transparent" { + n.bg = 0 + n.bg_a = 0.0 + } else { n.bg = ui_color(s) n.bg_a = cl_alpha } diff --git a/packages/ludic.ui/attr_box.ludic b/packages/ludic.ui/attr_box.ludic index 06ec2734..613f41ed 100644 --- a/packages/ludic.ui/attr_box.ludic +++ b/packages/ludic.ui/attr_box.ludic @@ -24,9 +24,9 @@ var el_s3: float = 0.0 function el_sides(v: Val) -> void { let parts = new []float if vl_num(v) { push(parts, ui_px(Value.as_float(v))) } else { - let words = lss_split(vl_text(v), 32) + let words = el_words(vl_text(v)) for i in 0 .. len(words) { - if len(words[i]) > 0 { push(parts, el_unit(words[i])) } + if len(words[i]) > 0 { push(parts, el_word_len(words[i])) } } } while len(parts) < 4 { @@ -37,11 +37,12 @@ function el_sides(v: Val) -> void { el_s2 = parts[2] el_s3 = parts[3] } -# a length: a number, 10px, 1.5em, 2rem, 50vw, 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 { if vl_num(v) { return ui_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) } if s == "fit" or s == "auto" or (len(s) > 0 and s[len(s) - 1] == 37) { return 0.0 } return el_unit(s) } diff --git a/packages/ludic.ui/attr_more.ludic b/packages/ludic.ui/attr_more.ludic new file mode 100644 index 00000000..ada6055f --- /dev/null +++ b/packages/ludic.ui/attr_more.ludic @@ -0,0 +1,94 @@ +# attr_more.ludic - width and height (where 0 is 0 and calc() may hold a percentage), the flex +# shorthand, flex-shrink, order and text-shadow +function el_more(n: UiNode, key: string, v: Val) -> bool { + let s = vl_text(v) + if key == "shrink" { nx(n).shrink = Math.max(el_num(s), 0.0) } else if key == "order" { nx(n).order = int(el_num(s)) } else if key == "text-shadow" { el_text_shadow(n, s) } else { return false } + return true +} +# width or height: a length (0 included), fill, a percentage, calc(), or auto/fit (none) +function el_dim(n: UiNode, wide: bool, v: Val) -> void { + let s = vl_text(v) + var fixed = UI_AUTO + var pct = 0.0 + var off = 0.0 + if ca_is(s) { + if ca_eval(s) { + pct = ca_pct + off = ca_px + if pct <= 0.0 { fixed = Math.max(ca_px, 0.0) } + } + } else if s == "fill" { fixed = UI_FILL } else if len(s) > 1 and s[len(s) - 1] == 37 { + pct = el_num(s) + if pct <= 0.0 { fixed = 0.0 } + } else if s != "auto" and s != "fit" and s != "" { fixed = el_len(v) } + if pct <= 0.0 { off = 0.0 } + if n.ex != null or off != 0.0 { + if wide { nx(n).w_off = off } else { nx(n).h_off = off } + } + if wide { + n.w = fixed + n.w_pct = Math.max(pct, 0.0) + } else { + n.h = fixed + n.h_pct = Math.max(pct, 0.0) + } +} +# a length out of calc(); a percentage has nothing to be a share of here +function el_calc_len(s: string) -> float { + if not ca_eval(s) { return 0.0 } + if ca_pct != 0.0 { ui_err(`{el_key}: {s} - a percentage in calc() is for a width or a height`) } + return ca_px +} +# flex: 1 | flex: 1 0 | flex: 2 1 auto | flex: none - the grow, then the shrink +function el_flex_short(n: UiNode, s: string) -> void { + let parts = el_words(s) + if s == "none" { + n.grow = 0.0 + nx(n).shrink = 0.0 + return + } + if len(parts) > 0 { n.grow = el_num(parts[0]) } + if len(parts) > 1 and len(parts[1]) > 0 and parts[1][0] >= 48 and parts[1][0] <= 57 { nx(n).shrink = el_num(parts[1]) } +} +# words apart by spaces, a bracket's own spaces kept: "calc(1em + 2px) 4px" is two +function el_words(s: string) -> []string { + let out = new []string + var at = 0 + var depth = 0 + for i in 0 .. len(s) { + if s[i] == 40 { depth += 1 } + if s[i] == 41 { depth -= 1 } + if s[i] == 32 and depth == 0 { + if i > at { push(out, s[at..i]) } + at = i + 1 + } + } + if len(s) > at { push(out, s[at..len(s)]) } + return out +} +# text-shadow: 1px 2px 3px rgba(0, 0, 0, 0.6) - x, y, a blur drawn sharp, the colour; or none +function el_text_shadow(n: UiNode, s: string) -> void { + let x = nx(n) + x.tsh_set = true + x.tsh_c = -2 + if tpl_words(s) == "none" { return } + let parts = el_words(s) + var nums = 0 + var col = "" + for i in 0 .. len(parts) { + let p = parts[i] + if col == "" and ((p[0] >= 48 and p[0] <= 57) or p[0] == 45 or p[0] == 46) { + if nums == 0 { x.tsh_x = el_unit(p) } + if nums == 1 { x.tsh_y = el_unit(p) } + nums += 1 + } else if col == "" { col = p } else { col = col + " " + p } + } + if col == "" { col = "rgba(0,0,0,0.6)" } + x.tsh_c = ui_color(col) + x.tsh_a = cl_alpha +} +# one word of a list of lengths (padding: calc(1em / 4) 2px) +function el_word_len(w: string) -> float { + if ca_is(w) { return el_calc_len(w) } + return el_unit(w) +} diff --git a/packages/ludic.ui/build_class.ludic b/packages/ludic.ui/build_class.ludic index 7011212b..c9ddec9f 100644 --- a/packages/ludic.ui/build_class.ludic +++ b/packages/ludic.ui/build_class.ludic @@ -28,7 +28,8 @@ function bd_class(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void { bd_kids(root, ce, key, out) if len(out) > at { bd_pass_down(out[at], k, e) } } -# puts those on the card's own root element +# puts those on the card's own root element; when that +# root is itself a component, each user in turn adds theirs (the outermost's id wins) function bd_pass_down(n: UiNode, k: UiTpl, e: UiEnv) -> void { let c = tpl_attr(k, "class") if c != null { @@ -39,7 +40,10 @@ function bd_pass_down(n: UiNode, k: UiTpl, e: UiEnv) -> void { } let id = tpl_attr(k, "id") if id != null { n.id = ui_eval_text(id, e) } - if k.style != null { n.style2 = k.style } - n.sheet2 = k.sheet - n.env2 = e + let up = new UiPass + up.sheet = k.sheet + up.style = k.style + up.env = e + if n.ups == null { n.ups = new []UiPass } + push(n.ups, up) } diff --git a/packages/ludic.ui/calc.ludic b/packages/ludic.ui/calc.ludic new file mode 100644 index 00000000..f3f1a1c2 --- /dev/null +++ b/packages/ludic.ui/calc.ludic @@ -0,0 +1,69 @@ +# calc.ludic - CSS's calc(): + - * / and brackets over lengths (px, em, rem, vw, vh, or a bare +# number, which is px as everywhere here), percentages and plain numbers; var() is already read by +# then. A width or height keeps its percentage apart (w_pct plus an offset) to be taken of the room +# the element is given; anywhere else a percentage in calc() has nothing to be a share of. +var ca_s: string = "" +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 +function ca_is(s: string) -> bool { return len(s) > 5 and s[0..5] == "calc(" } +# calc(...) worked out into ca_px and ca_pct; false (with the reason said) when it cannot be +function ca_eval(s: string) -> bool { + ca_s = s[5..len(s)] + ca_i = 0 + ca_bad = "" + let v = ca_sum() + ca_ws() + if ca_bad == "" and (ca_i >= len(ca_s) or ca_s[ca_i] != 41) { ca_bad = "a closing )" } + if ca_bad != "" { + ui_err(`{s}: calc() expected {ca_bad}`) + return false + } + ca_px = v.px + ca_pct = v.pct + if v.bare { ca_px = ui_px(v.n) } + return true +} +property CaVal { + px: float = 0.0 + pct: float = 0.0 + n: float = 0.0 + bare: bool = true # a plain number, which may scale a length +} +function ca_ws() -> void { + while ca_i < len(ca_s) and (ca_s[ca_i] == 32 or ca_s[ca_i] == 9 or ca_s[ca_i] == 10) { ca_i += 1 } +} +function ca_sum() -> CaVal { + var a = ca_product() + while ca_bad == "" { + ca_ws() + if ca_i >= len(ca_s) or (ca_s[ca_i] != 43 and ca_s[ca_i] != 45) { return a } + let minus = ca_s[ca_i] == 45 + ca_i += 1 + let b = ca_product() + a = ca_add(a, b, minus) + } + return a +} +function ca_add(a: CaVal, b: CaVal, minus: bool) -> CaVal { + var sign = 1.0 + if minus { sign = -1.0 } + let r = new CaVal + if a.bare and b.bare { + r.n = a.n + sign * b.n + return r + } + r.bare = false + r.px = ca_len(a).px + sign * ca_len(b).px + r.pct = a.pct + sign * b.pct + return r +} +# a bare number beside a length is a length in px +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) + return r +} diff --git a/packages/ludic.ui/calc_terms.ludic b/packages/ludic.ui/calc_terms.ludic new file mode 100644 index 00000000..bae716c7 --- /dev/null +++ b/packages/ludic.ui/calc_terms.ludic @@ -0,0 +1,76 @@ +# 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 +function ca_product() -> CaVal { + var a = ca_factor() + while ca_bad == "" { + ca_ws() + if ca_i >= len(ca_s) or (ca_s[ca_i] != 42 and ca_s[ca_i] != 47) { return a } + let div = ca_s[ca_i] == 47 + ca_i += 1 + let b = ca_factor() + if div and not b.bare { + ca_bad = "a plain number after /" + return a + } + if div and b.n == 0.0 { + ca_bad = "a number that is not 0 after /" + return a + } + if div { a = ca_scale(a, 1.0 / b.n) } else if a.bare { a = ca_scale(b, a.n) } else if b.bare { a = ca_scale(a, b.n) } else { + ca_bad = "a plain number on one side of *" + return a + } + } + return a +} +function ca_scale(a: CaVal, k: float) -> CaVal { + let r = new CaVal + r.bare = a.bare + r.n = a.n * k + r.px = a.px * k + r.pct = a.pct * k + return r +} +function ca_factor() -> CaVal { + ca_ws() + if ca_i >= len(ca_s) { + ca_bad = "a value" + return new CaVal + } + let c = ca_s[ca_i] + if c == 45 { + ca_i += 1 + return ca_scale(ca_factor(), -1.0) + } + if c == 40 or (ca_i + 5 <= len(ca_s) and ca_s[ca_i..ca_i + 5] == "calc(") { + while ca_s[ca_i] != 40 { ca_i += 1 } + ca_i += 1 + let v = ca_sum() + ca_ws() + if ca_i < len(ca_s) and ca_s[ca_i] == 41 { ca_i += 1 } else if ca_bad == "" { ca_bad = "a closing )" } + return v + } + return ca_number() +} +# 12, 1.5em, 50%, 20vw +function ca_number() -> CaVal { + let at = ca_i + while ca_i < len(ca_s) and ((ca_s[ca_i] >= 48 and ca_s[ca_i] <= 57) or ca_s[ca_i] == 46) { ca_i += 1 } + let num_end = ca_i + while ca_i < len(ca_s) and ((ca_s[ca_i] >= 97 and ca_s[ca_i] <= 122) or ca_s[ca_i] == 37) { ca_i += 1 } + let r = new CaVal + if num_end == at { + ca_bad = "a number" + return r + } + let word: string = ca_s[at..ca_i] + let unit: string = ca_s[num_end..ca_i] + if unit == "" { + r.n = el_num(word) + return r + } + r.bare = false + if unit == "%" { r.pct = el_num(word) } else if unit == "px" or unit == "em" or unit == "rem" or unit == "vw" or unit == "vh" { r.px = el_unit(word) } else { ca_bad = `a unit calc() knows, not {unit}` } + return r +} diff --git a/packages/ludic.ui/cascade.ludic b/packages/ludic.ui/cascade.ludic index 65536555..b06ff048 100644 --- a/packages/ludic.ui/cascade.ludic +++ b/packages/ludic.ui/cascade.ludic @@ -14,13 +14,17 @@ function cs_tree(n: UiNode) -> void { if not n.children[i].hidden { push(shown, n.children[i]) } } n.children = shown + cs_order(n) } function cs_node(n: UiNode) -> void { cs_sheet(n, ua_sheet()) - if n.sheet != null { cs_sheet(n, n.sheet) } - if n.sheet2 != null and n.sheet2 != n.sheet { cs_sheet(n, n.sheet2) } + if n.ups != null { cs_merged(n) } else if n.sheet != null { cs_sheet(n, n.sheet) } if n.style != null { cs_rule(n, n.style) } - if n.style2 != null { cs_rule_in(n, n.style2, n.env2) } + if n.ups != null { + for i in 0 .. len(n.ups) { + if n.ups[i].style != null { cs_rule_in(n, n.ups[i].style, n.ups[i].env) } + } + } for i in 0 .. len(n.akeys) { el_apply(n, n.akeys[i], n.vals[i]) } an_apply(n) tr_apply(n) diff --git a/packages/ludic.ui/cascade_merge.ludic b/packages/ludic.ui/cascade_merge.ludic new file mode 100644 index 00000000..0f5f6bea --- /dev/null +++ b/packages/ludic.ui/cascade_merge.ludic @@ -0,0 +1,53 @@ +# cascade_merge.ludic - a component's root is styled by its own sheet and by the sheet of every user +# that passed a class down to it, all at once: the rules that match, from all of them, ordered by +# specificity as one sheet's are. On a tie the user's rule wins (the outermost user's last), as a +# later rule does. Then `order`, which rearranges a flex box's children without touching the tree. +function cs_merged(n: UiNode) -> void { + let rules = new []UiRule + let envs = new []UiEnv + let seen = new []UiSheet + if n.sheet != null { cs_gather(n, n.sheet, n.env, rules, envs, seen) } + for i in 0 .. len(n.ups) { + if n.ups[i].sheet != null { cs_gather(n, n.ups[i].sheet, n.ups[i].env, rules, envs, seen) } + } + for i in 0 .. len(rules) { cs_rule_in(n, rules[i], envs[i]) } +} +# a sheet's matching rules into the list, each after every rule at most as specific (stable) +function cs_gather(n: UiNode, sh: UiSheet, e: UiEnv, rules: []UiRule, envs: []UiEnv, seen: []UiSheet) -> void { + for i in 0 .. len(seen) { + if seen[i] == sh { return } + } + push(seen, sh) + for i in 0 .. len(sh.rules) { + let r = sh.rules[i] + if md_ok(r) and sel_match(r.sel, n) { + push(rules, r) + push(envs, e) + var j = len(rules) - 1 + while j > 0 and rules[j - 1].spec > r.spec { + rules[j] = rules[j - 1] + envs[j] = envs[j - 1] + j -= 1 + } + rules[j] = r + envs[j] = e + } + } +} +# CSS's order: the children placed by it, lowest first, the tree's order kept between equals +function cs_order(n: UiNode) -> void { + var any = false + for i in 0 .. len(n.children) { + if ly_order(n.children[i]) != 0 { any = true } + } + if not any { return } + for i in 1 .. len(n.children) { + let c = n.children[i] + var j = i + while j > 0 and ly_order(n.children[j - 1]) > ly_order(c) { + n.children[j] = n.children[j - 1] + j -= 1 + } + n.children[j] = c + } +} diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic index 19a4fa3f..e4e2c019 100644 --- a/packages/ludic.ui/index.ludic +++ b/packages/ludic.ui/index.ludic @@ -4,6 +4,7 @@ module ludic_ui numbers float import "consts.ludic" import "node.ludic" +import "node_more.ludic" import "view.ludic" import "backend.ludic" import "expr.ludic" @@ -42,6 +43,9 @@ import "attr_box.ludic" import "attr_border.ludic" import "attr_place.ludic" import "attr_edge.ludic" +import "attr_more.ludic" +import "calc.ludic" +import "calc_terms.ludic" import "ua.ludic" import "anim.ludic" import "transition.ludic" @@ -53,7 +57,10 @@ import "select.ludic" import "select_parts.ludic" import "select_match.ludic" import "cascade.ludic" +import "cascade_merge.ludic" import "layout.ludic" +import "layout_box.ludic" +import "layout_shrink.ludic" import "layout_text.ludic" import "layout_run.ludic" import "layout_wrap.ludic" diff --git a/packages/ludic.ui/layout.ludic b/packages/ludic.ui/layout.ludic index ce2034de..d65aad05 100644 --- a/packages/ludic.ui/layout.ludic +++ b/packages/ludic.ui/layout.ludic @@ -28,18 +28,19 @@ function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void { n.mh = n.mh + n.rsize } var fixed_w = n.w - if n.w_pct > 0.0 { fixed_w = avail * n.w_pct / 100.0 } + if n.w_pct > 0.0 { fixed_w = ly_pw(n, avail) } if (n.kind == UI_BOX or n.kind == UI_SCROLL) and n.children != null { var inner = avail - n.ml - n.mr - if fixed_w > 0.0 { inner = fixed_w } + if fixed_w >= 0.0 { inner = fixed_w } inner = ly_clamp(inner, n.min_w, n.max_w) - ly_hin(n) if n.dir == UI_ROW and n.wrap { ly_measure_wrap(n, inner) } else { ly_measure_box(n, inner) } } n.content_h = n.mh - if fixed_w > 0.0 { n.mw = fixed_w } - if n.h > 0.0 { n.mh = n.h } + if fixed_w >= 0.0 { n.mw = fixed_w } + if n.h >= 0.0 { n.mh = n.h } n.mw = ly_clamp(n.mw, n.min_w, n.max_w) n.mh = ly_clamp(n.mh, n.min_h, n.max_h) + ly_min_note(n) } var ly_fg_a: float = 1.0 function ly_hin(n: UiNode) -> float { return n.pl + n.pr + n.bl + n.br } @@ -50,54 +51,3 @@ function ly_clamp(v: float, lo: float, hi: float) -> float { if out < lo { out = lo } return out } -function ly_measure_box(n: UiNode, inner: float) -> void { - var main = 0.0 - var cross = 0.0 - var count = 0 - for i in 0 .. len(n.children) { - let c = n.children[i] - ly_fg_a = n.rfg_a - ly_measure(c, inner, n.rsize, n.rfg) - if c.pos < 2 { - main = main + ly_main(c, n.dir) - if count > 0 { main = main + n.gap } - cross = Math.max(cross, ly_cross(c, n.dir)) - count += 1 - } - } - if n.dir == UI_ROW { - n.mw = n.mw + main - n.mh = n.mh + cross - } else { - n.mw = n.mw + cross - n.mh = n.mh + main - } -} -# a wrapping row: children in lines no wider than `inner`, the lines stacked `gap` apart -function ly_measure_wrap(n: UiNode, inner: float) -> void { - var line_w = 0.0 - var line_h = 0.0 - var widest = 0.0 - var total = 0.0 - var count = 0 - for i in 0 .. len(n.children) { - let c = n.children[i] - ly_fg_a = n.rfg_a - ly_measure(c, inner, n.rsize, n.rfg) - if c.pos >= 2 { continue } - let cw = ly_main(c, UI_ROW) - if count > 0 and line_w + n.gap + cw > inner { - widest = Math.max(widest, line_w) - total = total + line_h + n.gap - line_w = 0.0 - line_h = 0.0 - count = 0 - } - if count > 0 { line_w = line_w + n.gap } - line_w = line_w + cw - line_h = Math.max(line_h, ly_cross(c, UI_ROW)) - count += 1 - } - n.mw = n.mw + Math.max(widest, line_w) - n.mh = n.mh + total + line_h -} diff --git a/packages/ludic.ui/layout_box.ludic b/packages/ludic.ui/layout_box.ludic new file mode 100644 index 00000000..de68db28 --- /dev/null +++ b/packages/ludic.ui/layout_box.ludic @@ -0,0 +1,86 @@ +# layout_box.ludic - a box's content measured: its children in a line along its direction (a +# row's text wrapped into the room its siblings leave), or a wrapping row's lines +function ly_measure_box(n: UiNode, inner: float) -> void { + for i in 0 .. len(n.children) { + ly_fg_a = n.rfg_a + ly_measure(n.children[i], inner, n.rsize, n.rfg) + } + if n.dir == UI_ROW and inner > 0.0 { ly_row_fit(n, inner) } + var main = 0.0 + var cross = 0.0 + var count = 0 + for i in 0 .. len(n.children) { + let c = n.children[i] + if c.pos < 2 { + main = main + ly_main(c, n.dir) + if count > 0 { main = main + n.gap } + cross = Math.max(cross, ly_cross(c, n.dir)) + count += 1 + } + } + if n.dir == UI_ROW { + n.mw = n.mw + main + n.mh = n.mh + cross + } else { + n.mw = n.mw + cross + n.mh = n.mh + main + } +} +# a row wider than its room: its texts (and boxes of their own width) measured again in the room the +# rest leave them, each giving up a share of the excess as wide as it is, so a line wraps beside an icon +function ly_row_fit(n: UiNode, inner: float) -> void { + var used = 0.0 + var flex = 0.0 + var count = 0 + for i in 0 .. len(n.children) { + let c = n.children[i] + if c.pos >= 2 { continue } + if count > 0 { used = used + n.gap } + used = used + ly_main(c, UI_ROW) + if ly_flexes(c) { flex = flex + ly_main(c, UI_ROW) } + count += 1 + } + let over = used - inner + if over <= 0.0 or flex <= 0.0 { return } + for i in 0 .. len(n.children) { + let c = n.children[i] + let share = ly_main(c, UI_ROW) - over * ly_main(c, UI_ROW) / flex + if ly_flexes(c) and share > 0.0 { + ly_fg_a = n.rfg_a + ly_measure(c, share, n.rsize, n.rfg) + } + } +} +function ly_flexes(c: UiNode) -> bool { + if c.pos >= 2 or c.w >= 0.0 or c.w_pct > 0.0 { return false } + if c.kind == UI_TEXT { return not c.nowrap } + return c.kind == UI_BOX and c.children != null and len(c.children) > 0 +} +# a wrapping row: children in lines no wider than `inner`, the lines stacked `gap` apart +function ly_measure_wrap(n: UiNode, inner: float) -> void { + var line_w = 0.0 + var line_h = 0.0 + var widest = 0.0 + var total = 0.0 + var count = 0 + for i in 0 .. len(n.children) { + let c = n.children[i] + ly_fg_a = n.rfg_a + ly_measure(c, inner, n.rsize, n.rfg) + if c.pos >= 2 { continue } + let cw = ly_main(c, UI_ROW) + if count > 0 and line_w + n.gap + cw > inner { + widest = Math.max(widest, line_w) + total = total + line_h + n.gap + line_w = 0.0 + line_h = 0.0 + count = 0 + } + if count > 0 { line_w = line_w + n.gap } + line_w = line_w + cw + line_h = Math.max(line_h, ly_cross(c, UI_ROW)) + count += 1 + } + n.mw = n.mw + Math.max(widest, line_w) + n.mh = n.mh + total + line_h +} diff --git a/packages/ludic.ui/layout_run.ludic b/packages/ludic.ui/layout_run.ludic index ed581d02..e972be48 100644 --- a/packages/ludic.ui/layout_run.ludic +++ b/packages/ludic.ui/layout_run.ludic @@ -28,8 +28,8 @@ function ly_grow(c: UiNode, dir: int) -> float { # where a child starts along the run: `fill` from nothing, a percentage from the room it is in function ly_basis(c: UiNode, dir: int, room: float) -> float { if ly_fills(c, dir) { return 0.0 } - if dir == UI_ROW and c.w_pct > 0.0 { return room * c.w_pct / 100.0 + c.ml + c.mr } - if dir == UI_COL and c.h_pct > 0.0 { return room * c.h_pct / 100.0 + c.mt + c.mb } + if dir == UI_ROW and c.w_pct > 0.0 { return ly_pw(c, room) + c.ml + c.mr } + if dir == UI_COL and c.h_pct > 0.0 { return ly_ph(c, room) + c.mt + c.mb } return ly_main(c, dir) } function ly_stretches(n: UiNode, c: UiNode) -> bool { @@ -43,22 +43,25 @@ function ly_place_line(n: UiNode, from: int, to: int, top: float, main_room: flo var used = 0.0 var grows = 0.0 var count = 0 + let sizes = new []float for i in from .. to { + push(sizes, ly_basis(n.children[i], n.dir, main_room)) if n.children[i].pos < 2 { - used = used + ly_basis(n.children[i], n.dir, main_room) + used = used + sizes[i - from] grows = grows + ly_grow(n.children[i], n.dir) count += 1 } } used = used + n.gap * float(Math.max(count - 1, 0)) let free = main_room - used + if free < 0.0 { ly_shrink_line(n, from, sizes, 0.0 - free) } ly_justify(n, free, count, grows) let extra = ly_extra var at = ly_lead for i in from .. to { let c = n.children[i] if c.pos >= 2 { continue } - var m = ly_basis(c, n.dir, main_room) + var m = sizes[i - from] if grows > 0.0 and free > 0.0 { m = m + free * ly_grow(c, n.dir) / grows } ly_place_child(n, c, at, top, m, cross_room) if c.pos == 1 { ly_offset(c, ly_or0(c.left) - ly_or0(c.right), ly_or0(c.top) - ly_or0(c.bottom)) } @@ -68,8 +71,8 @@ function ly_place_line(n: UiNode, from: int, to: int, top: float, main_room: flo # one child: `m` along the run and its own size (or the whole room, stretched) across, less margins function ly_place_child(n: UiNode, c: UiNode, at: float, top: float, m: float, cross_room: float) -> void { var cr = ly_cross(c, n.dir) - if n.dir == UI_ROW and c.h_pct > 0.0 { cr = cross_room * c.h_pct / 100.0 + c.mt + c.mb } - if n.dir == UI_COL and c.w_pct > 0.0 { cr = cross_room * c.w_pct / 100.0 + c.ml + c.mr } + if n.dir == UI_ROW and c.h_pct > 0.0 { cr = ly_ph(c, cross_room) + c.mt + c.mb } + if n.dir == UI_COL and c.w_pct > 0.0 { cr = ly_pw(c, cross_room) + c.ml + c.mr } if ly_stretches(n, c) { cr = cross_room } var a = n.align if c.self >= 0 { a = c.self } diff --git a/packages/ludic.ui/layout_shrink.ludic b/packages/ludic.ui/layout_shrink.ludic new file mode 100644 index 00000000..884ea78b --- /dev/null +++ b/packages/ludic.ui/layout_shrink.ludic @@ -0,0 +1,75 @@ +# layout_shrink.ludic - flex-shrink: when a line's children want more room than it has, those that +# shrink give it up, each in proportion to its shrink times its size, and none below what it must +# keep (its min size, a fixed size, its content's). A scroll box keeps nothing - it scrolls - so a +# list in a column takes the room its siblings leave; a box in a column shrinks as far as the scroll +# boxes inside it let it, and anything else shrinks only when flex-shrink says so. +function ly_shrink(dir: int, c: UiNode) -> float { + if c.ex != null and c.ex.shrink >= 0.0 { return c.ex.shrink } + if c.kind == UI_SCROLL { return 1.0 } + if dir == UI_COL and c.kind == UI_BOX { return 1.0 } + return 0.0 +} +# how small a child may be pressed along the run, margins included +function ly_floor(dir: int, c: UiNode) -> float { + if dir == UI_ROW { + var w = c.min_w + if c.w >= 0.0 { w = Math.max(w, c.w) } + return w + c.ml + c.mr + } + return Math.max(ly_min_h(c), c.min_h) + c.mt + c.mb +} +function ly_min_h(c: UiNode) -> float { + if c.ex != null and c.ex.has_min { return c.ex.mh_min } + return c.mh +} +# `sizes` (the children from..to) shrunk by `over` in all +function ly_shrink_line(n: UiNode, from: int, sizes: []float, over0: float) -> void { + var over = over0 + let frozen = new []bool + for k in 0 .. len(sizes) { push(frozen, n.children[from + k].pos >= 2) } + for pass in 0 .. 8 { + var wsum = 0.0 + for k in 0 .. len(sizes) { + if not frozen[k] { wsum = wsum + ly_shrink(n.dir, n.children[from + k]) * sizes[k] } + } + if wsum <= 0.0 or over <= 0.01 { return } + var taken = 0.0 + for k in 0 .. len(sizes) { + let c = n.children[from + k] + let wk = ly_shrink(n.dir, c) * sizes[k] + if frozen[k] or wk <= 0.0 { continue } + let floor = Math.min(ly_floor(n.dir, c), sizes[k]) + var want = sizes[k] - over * wk / wsum + if want <= floor { + want = floor + frozen[k] = true + } + taken = taken + sizes[k] - want + sizes[k] = want + } + over = over - taken + } +} +# how short the box can be pressed: a scroll box to its padding, a column to what its children can +# be, a row to its tallest child's least; kept only when it is less than the box's own height +function ly_min_note(n: UiNode) -> void { + var m = n.mh + if n.h < 0.0 and n.kind == UI_SCROLL { m = ly_vin(n) } else if n.h < 0.0 and n.kind == UI_BOX and n.children != null and not n.wrap { + var sum = 0.0 + var count = 0 + for i in 0 .. len(n.children) { + let c = n.children[i] + if c.pos >= 2 { continue } + let cm = ly_min_h(c) + c.mt + c.mb + if n.dir == UI_ROW { sum = Math.max(sum, cm) } else { sum = sum + cm } + if n.dir == UI_COL and count > 0 { sum = sum + n.gap } + count += 1 + } + m = ly_vin(n) + sum + } + m = ly_clamp(m, n.min_h, n.max_h) + if m < n.mh - 0.01 { + nx(n).mh_min = m + n.ex.has_min = true + } else if n.ex != null { n.ex.has_min = false } +} diff --git a/packages/ludic.ui/layout_text.ludic b/packages/ludic.ui/layout_text.ludic index 368eb366..8433de2b 100644 --- a/packages/ludic.ui/layout_text.ludic +++ b/packages/ludic.ui/layout_text.ludic @@ -5,7 +5,7 @@ function ly_measure_text(n: UiNode, avail: float) -> void { let lh = n.rsize * 1.25 let one = ly_text_w(n.text, n.rsize) var room = avail - n.ml - n.mr - ly_hin(n) - if n.w > 0.0 { room = n.w - ly_hin(n) } + if n.w >= 0.0 { room = n.w - ly_hin(n) } if n.nowrap or room <= 0.0 or one <= room { n.mw = n.mw + one n.mh = n.mh + lh diff --git a/packages/ludic.ui/layout_wrap.ludic b/packages/ludic.ui/layout_wrap.ludic index 6b681403..317d2fe8 100644 --- a/packages/ludic.ui/layout_wrap.ludic +++ b/packages/ludic.ui/layout_wrap.ludic @@ -36,10 +36,10 @@ function ly_place_abs(n: UiNode, c: UiNode) -> void { } var w = c.mw var h = c.mh - if c.w_pct > 0.0 { w = bw * c.w_pct / 100.0 } - if c.h_pct > 0.0 { h = bh * c.h_pct / 100.0 } - if c.w == UI_FILL or (c.left != UI_AUTO and c.right != UI_AUTO and c.w <= 0.0) { w = bw - ly_or0(c.left) - ly_or0(c.right) - c.ml - c.mr } - if c.h == UI_FILL or (c.top != UI_AUTO and c.bottom != UI_AUTO and c.h <= 0.0) { h = bh - ly_or0(c.top) - ly_or0(c.bottom) - c.mt - c.mb } + if c.w_pct > 0.0 { w = ly_pw(c, bw) } + if c.h_pct > 0.0 { h = ly_ph(c, bh) } + if c.w == UI_FILL or (c.left != UI_AUTO and c.right != UI_AUTO and c.w == UI_AUTO) { w = bw - ly_or0(c.left) - ly_or0(c.right) - c.ml - c.mr } + if c.h == UI_FILL or (c.top != UI_AUTO and c.bottom != UI_AUTO and c.h == UI_AUTO) { h = bh - ly_or0(c.top) - ly_or0(c.bottom) - c.mt - c.mb } var x = bx + n.pl + c.ml if c.left != UI_AUTO { x = bx + c.left + c.ml } else if c.right != UI_AUTO { x = bx + bw - c.right - c.mr - w } var y = by + n.pt + c.mt diff --git a/packages/ludic.ui/node.ludic b/packages/ludic.ui/node.ludic index 87726935..48cc2dac 100644 --- a/packages/ludic.ui/node.ludic +++ b/packages/ludic.ui/node.ludic @@ -12,15 +12,13 @@ export property UiNode { vals: []Val = null # the same, as values, applied after every rule style: UiRule = null # its style="..." declarations sheet: UiSheet = null # the stylesheets of the file it was written in - sheet2: UiSheet = null # a component's root: its user's sheet too, and their style="..." - style2: UiRule = null - env2: UiEnv = null + ups: []UiPass = null # a component's root: each user's sheet and style="...", inner first parent: UiNode = null index: int = 0 # its place among its parent's children, for :first-child and the rest src: string = "" # an 's picture size: float = 0.0 - w: float = 0.0 - h: float = 0.0 + w: float = -1000000.0 # a fixed width or height (0 is 0); UI_AUTO: none, UI_FILL: fill + h: float = -1000000.0 w_pct: float = 0.0 # a width or height as a percentage of the parent's room h_pct: float = 0.0 pt: float = 0.0 # padding, top right bottom left @@ -108,5 +106,6 @@ export property UiNode { 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 + ex: UiNodeX = null # what fewer elements say (node_more.ludic), made when one does } diff --git a/packages/ludic.ui/node_more.ludic b/packages/ludic.ui/node_more.ludic new file mode 100644 index 00000000..4228cee1 --- /dev/null +++ b/packages/ludic.ui/node_more.ludic @@ -0,0 +1,40 @@ +# node_more.ludic - what only some elements say, kept apart from UiNode so the common node stays +# small: made the first time a property needs it (nx), and null everywhere else +# a component user's styles for the root it passed class, style and id down to +export property UiPass { + sheet: UiSheet = null # the sheet of the file the component's tag was written in + style: UiRule = null # the tag's style="..." + env: UiEnv = null # where the tag's values are read +} +export property UiNodeX { + shrink: float = -1.0 # flex-shrink; -1: a scroll box's 1, anything else's 0 (or 1 in a column) + order: int = 0 # flex's order + w_off: float = 0.0 # calc(): the lengths added to a percentage width or height + h_off: float = 0.0 + mh_min: float = 0.0 # how short a column can be pressed: its scroll boxes give way + has_min: bool = false + tsh_set: bool = false # text-shadow, inherited as CSS's is; tsh_c -2 is none + tsh_x: float = 0.0 + tsh_y: float = 0.0 + tsh_c: int = -2 + tsh_a: float = 1.0 +} +function nx(n: UiNode) -> UiNodeX { + if n.ex == null { n.ex = new UiNodeX } + return n.ex +} +function ly_woff(n: UiNode) -> float { + if n.ex == null { return 0.0 } + return n.ex.w_off +} +function ly_hoff(n: UiNode) -> float { + if n.ex == null { return 0.0 } + return n.ex.h_off +} +# a percentage width or height in `room`, with what calc() added to it +function ly_pw(n: UiNode, room: float) -> float { return room * n.w_pct / 100.0 + ly_woff(n) } +function ly_ph(n: UiNode, room: float) -> float { return room * n.h_pct / 100.0 + ly_hoff(n) } +function ly_order(n: UiNode) -> int { + if n.ex == null { return 0 } + return n.ex.order +} diff --git a/packages/ludic.ui/ua.ludic b/packages/ludic.ui/ua.ludic index e00b4588..5b0d0611 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 } progress, meter { width: 160; height: 10; background: #202428; border-radius: 5 } progress .ui-fill, meter .ui-fill { justify-content: start; border-radius: 5 } [popover] { position: absolute; z-index: 900 } .ui-tooltip { position: fixed; padding: 6 10; background: #202428; border: 1px solid #555a60; font-size: 15; z-index: 1000 }" +const UA_LSS: string = "h1 { font-size: 32 } h2 { font-size: 26 } h3 { font-size: 22 } h4 { font-size: 19 } h5 { font-size: 16 } h6 { font-size: 14 } small { font-size: 13 } hr { height: 1; width: fill; background: #808080; margin: 4 0 } scroll { width: fill; height: fill } img { width: 24; height: 24 } button { align-items: center; justify-content: center; gap: 8; padding: 6 14; background: #3a3f46; border-radius: 4 } button:hover { background: #4a5058 } button:disabled { opacity: 0.4 } :focus-visible { outline: 2px solid #ffcc55; outline-offset: 2 } input, select { align-items: center; gap: 10; padding: 6 10 } .ui-label { flex-grow: 1 } .ui-labels { flex-grow: 1; gap: 2 } .ui-labels .ui-label { flex-grow: 0 } .ui-note { font-size: 13; color: #9aa0a6 } .ui-track { flex-direction: row; align-items: center; width: 44; height: 22; padding: 3; background: #555a60; border-radius: 11 } .ui-knob { width: 16; height: 16; background: #eeeeee; border-radius: 8 } input:checked .ui-track { justify-content: end; background: #d9a54a } input[type=range] .ui-track { width: 200; height: 8; padding: 0; border-radius: 4 } .ui-fill { height: fill; background: #d9a54a; justify-content: end; align-items: center; border-radius: 4 } .ui-thumb { width: 14; height: 14; background: #eeeeee; border-radius: 7; margin: 0 -7 } .ui-value { min-width: 40 } .ui-field { width: 200; padding: 4 8; background: #202428; border: 1px solid #555a60 } .ui-caret { width: 2; height: 1.2em; background: #eeeeee } .ui-prev, .ui-next { color: #aaaaaa } progress, meter { width: 160; height: 10; background: #202428; border-radius: 5 } progress .ui-fill, meter .ui-fill { justify-content: start; border-radius: 5 } [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/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index 3b6b1cad..ab002a69 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -760,6 +760,7 @@ function cmd_dev_test() -> int { controller_case("library/ui_controls", "", "1 false 7 2 Adalove 70 1 false 8 2 Adalove 70; scrolled 40", "ui_controls.ludic (L11: focus and keys, the built-in controls, a range dragged, a box scrolled)") controller_case("library/ui_anim", "", "0 100 100 | 49 50 100 | 100 100 61", "ui_anim.ludic (L11: @keyframes and animation, looping and alternating, and a transition)") controller_case("library/ui_popover", "", "drop false | drop true | drop false | drop false | fill 60 of 200", "ui_popover.ludic (L11: a popover keeps the keys and closes on a press outside or Esc; progress fills)") + controller_case("library/ui_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)") 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")