diff --git a/LANGUAGE.md b/LANGUAGE.md index 9a1221d9..8da9eae1 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -324,7 +324,12 @@ component Counter { - **The template** has one root (use `` for several). A component is used by its name as a tag. `set count = 0` in an action sets its state, and `emit close` runs what its parent passed as `on-close`. `class`, `style` and `id` on a component's tag land on its root element, styled by - the parent's sheet as well as its own. + the parent's sheet as well as its own; when that root is itself a component, every user in turn + passes theirs down (the outermost's `id` wins). The rules of all those sheets that match the root + are weighed together by specificity, as one sheet's are, and a user's rule wins a tie. +- **A component's names.** An event may be called anything, `set` included (`on set(v: int)`, + pressed as `set(4)`; `set x = ...` is still the action). A `string` prop given a number reads it + as text (`label="{3}"` is `"3"`). Two components of one name are an error that names both files. - **Compiled in.** The compiler reads the template and the styles from beside the declaration and inlines each `@import` (a path starting with `/` is from the project's root), so a missing template fails the build and nothing has to ship beside the program. `ui_reload()` reads the @@ -368,12 +373,21 @@ applies to both: - **The box model and flex.** Sizes are border boxes. - `padding` and `margin` take one to four lengths, or one side by name (`padding-left`). - `border` is `2px solid #ffcc00`, or `border-width` and `border-color`. - - A length is `12`, `12px`, `50%`, `fit`/`auto` or `fill`. + - A length is `12`, `12px`, `50%`, `fit`/`auto`, `fill` or `calc()`. `width: 0` and `height: 0` + are 0, not unset. + - `calc()` takes `+`, `-`, `*` and `/` and brackets over `px`, `em`, `rem`, `vw`, `vh`, plain + numbers and `var()`; in a `width` or `height` it may also hold a percentage of the room + (`calc(50% - 10px)`). - `flex-grow` (or `flex`) shares out the spare room along `flex-direction`, and `fill` is a share of 1. `justify-content` takes `flex-start`/`start`, `center`, `end`, `space-between`, `space-around` or `space-evenly`. - `align-items` and `align-self` take `start`, `center`, `end` or `stretch`. `flex-wrap: wrap` breaks a row into lines, and `min-`/`max-width`/`-height` bound it. + - `flex-shrink` gives up room when a line's children want more than it has, each in proportion + to its shrink times its size, and none below its min size, a fixed size or its content. A scroll + box shrinks (and scrolls) and a box in a column shrinks as far as the scroll boxes in it let it, + so a list in a column takes the room its siblings leave with no height of its own. `flex: 1 0` + is the grow and the shrink. `order` rearranges a box's children without touching the tree. - `gap`, `text-align`, `display: none`, `background(-color)`, `color`, `opacity` and `font-size` are CSS's. @@ -411,8 +425,8 @@ applies to both: neither takes room in the flow. - Units: `em`, `rem`, `vw` and `vh`. - `@media (min-width: ..) and (max-height: ..) { ... }`. - - Text wraps between words to fit its box; `white-space: nowrap` keeps one line and - `text-overflow: ellipsis` cuts it. + - Text wraps between words to fit its box - in a row, in the room its siblings leave it; + `white-space: nowrap` keeps one line and `text-overflow: ellipsis` cuts it. - `overflow: auto|scroll|hidden` makes a box scroll. - Selectors also take `+` and `~`, `:nth-child(2n+1)`, `:checked` and `:active`. - **More React.** @@ -435,31 +449,51 @@ applies to both: - A test drives all of it with `ui_input(i)`. - **Controls are built in**, each made of plain parts a stylesheet styles (`.ui-label`, `.ui-track`, `.ui-knob`, `.ui-fill`, `.ui-thumb`, `.ui-field`, `.ui-value`, `.ui-caret`, - `.ui-prev`, `.ui-next`): + `.ui-prev`, `.ui-next`; a track is a row, and a range's fill is as tall as its track): - ` + +

{count} {bind} {vol} {said}

+ diff --git a/examples/library/ui_inputs_parts/Tag.ludic b/examples/library/ui_inputs_parts/Tag.ludic new file mode 100644 index 00000000..196cadfa --- /dev/null +++ b/examples/library/ui_inputs_parts/Tag.ludic @@ -0,0 +1,4 @@ +# Tag.ludic - a string prop, which a number given to it arrives in as text +component Tag { + prop text: string = "none" +} diff --git a/examples/library/ui_inputs_parts/Tag.xml b/examples/library/ui_inputs_parts/Tag.xml new file mode 100644 index 00000000..6fd5cee1 --- /dev/null +++ b/examples/library/ui_inputs_parts/Tag.xml @@ -0,0 +1 @@ +

{text}!

diff --git a/examples/library/ui_look.ludic b/examples/library/ui_look.ludic new file mode 100644 index 00000000..9d5beab7 --- /dev/null +++ b/examples/library/ui_look.ludic @@ -0,0 +1,65 @@ +# ui_look.ludic — what ludic.ui draws, through a backend that writes each call down: a component +# whose root is another component, styled by every sheet above it by specificity; text-shadow under +# text; a native reading the opacity it is drawn at (ui_opacity); border-image as painted, tinted by +# a background colour, and gone under `transparent`; a picture drawn as it is and an atlas cell in +# the text's colour; a tooltip of two lines; and popovers placed beside their anchors, one flipped. +import "ludic.ui" +import "ui_look_parts/Look.ludic" +import "ui_look_parts/Middle.ludic" +import "ui_look_parts/Inner.ludic" +import "ui_look_parts/Menu.ludic" +program UiLook { + numbers float + var said: []string = new []string + var seen: float = 0.0 + function rec_rect(x: float, y: float, w: float, h: float, c: int, a: float) -> void { } + function rec_text(x: float, y: float, s: string, size: float, c: int, a: float) -> void { push(said, `text {s} {int(x)},{int(y)} {ui_hex(c)}`) } + function rec_image(src: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void { push(said, `image {src} {ui_hex(c)}`) } + function rec_nine(src: string, slice: float, x: float, y: float, w: float, h: float, dst: float, c: int, a: float) -> void { push(said, `nine {int(w)} {ui_hex(c)}`) } + function gauge_measure(n: UiNode, avail: float) -> void { } + function gauge_draw(n: UiNode) -> void { seen = ui_opacity() } + function count(s: string) -> int { + var n = 0 + for i in 0 .. len(said) { + if Text.starts_with(said[i], s) { n += 1 } + } + return n + } + function has(s: string) -> string { + for i in 0 .. len(said) { + if Text.starts_with(said[i], s) { return said[i] } + } + return "-" + } + function show(name: string, i: UiInput) -> UiNode { + ui_input(i) + said = new []string + ui_show(name, null, 0.0, 0.0, 400.0, 300.0) + let root: UiNode = ui_nodes(name, null) + ui_place(root, 0.0, 0.0, 400.0, 300.0) + return root.children[0] + } + entry { + let b = new UiBackend + b.rect = fn rec_rect + b.text = fn rec_text + b.image = fn rec_image + b.nine = fn rec_nine + ui_backend(b) + ui_native("gauge", fn gauge_measure, fn gauge_draw) + var page = show("Look", new UiInput) + let inner = page.children[0] + let tip = page.children[7] + let rest = new UiInput + rest.x = tip.x + 2.0 + rest.y = tip.y + 2.0 + for f in 0 .. 40 { page = show("Look", rest) } + print(`root {inner.tag}.{inner.classes[0]}.{inner.classes[1]}.{inner.classes[2]} pad {int(inner.pt)} margin {int(inner.mt)} color {ui_hex(inner.fg)}`) + print(`{has("text Hi")} | {has("text Hi 0")} | opacity {seen} | {has("nine 40 #ffffff")} {has("nine 40 #ff0000")} of {count("nine")}`) + print(`{has("image photo")} | {has("image icon")} | {has("text first")} | {has("text second")}`) + let menu = show("Menu", new UiInput) + let a = menu.children[1] + let c = menu.children[3] + print(`by id at {int(a.x)},{int(a.y)} | flipped at {int(c.x)},{int(c.y)} {int(c.cw)}x{int(c.ch)}`) + } +} diff --git a/examples/library/ui_look_parts/Inner.lss b/examples/library/ui_look_parts/Inner.lss new file mode 100644 index 00000000..a2a23a45 --- /dev/null +++ b/examples/library/ui_look_parts/Inner.lss @@ -0,0 +1 @@ +div.in { padding: 5px } diff --git a/examples/library/ui_look_parts/Inner.ludic b/examples/library/ui_look_parts/Inner.ludic new file mode 100644 index 00000000..ea13f836 --- /dev/null +++ b/examples/library/ui_look_parts/Inner.ludic @@ -0,0 +1,4 @@ +# Inner.ludic - the innermost root: its own rule is more specific than Middle's, so it holds +component Inner { + state n: int = 0 +} diff --git a/examples/library/ui_look_parts/Inner.xml b/examples/library/ui_look_parts/Inner.xml new file mode 100644 index 00000000..6ac2251d --- /dev/null +++ b/examples/library/ui_look_parts/Inner.xml @@ -0,0 +1 @@ +

in

diff --git a/examples/library/ui_look_parts/Look.lss b/examples/library/ui_look_parts/Look.lss new file mode 100644 index 00000000..10bfe048 --- /dev/null +++ b/examples/library/ui_look_parts/Look.lss @@ -0,0 +1,9 @@ +.page { width: 400px; gap: 2px } +.m { margin: 9px } +.shade { text-shadow: 2px 3px 4px #102030 } +.dim { opacity: 0.5 } +gauge { width: 10px; height: 10px } +.chip { width: 40px; height: 20px; border-image: url(chip.png) 8 } +.clear { background: transparent } +.red { background: #ff0000 } +.pics { flex-direction: row; color: #00ff00 } diff --git a/examples/library/ui_look_parts/Look.ludic b/examples/library/ui_look_parts/Look.ludic new file mode 100644 index 00000000..6dcb508d --- /dev/null +++ b/examples/library/ui_look_parts/Look.ludic @@ -0,0 +1,5 @@ +# Look.ludic - what is drawn: a nested component's root styled by every sheet above it, a shadow +# under text, a native at its group's opacity, border-images, pictures, and a tooltip of two lines +component Look { + state n: int = 0 +} diff --git a/examples/library/ui_look_parts/Look.xml b/examples/library/ui_look_parts/Look.xml new file mode 100644 index 00000000..953c4731 --- /dev/null +++ b/examples/library/ui_look_parts/Look.xml @@ -0,0 +1,10 @@ +
+ +

Hi

+
+
+
+
+
+

rest here

+
diff --git a/examples/library/ui_look_parts/Menu.lss b/examples/library/ui_look_parts/Menu.lss new file mode 100644 index 00000000..c391c723 --- /dev/null +++ b/examples/library/ui_look_parts/Menu.lss @@ -0,0 +1,4 @@ +.page { width: 400px; height: 300px } +.cell { position: absolute; left: 20px; top: 50px; width: 60px; height: 30px } +.edge { position: absolute; left: 350px; top: 280px; width: 50px; height: 30px } +.verbs { width: 100px; margin-left: 4px; margin-right: 4px } diff --git a/examples/library/ui_look_parts/Menu.ludic b/examples/library/ui_look_parts/Menu.ludic new file mode 100644 index 00000000..040af0e7 --- /dev/null +++ b/examples/library/ui_look_parts/Menu.ludic @@ -0,0 +1,5 @@ +# Menu.ludic - popovers beside what they belong to: one by the anchor's id, one beside the element +# before it, which has no room on its right and opens to the left +component Menu { + state n: int = 0 +} diff --git a/examples/library/ui_look_parts/Menu.xml b/examples/library/ui_look_parts/Menu.xml new file mode 100644 index 00000000..f1d58856 --- /dev/null +++ b/examples/library/ui_look_parts/Menu.xml @@ -0,0 +1,6 @@ +
+ +
+ +
+
diff --git a/examples/library/ui_look_parts/Middle.lss b/examples/library/ui_look_parts/Middle.lss new file mode 100644 index 00000000..25c4e0e7 --- /dev/null +++ b/examples/library/ui_look_parts/Middle.lss @@ -0,0 +1 @@ +.i { padding: 3px; color: #ff0000 } diff --git a/examples/library/ui_look_parts/Middle.ludic b/examples/library/ui_look_parts/Middle.ludic new file mode 100644 index 00000000..f849c893 --- /dev/null +++ b/examples/library/ui_look_parts/Middle.ludic @@ -0,0 +1,4 @@ +# Middle.ludic - a component whose root is another component +component Middle { + state n: int = 0 +} diff --git a/examples/library/ui_look_parts/Middle.xml b/examples/library/ui_look_parts/Middle.xml new file mode 100644 index 00000000..eef6d672 --- /dev/null +++ b/examples/library/ui_look_parts/Middle.xml @@ -0,0 +1 @@ + diff --git a/packages/ludic.ui/controls.ludic b/packages/ludic.ui/controls.ludic index 081128ac..73192673 100644 --- a/packages/ludic.ui/controls.ludic +++ b/packages/ludic.ui/controls.ludic @@ -1,8 +1,7 @@ # controls.ludic - what a control does: a button (or anything with on-click) is pressed, a checkbox # flips, a radio chooses its value, a select steps through its options, a range moves by its step -# (or to where the pointer is on its track), a text field takes what is typed, a key field the next -# key. Each says so with on-change and event.value, after the frame is drawn. -var ct_capture: string = "" +# (or to where the pointer is on its track), a text field takes what is typed, a number field its +# digits, a key field the next key. Each says so with on-change and event.value, after the frame is drawn. function ct_type(n: UiNode) -> string { if n.tag == "select" { return "select" } if n.tag != "input" { return "" } @@ -10,7 +9,7 @@ function ct_type(n: UiNode) -> string { if t == "" { return "text" } return t } -function ct_takes_arrows(n: UiNode) -> bool { return ct_type(n) == "range" or ct_type(n) == "select" } +function ct_takes_arrows(n: UiNode) -> bool { return ct_type(n) == "range" or ct_type(n) == "select" or ct_type(n) == "number" } function ct_takes_space(n: UiNode) -> bool { return ct_type(n) == "text" } function ct_change(n: UiNode, v: Val) -> void { ui_sound(ct_sound(n)) @@ -24,7 +23,7 @@ function ct_sound(n: UiNode) -> string { function ct_activate(n: UiNode) -> void { if not n.enabled { return } let t = ct_type(n) - if t == "checkbox" { ct_change(n, vl_bool(not ui_attr_on(n, "checked"))) } else if t == "radio" { ct_change(n, ui_attr(n, "value")) } else if t == "select" { ct_step(n, 1) } else if t == "key" { ct_capture = n.key } else if t == "" { + if t == "checkbox" { ct_change(n, vl_bool(not ui_attr_on(n, "checked"))) } else if t == "radio" { ct_change(n, ui_attr(n, "value")) } else if t == "select" { ct_step(n, 1) } else if t == "key" { ct_key_start(n) } else if t == "number" { ct_num_commit(n) } else if t == "" { ui_sound(ct_sound(n)) ui_fire(n, "press", Value.null()) } @@ -36,6 +35,7 @@ function ct_step(n: UiNode, d: int) -> void { let ov = ui_attr(n.opts[at], "value") if Value.kind(ov) == 0 { ct_change(n, Value.int(at)) } else { ct_change(n, ov) } } + if t == "number" { ct_num_step(n, d) } if t == "range" { var step = Value.as_float(ui_attr(n, "step")) if step <= 0.0 { step = 1.0 } @@ -70,11 +70,8 @@ function ct_drag(root: UiNode, key: string, x: float, y: float) -> void { # typing into a focused text field, or the key a key field is waiting for function ct_typed(n: UiNode) -> void { let t = ct_type(n) - if t == "key" and ct_capture == n.key and in_now.key != 0 and not in_now.enter { - ct_capture = "" - ct_change(n, Value.int(in_now.key)) - return - } + if t == "key" { ct_key_typed(n) } + if t == "number" { ct_num_typed(n) } if t != "text" or (in_now.typed == "" and not in_now.backspace) { return } var s = ui_attr_text(n, "value") if in_now.backspace and len(s) > 0 { s = s[0..ct_char_start(s)] } diff --git a/packages/ludic.ui/controls_build.ludic b/packages/ludic.ui/controls_build.ludic index 0b0a80c6..b5177c68 100644 --- a/packages/ludic.ui/controls_build.ludic +++ b/packages/ludic.ui/controls_build.ludic @@ -1,8 +1,9 @@ # controls_build.ludic - a control's parts, made as plain child boxes a stylesheet can style: # input[type=checkbox|radio] .ui-label .ui-track > .ui-knob # input[type=range] .ui-label .ui-track > .ui-fill, .ui-thumb .ui-value -# input[type=text|key] .ui-label .ui-field > .ui-value (+ .ui-caret while focused) +# input[type=text|number|key] .ui-label .ui-field > .ui-value (+ .ui-caret while focused) # select .ui-label .ui-prev .ui-value .ui-next (its