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)")