diff --git a/LANGUAGE.md b/LANGUAGE.md index 30174792..2f0da60c 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -530,7 +530,13 @@ applies to both: is drawn as painted, as CSS draws one; with one it is tinted by it, so one rounded texture serves every colour, and `transparent` (or any colour with no alpha) leaves nothing to see. - Text: `text-shadow: x y blur colour` draws the text again under itself, offset (the blur is - drawn sharp), and is inherited. + drawn sharp), and is inherited. `text-fit: shrink 12px` draws a line too long for its box + smaller, down to that size, then cuts what still does not fit with an ellipsis (for a chip + whose words may be long in another language). `line-height` is a multiple of the font size + (`1.5`), px or em. Both are inherited. + - `em` is the font size the element ends with, wherever its `font-size` is set among the rules + that style it; in `font-size` itself it is the parent's. A sheet's own rule wins a tie with a + rule it `@import`s. - Pictures: an `` of a file is drawn as it is; a cell of an atlas is a glyph and takes the `color` around it, as text does. An `` with a `color` of its own is tinted by it. - Motion: `opacity` fades the element and everything inside it. `@keyframes` with `animation: diff --git a/changes/defaults-views-templates.md b/changes/defaults-views-templates.md index d637fa02..7b99a6aa 100644 --- a/changes/defaults-views-templates.md +++ b/changes/defaults-views-templates.md @@ -89,6 +89,9 @@ What a game's screens found missing, now in `ludic.ui`: there rather than whenever the pointer is down over it; - an anchored popover's `align="start|center|end"`, and `within="id"` (by default the nearest scroll box around it) for the bounds it is flipped against and kept inside; +- `text-fit: shrink MIN` shrinks a line to its box, then cuts it with an ellipsis; `line-height`; + an `em` reads the font size the element ends with (a `font-size` later in the rule, or in a later + rule), not the one it had so far; - a transition lands exactly on its end value (it had stopped a rounding error short of it, at every frame rate). diff --git a/examples/library/ui_text.ludic b/examples/library/ui_text.ludic new file mode 100644 index 00000000..73b74967 --- /dev/null +++ b/examples/library/ui_text.ludic @@ -0,0 +1,23 @@ +# ui_text.ludic — text: text-fit shrinks a line to its chip down to a least size and then cuts it with +# an ellipsis; line-height as a multiple and in px; an em reads the font size the element ends with +# (set later in the rule, or by a later rule); a sheet's own rule wins a tie with one it imports. +import "ludic.ui" +import "ui_text_parts/Fit.ludic" +program UiText { + numbers float + var said: []string = new []string + function rec_text(x: float, y: float, s: string, size: float, c: int, a: float) -> void { push(said, `{s}@{int(size * 10.0)} y{int(y)}`) } + function w(s: string, size: float) -> float { return float(len(s)) * size * 0.5 } + entry { + let b = new UiBackend + b.text = fn rec_text + b.measure = fn w + ui_backend(b) + ui_show("Fit", null, 0.0, 0.0, 400.0, 600.0) + let root: UiNode = ui_nodes("Fit", null) + ui_place(root, 0.0, 0.0, 400.0, 600.0) + let page = root.children[0] + let lines = page.children[3] + print(`{said[0]} | {said[1]} | {said[2]} | lines {len(lines.lines)} {int(lines.mh)} {said[3]} {said[4]} | tall {int(page.children[4].mh)} | em {int(page.children[5].pt)} {int(page.children[6].pt)} | tie {ui_hex(page.children[7].fg)}`) + } +} diff --git a/examples/library/ui_text_parts/Fit.lss b/examples/library/ui_text_parts/Fit.lss new file mode 100644 index 00000000..6ef13638 --- /dev/null +++ b/examples/library/ui_text_parts/Fit.lss @@ -0,0 +1,10 @@ +@import "base.lss"; +.page { width: 400px } +.chip { width: 100px; font-size: 16px; text-fit: shrink 10px } +.mid { width: 80px } +.lines { width: 60px; font-size: 16px; line-height: 2 } +.tall { line-height: 30px } +.em { padding: 1em; font-size: 20px } +.em2 { padding: 1em } +.page .em2 { font-size: 24px } +.tie { color: #00ff00 } diff --git a/examples/library/ui_text_parts/Fit.ludic b/examples/library/ui_text_parts/Fit.ludic new file mode 100644 index 00000000..035532d9 --- /dev/null +++ b/examples/library/ui_text_parts/Fit.ludic @@ -0,0 +1,4 @@ +# Fit.ludic - chips too narrow for their words, lines spaced by line-height, ems read against the +# font size an element ends with, and a sheet's own rule over an imported one of the same weight +component Fit { +} diff --git a/examples/library/ui_text_parts/Fit.xml b/examples/library/ui_text_parts/Fit.xml new file mode 100644 index 00000000..d9797706 --- /dev/null +++ b/examples/library/ui_text_parts/Fit.xml @@ -0,0 +1,10 @@ +
+

Photograph the Bells from the signs

+

Short

+

Medium words

+

one two three four five six

+

a

+
x
+
y
+

t

+
diff --git a/examples/library/ui_text_parts/base.lss b/examples/library/ui_text_parts/base.lss new file mode 100644 index 00000000..92a18763 --- /dev/null +++ b/examples/library/ui_text_parts/base.lss @@ -0,0 +1 @@ +.tie { color: #0000ff } diff --git a/packages/ludic.ui/attr.ludic b/packages/ludic.ui/attr.ludic index ae226792..fe70bfac 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-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 " +const AT_UNDRAWN: string = " font-weight font-family font-style box-shadow cursor transition animation 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 } diff --git a/packages/ludic.ui/attr_more.ludic b/packages/ludic.ui/attr_more.ludic index d408621d..88cf95c5 100644 --- a/packages/ludic.ui/attr_more.ludic +++ b/packages/ludic.ui/attr_more.ludic @@ -1,8 +1,8 @@ # attr_more.ludic - width and height (where 0 is 0 and calc() may hold a percentage), the flex -# shorthand, flex-shrink, order, text-shadow and pointer-events +# shorthand, flex-shrink, order, text-shadow, pointer-events, text-fit and line-height (text_fit.ludic) 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 if key == "pointer-events" { nx(n).no_pointer = tpl_words(s) == "none" } else { return false } + 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 if key == "pointer-events" { nx(n).no_pointer = tpl_words(s) == "none" } else if key == "text-fit" { el_text_fit(n, s) } else if key == "line-height" { el_line_h(n, s) } else { return false } return true } # width or height: a length (0 included), fill, a percentage, calc(), or auto/fit (none) diff --git a/packages/ludic.ui/cascade.ludic b/packages/ludic.ui/cascade.ludic index b06ff048..7649781d 100644 --- a/packages/ludic.ui/cascade.ludic +++ b/packages/ludic.ui/cascade.ludic @@ -16,7 +16,16 @@ 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 function cs_node(n: UiNode) -> void { + cs_pass = 1 + cs_styles(n) + cs_pass = 0 + cs_styles(n) + an_apply(n) + tr_apply(n) +} +function cs_styles(n: UiNode) -> void { cs_sheet(n, ua_sheet()) 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) } @@ -25,10 +34,12 @@ function cs_node(n: UiNode) -> void { 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) + for i in 0 .. len(n.akeys) { + if cs_pass_ok(n.akeys[i]) { el_apply(n, n.akeys[i], n.vals[i]) } + } } +var cs_pass: int = 0 +function cs_pass_ok(key: string) -> bool { return cs_pass == 0 or at_alias(key) == "size" } function cs_sheet(n: UiNode, sh: UiSheet) -> void { for i in 0 .. len(sh.rules) { let r = sh.rules[i] @@ -42,7 +53,7 @@ function cs_rule_in(n: UiNode, r: UiRule, e: UiEnv) -> void { if len(r.keys[j]) > 2 and r.keys[j][0..2] == "--" { el_attr(n, r.keys[j], ui_eval(r.vals[j], e)) } } for j in 0 .. len(r.keys) { - if not (len(r.keys[j]) > 2 and r.keys[j][0..2] == "--") { el_attr(n, r.keys[j], ui_eval(r.vals[j], e)) } + if not (len(r.keys[j]) > 2 and r.keys[j][0..2] == "--") and cs_pass_ok(r.keys[j]) { el_attr(n, r.keys[j], ui_eval(r.vals[j], e)) } } } var ua_sh: UiSheet = null diff --git a/packages/ludic.ui/draw_content.ludic b/packages/ludic.ui/draw_content.ludic index 1856b01d..e4a6a837 100644 --- a/packages/ludic.ui/draw_content.ludic +++ b/packages/ludic.ui/draw_content.ludic @@ -2,21 +2,22 @@ # text-shadow, and a picture # a text's lines (one, or the wrapped ones), each placed by text-align, the last cut by an ellipsis function fr_text(n: UiNode) -> void { - let y = n.y + n.pt + n.bt + let lh = ly_line_h(n) + let y = n.y + n.pt + n.bt + (lh - n.rsize * 1.25) / 2.0 let a = n.rfg_a * fr_ga let sh = fr_text_shadow(n) if n.lines == null { - var s = n.text - if n.ellipsis { s = ly_ellipsis(s, n.rsize, n.cw - ly_hin(n)) } - fr_line(n, sh, fr_line_x(n, s), y, s, a) + var s = tf_fit(n, n.text) + if n.ellipsis { s = ly_ellipsis(s, tf_size, n.cw - ly_hin(n)) } + fr_line(n, sh, fr_line_x(n, s, tf_size), y, s, a, tf_size) return } - for i in 0 .. len(n.lines) { fr_line(n, sh, fr_line_x(n, n.lines[i]), y + float(i) * n.rsize * 1.25, n.lines[i], a) } + for i in 0 .. len(n.lines) { fr_line(n, sh, fr_line_x(n, n.lines[i], n.rsize), y + float(i) * lh, n.lines[i], a, n.rsize) } } # one line, over its shadow when it has one -function fr_line(n: UiNode, sh: UiNodeX, x: float, y: float, s: string, a: float) -> void { - if sh != null { ui_be.text(x + sh.tsh_x, y + sh.tsh_y, s, n.rsize, sh.tsh_c, sh.tsh_a * fr_ga) } - ui_be.text(x, y, s, n.rsize, n.rfg, a) +function fr_line(n: UiNode, sh: UiNodeX, x: float, y: float, s: string, a: float, size: float) -> void { + if sh != null { ui_be.text(x + sh.tsh_x, y + sh.tsh_y, s, size, sh.tsh_c, sh.tsh_a * fr_ga) } + ui_be.text(x, y, s, size, n.rfg, a) } # text-shadow is inherited: the nearest element that says it, or null (none) function fr_text_shadow(n: UiNode) -> UiNodeX { @@ -33,10 +34,10 @@ function fr_text_shadow(n: UiNode) -> UiNodeX { # the opacity a native's draw is at: every group opacity around it, its own included export function ui_opacity() -> float { return fr_ga } # where a line starts: text-align within the room the box gives it -function fr_line_x(n: UiNode, s: string) -> float { +function fr_line_x(n: UiNode, s: string, size: float) -> float { let x = n.x + n.pl + n.bl if n.talign == UI_START { return x } - let spare = n.cw - ly_hin(n) - ly_text_w(s, n.rsize) + let spare = n.cw - ly_hin(n) - ly_text_w(s, size) if n.talign == UI_CENTER { return x + spare / 2.0 } return x + spare } diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic index 65f776b8..059c6c0e 100644 --- a/packages/ludic.ui/index.ludic +++ b/packages/ludic.ui/index.ludic @@ -70,6 +70,7 @@ import "layout.ludic" import "layout_box.ludic" import "layout_shrink.ludic" import "layout_text.ludic" +import "text_fit.ludic" import "layout_run.ludic" import "layout_wrap.ludic" import "frame.ludic" diff --git a/packages/ludic.ui/layout_text.ludic b/packages/ludic.ui/layout_text.ludic index 8433de2b..a44f913d 100644 --- a/packages/ludic.ui/layout_text.ludic +++ b/packages/ludic.ui/layout_text.ludic @@ -2,11 +2,13 @@ # normal), breaking between words; nowrap keeps one line, and text-overflow: ellipsis cuts it function ly_measure_text(n: UiNode, avail: float) -> void { n.lines = null - let lh = n.rsize * 1.25 - let one = ly_text_w(n.text, n.rsize) + let lh = ly_line_h(n) + var 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.nowrap or room <= 0.0 or one <= room { + let fits = n.ex != null and n.ex.fit_min > 0.0 + if fits and room > 0.0 { one = Math.min(one, room) } + if n.nowrap or fits or room <= 0.0 or one <= room { n.mw = n.mw + one n.mh = n.mh + lh return diff --git a/packages/ludic.ui/node_more.ludic b/packages/ludic.ui/node_more.ludic index c38b390b..dd06e6e1 100644 --- a/packages/ludic.ui/node_more.ludic +++ b/packages/ludic.ui/node_more.ludic @@ -19,6 +19,9 @@ export property UiNodeX { tsh_c: int = -2 tsh_a: float = 1.0 no_pointer: bool = false # pointer-events: none - the pointer goes through it + fit_min: float = 0.0 # text-fit: shrink MIN - the smallest a line is shrunk to fit its box + lh_kind: int = 0 # line-height: 0 normal, 1 a multiple of the font size, 2 px + lh_v: float = 0.0 } function nx(n: UiNode) -> UiNodeX { if n.ex == null { n.ex = new UiNodeX } diff --git a/packages/ludic.ui/text_fit.ludic b/packages/ludic.ui/text_fit.ludic new file mode 100644 index 00000000..253ee76e --- /dev/null +++ b/packages/ludic.ui/text_fit.ludic @@ -0,0 +1,52 @@ +# text_fit.ludic - text-fit: shrink 12px draws a line that does not fit its box smaller, down to that +# size, and cuts what still does not fit with an ellipsis; line-height (a multiple of the font size, +# px or em) spaces a text's lines. Both are inherited, as font properties are. +var tf_size: float = 0.0 # the size tf_fit drew at +function el_text_fit(n: UiNode, s: string) -> void { + let parts = lss_split(tpl_words(s), 32) + nx(n).fit_min = 0.0 + if len(parts) >= 2 and parts[0] == "shrink" { nx(n).fit_min = Math.max(el_unit(parts[1]), 1.0) } +} +function el_line_h(n: UiNode, s0: string) -> void { + let s = tpl_words(s0) + let x = nx(n) + x.lh_kind = 0 + if s == "normal" or s == "" { return } + let c = s[len(s) - 1] + if c >= 48 and c <= 57 or c == 46 { + x.lh_kind = 1 + x.lh_v = el_num(s) + return + } + x.lh_kind = 2 + x.lh_v = el_unit(s) +} +# the nearest element, out from this one, that says `what` (0 text-fit, 1 line-height), or null +function tf_up(n: UiNode, what: int) -> UiNodeX { + var at = n + while at != null { + if at.ex != null and what == 0 and at.ex.fit_min > 0.0 { return at.ex } + if at.ex != null and what == 1 and at.ex.lh_kind > 0 { return at.ex } + at = at.parent + } + return null +} +function ly_line_h(n: UiNode) -> float { + let x = tf_up(n, 1) + if x == null { return n.rsize * 1.25 } + if x.lh_kind == 1 { return n.rsize * x.lh_v } + return x.lh_v +} +# one line to fit its box: at its own size if it does, else smaller down to text-fit's least, then +# cut; tf_size says the size it is drawn at +function tf_fit(n: UiNode, s: string) -> string { + tf_size = n.rsize + let x = tf_up(n, 0) + let room = n.cw - ly_hin(n) + if x == null or room <= 0.0 { return s } + let w = ly_text_w(s, tf_size) + if w <= room { return s } + tf_size = Math.max(x.fit_min, n.rsize * room / w) + while tf_size > x.fit_min and ly_text_w(s, tf_size) > room { tf_size = Math.max(x.fit_min, tf_size - 0.5) } + return ly_ellipsis(s, tf_size, room) +} diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index 5365074c..877b30e8 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -766,6 +766,7 @@ function cmd_dev_test() -> int { controller_case("library/ui_gamepad", "", "last 6 vol 0 q 0 open false | last 0 vol 0 q 0 open false | last 0 vol 2 q 0 open false | last 0 vol 2 q 2 open false | last 0 vol 2 q 2 open true | last 0 vol 2 q 2 open false", "ui_gamepad.ludic (L11: the pad and held keys - a held direction repeats on the clock at any rate, A presses, left and right step, B closes a popover)") controller_case("library/ui_pointer", "", "pointerdown 100,80 0 0; pointerup 300,300 0 0; wheel 50,50 0 1; dragged 200 | zoomed 1 pad 7 10 1 2 hold true 0 0 active 16711680 | zoomed 1 pad 7 10 1 2 hold false 1 0 | zoomed 1 pad 7 10 1 2 hold true 1 0 | zoomed 1 pad 7 10 1 2 hold false 2 0 3817286", "ui_pointer.ludic (L11: pointer events for natives and markup, capture until release, a button over a native keeps its press; on-down, on-up and :active)") controller_case("library/ui_popover_align", "", "centred, flipped in the panel 90,40 | end below 110,70 | above b in the list 0 inside 1", "ui_popover_align.ludic (L11: anchored popovers aligned along their side, flipped and kept within a named container or their scroll box)") + controller_case("library/ui_text", "", "Photograph the Be...@100 y0 | Short@160 y20 | Medium words@133 y40 | lines 5 160 one two@160 y66 three@160 y98 | tall 30 | em 20 24 | tie #00ff00", "ui_text.ludic (L11: text-fit shrinks then cuts, line-height, em against the final font size, a sheet over its imports on a tie)") 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)")