feat(lang): L11 views and templates - the UI is markup, not code
A `view Name { field = x; function q(..); on e(..) }` declaration is the one bridge between a
program and its UI: it writes view_<name>() -> UiView, whose model is a Value of every field and
whose call runs a query or an event by name.
ludic.ui is a template runtime:
- HTML-shaped XML screens and components, loaded at run time;
- {expression} bindings, if/else/each, props, slots, per-instance state;
- on-press / onclick actions (event, set, emit);
- component libraries (export="true", <import src as>).
Styling:
- stylesheets in <style> or importable .lss files (@import);
- CSS selectors (#id, .class, [attr=v], descendant and > combinators, :hover, :disabled,
:first-child, :last-child, :nth-child, :not) weighed by specificity;
- the box model and flex under CSS's property names.
Also:
- default parameters, and positional-then-named calls;
- Value gains a float kind;
- a function shadowing a runtime one is refused;
- an index is evaluated before the slice is read;
- runtime errors name the right file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
334469ef61
commit
66a2bc2214
64 changed files with 74806 additions and 63975 deletions
136
LANGUAGE.md
136
LANGUAGE.md
|
|
@ -275,6 +275,142 @@ The entries are compiled in: nothing is parsed at start-up, and a build that suc
|
|||
every resource it uses. The path is the project's (where the build runs), else beside the file that
|
||||
declares the registry.
|
||||
|
||||
### Default parameters, and calls that name what they change
|
||||
|
||||
A parameter can have a default, and a call leaves out what it does not change - the last ones when
|
||||
it passes arguments by position, or any it does not name. A call can pass its first arguments by
|
||||
position and the rest by name:
|
||||
|
||||
```ludic
|
||||
program Boxes {
|
||||
numbers float
|
||||
function box(label: string, w: float = 300.0, pad: float = 8.0, bold: bool = false) -> string {
|
||||
return `{label} {w} {pad} {bold}`
|
||||
}
|
||||
entry {
|
||||
print(box("a")) # every default
|
||||
print(box("b", 120.0)) # the first two by position
|
||||
print(box("c", bold: true)) # the subject by position, a prop by name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A default is an expression written with the function and evaluated for each call that leaves it
|
||||
out. A call that leaves out a parameter with no default, names one the function does not have, or
|
||||
puts a positional argument after a named one is refused with that said.
|
||||
|
||||
### Views and templates (`ludic.ui`)
|
||||
|
||||
A screen is not code. It is a template - markup in a file the program loads at run time - and it
|
||||
sees only what the program's `view` lets it: its fields to read, its functions to ask, and its
|
||||
`on` events to send. Nothing else crosses in either direction.
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — a fragment of a program that imports ludic.ui
|
||||
view Shop {
|
||||
purse = purse # a global's own type
|
||||
stock = stock # a list of records is a list of objects
|
||||
owned: int = len(bought) # any expression, with its type said
|
||||
function afford(i: int) -> bool { return purse >= stock[i].cost }
|
||||
on buy(i: int) { purse -= stock[i].cost }
|
||||
}
|
||||
```
|
||||
|
||||
`view Shop` writes `view_shop() -> UiView`. Its model is a `Value` object of every field, with a
|
||||
record as an object of the fields a template can read and a list or registry as a list. Its `call`
|
||||
runs a function or an event by name with arguments from the template: an int, a float, a bool or a
|
||||
string.
|
||||
|
||||
```xml
|
||||
<ui>
|
||||
<import src="kit.xml" as="kit"/>
|
||||
<screen name="shop" gap="4">
|
||||
<state pick="{-1}"/>
|
||||
<text size="24">The purse: ${purse}</text>
|
||||
<each in="{stock}" as="item" index="i">
|
||||
<kit:Line item="{item}" picked="{pick == i}" on-chose="set pick = i">
|
||||
<button enabled="{afford(i)}" on-press="buy(i); emit chose">Buy</button>
|
||||
</kit:Line>
|
||||
</each>
|
||||
<if test="{owned == 0}"><text>Nothing bought yet</text></if>
|
||||
<else><text>{owned} bought</text></else>
|
||||
</screen>
|
||||
</ui>
|
||||
```
|
||||
|
||||
- **Elements.** A template is HTML-shaped:
|
||||
- `div`, `section`, `header`, `footer`, `nav`, `main`, `article`, `aside`, `ul`, `ol`, `li` and
|
||||
`form` are boxes laid out in a column; `row` is one laid out in a row.
|
||||
- `p`, `span`, `label`, `h1`-`h6`, `strong`, `em`, `small`, `b`, `i` and `a` are text; words
|
||||
inside a box become a text of their own.
|
||||
- `button`, `img src`, `hr` and `spacer`; `scroll` is a column that scrolls.
|
||||
|
||||
A default stylesheet, like a browser's, sizes the headings and pads the buttons.
|
||||
- **Attributes.** `id`, `class` (which may be bound: `class="{picked ? 'on' : ''} row"`),
|
||||
`style="padding: 4px; color: gold"`, `hidden`, `disabled`, `onclick` or `on-click` (also
|
||||
`on-press`), and any attribute a property is named after (`width="300"`). Any other attribute is
|
||||
kept for `[attr=value]` selectors, as HTML's are.
|
||||
- **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`.
|
||||
- `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.
|
||||
- `gap`, `text-align`, `display: none`, `background(-color)`, `color`, `opacity` and `font-size`
|
||||
are CSS's.
|
||||
|
||||
Every property also has a short name (`w`, `h`, `pad`, `bg`, `size`, `grow`, `align`,
|
||||
`justify`, `self`, `dir`, `wrap`, `alpha`). Rounded corners, font weight and a few more are
|
||||
things the renderer does not draw, and the runtime says so rather than ignoring them. A colour
|
||||
is the renderer's name for one, `#rrggbb` or `#rgb`.
|
||||
- **Stylesheets.** Rules go in a `<style>` or in an `.lss` file (a Ludic StyleSheet: CSS-shaped,
|
||||
but its own language, so no editor mistakes it for CSS):
|
||||
|
||||
```
|
||||
@import "base.lss";
|
||||
button { height: 40px }
|
||||
.card p, #title { color: accent }
|
||||
ul > li:nth-child(even):not(.keep) { background: bg2 }
|
||||
[kind=warn] { border: 1px solid bad }
|
||||
```
|
||||
|
||||
- Selectors: a tag, `*`, `#id`, `.class`, `[attr]` or `[attr=value]`, and the pseudo-classes
|
||||
`:hover`, `:disabled`, `:enabled`, `:first-child`, `:last-child`, `:only-child`,
|
||||
`:nth-child(odd|even|n)`, `:empty`, `:root` and `:not(...)`. They combine into compounds,
|
||||
joined by a space (anywhere inside) or `>` (straight inside), and a list is `,`-separated.
|
||||
- Specificity is CSS's: ids 100, classes, attributes and pseudo-classes 10, tags 1. The default
|
||||
sheet comes first, then rules from least to most specific (the later rule on a tie), then
|
||||
`style="..."`, then the element's own attributes.
|
||||
- A value may hold `{holes}`, read where the element is.
|
||||
- A file's cascade is its imports' rules, then its own. `<import src="look.lss"/>` brings a
|
||||
stylesheet in, and one stylesheet can `@import` another, so a look is a file others can use.
|
||||
A library's components keep the styles of the file they were written in.
|
||||
- **Bindings.** Any attribute and any text can hold `{expressions}`. An attribute that is one
|
||||
`{expression}` and nothing else keeps its type. Expressions read loop names, props, state and the
|
||||
model, and support `.field`, `[index]`, arithmetic, comparisons, `and` / `or` / `not`,
|
||||
`c ? a : b`, `len()`, `range()`, and the view's functions.
|
||||
- **Structure.** `<if test>` with an `<else>` after it, and `<each in as index>`.
|
||||
- **Components.** `<component name="Line">` is used as `<Line ...>`. Its attributes become its
|
||||
props, and its content goes where it says `<slot/>`. It has its own `<state>`, kept between
|
||||
frames by where it sits in the tree. It sees its props, its state and the model, and never the
|
||||
names of whoever used it.
|
||||
- **Events.** `on-press="buy(i); set pick = i"` sends the view an event and sets a state. `emit
|
||||
chose` runs what the component's user gave as `on-chose`. The actions run once the frame is drawn,
|
||||
so no event can change a frame part way through.
|
||||
- **Libraries.** A component is private to its file unless it says `export="true"`. `<import
|
||||
src="kit.xml"/>` brings in a file's exported components under their own names; `as="kit"` brings
|
||||
them in as `<kit:Name>`. So a component library is a file of components, and two libraries never
|
||||
collide. Screens belong to the program and are found by name.
|
||||
- **The renderer is registered** (`ui_backend`). It must provide rectangles, text and a text's
|
||||
width. It can also provide its own button (focus, keys, sound), colour names, a scale, scroll
|
||||
regions and a file reader. `import "ludic.ui/screen.ludic"` gives the 2D screen's renderer
|
||||
(`ui_screen_backend()`). `ui_load(path)` reads a file, and `ui_show(screen, view_shop(), x, y, w,
|
||||
h)` shows a screen, answers the pointer and runs what was pressed. `ui_nodes`, `ui_place`,
|
||||
`ui_hit`, `ui_press` and `ui_dump` do the same steps one at a time, for a test.
|
||||
|
||||
### Memory is safe unless it says `unsafe`
|
||||
|
||||
The typed buffers are slices: `words(n)`, `floats(n)`, `fixeds(n)`, `doubles(n)` and
|
||||
|
|
|
|||
37
changes/defaults-views-templates.md
Normal file
37
changes/defaults-views-templates.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Default parameters, views and templates.** A parameter can have a default (`pad: float = 8.0`).
|
||||
A call leaves out what it does not change, and may pass its first arguments by position and the
|
||||
rest by name.
|
||||
|
||||
A `view` declaration is the one bridge between a program and its UI. It names the fields a
|
||||
template may read, the functions it may ask and the `on` events it may send, and it writes
|
||||
`view_<name>() -> UiView`.
|
||||
|
||||
`ludic.ui` is now a template runtime. Screens and components are XML files loaded at run time,
|
||||
with:
|
||||
- `{expression}` bindings;
|
||||
- `<if>`, `<else>` and `<each>`;
|
||||
- props, `<slot/>` and per-instance `<state>`;
|
||||
- `on-press` actions that send events, `set` state or `emit` to the component's user;
|
||||
- component libraries (`export="true"`, `<import src as>`);
|
||||
- HTML's elements (`div`, `p`, `h1`-`h6`, `ul`/`li`, `img`, `hr`, ...), with a default stylesheet;
|
||||
- HTML's attributes: `id`, `class`, `style`, `hidden`, `disabled` and `onclick`, with any other
|
||||
attribute kept for selectors;
|
||||
- the CSS box model (padding and margin in 1-4 values, borders, `px` and `%`) and flex layout
|
||||
(`flex-grow`, `justify-content`, `align-items`/`align-self`, `flex-wrap`, min and max sizes)
|
||||
under CSS's property names;
|
||||
- stylesheets, in a `<style>` or an `.lss` file (a Ludic StyleSheet) that others import and that
|
||||
can `@import` more;
|
||||
- CSS's selectors: `#id`, compound classes, `[attr=value]`, descendant and `>` combinators,
|
||||
`:hover`, `:disabled`, `:first-child`, `:last-child`, `:nth-child`, `:not` and more, weighed by
|
||||
specificity.
|
||||
|
||||
Screens are drawn through a registered renderer. `Value` gains a float kind.
|
||||
|
||||
Also:
|
||||
- A program's function named like one of the runtime's is refused; it had been silently taking the
|
||||
runtime's own calls.
|
||||
- An index is evaluated before the slice's elements are read. A `xs[f()]` whose `f` grew `xs`
|
||||
read stale memory.
|
||||
- A runtime error names the file its expression is in, not the program's.
|
||||
14
docs/language/value/value-as_float.md
Normal file
14
docs/language/value/value-as_float.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
id: value-as_float
|
||||
name: Value.as_float
|
||||
category: value
|
||||
kind: namespace-method
|
||||
tokens: Value.as_float
|
||||
sig: Value.as_float(value) -> float
|
||||
tip: Read a number node as a float.
|
||||
order: 37
|
||||
ns: Value
|
||||
member: as_float
|
||||
---
|
||||
|
||||
Returns the number any number node carries as a float: a <code>float</code> as it is, an <code>int</code> widened, a <code>fixed</code> converted from Q16.16. Other nodes return <code>0.0</code>.
|
||||
|
|
@ -11,4 +11,4 @@ ns: Value
|
|||
member: as_int
|
||||
---
|
||||
|
||||
Returns the raw scalar an int/fixed/bool node carries (a <code>fixed</code> comes back as its Q16.16 bits). Non-scalar nodes return <code>0</code>.
|
||||
Returns the raw scalar an int/fixed/bool node carries, or a float node's whole part (a <code>fixed</code> comes back as its Q16.16 bits). Non-scalar nodes return <code>0</code>.
|
||||
|
|
|
|||
14
docs/language/value/value-float.md
Normal file
14
docs/language/value/value-float.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
id: value-float
|
||||
name: Value.float
|
||||
category: value
|
||||
kind: namespace-method
|
||||
tokens: Value.float
|
||||
sig: Value.float(x: float) -> value
|
||||
tip: Wrap a float in a value node.
|
||||
order: 36
|
||||
ns: Value
|
||||
member: float
|
||||
---
|
||||
|
||||
Builds a <code>float</code> node (kind <code>7</code>) carrying <code>x</code>. Read it back with <a href="value-as_float"><code>Value.as_float</code></a>; <a href="value-as_int"><code>Value.as_int</code></a> gives its whole part. <code>Json.encode</code> writes it as a number.
|
||||
18
examples/lang/defaults.ludic
Normal file
18
examples/lang/defaults.ludic
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# defaults.ludic — L11: a parameter can have a default, and a call leaves out what it does not
|
||||
# change - from the end when positional, or by not naming it. The leading arguments can be
|
||||
# positional and the rest named, which is how a component is called: its subject, then its props.
|
||||
#
|
||||
# Running it prints: a 300.0 8.0 / b 120.0 8.0 / c! 300.0 8.0 / d 300.0 2.0 / x / y
|
||||
program Defaults {
|
||||
numbers float
|
||||
const WIDE: float = 300.0
|
||||
function box(label: string, w: float = WIDE, pad: float = 8.0, bold: bool = false) -> string {
|
||||
var b = ""
|
||||
if bold { b = "!" }
|
||||
return `{label}{b} {w} {pad}`
|
||||
}
|
||||
function first<T>(xs: []T, at: int = 0) -> T { return xs[at] }
|
||||
entry {
|
||||
print(`{box("a")} / {box("b", 120.0)} / {box("c", bold: true)} / {box(pad: 2.0, label: "d")} / {first(["x", "y"])} / {first(["x", "y"], 1)}`)
|
||||
}
|
||||
}
|
||||
25
examples/library/ui_flex.ludic
Normal file
25
examples/library/ui_flex.ludic
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# ui_flex.ludic — ludic.ui's flex layout and stylesheets without a renderer: grow shares the spare
|
||||
# room, justify spreads it, a row wraps, align stretches; a stylesheet file (which @imports another)
|
||||
# styles whoever imports it, a more specific rule wins, a bound class switches one on, and an attribute beats all.
|
||||
import "ludic.ui"
|
||||
program UiFlex {
|
||||
numbers float
|
||||
var picked: int = 1
|
||||
view Tags {
|
||||
picked = picked
|
||||
}
|
||||
const BASE: string = "/* base.lss - every text small */\ntext { size: 10 }\n"
|
||||
const THEME: string = "/* theme.lss - the base, and a colour for what is on */\n@import \"base.lss\";\n.on { color: #ff0000 }\n"
|
||||
const PAGE: string = "<ui>\n<import src='theme.lss'/>\n<style>\nrow.bar { gap: 10; pad: 5 }\ntext.big { size: {picked * 20} }\n</style>\n<screen name='flex' gap='8'>\n <row class='bar' w='fill'>\n <text grow='1'>a</text><text grow='3'>b</text>\n </row>\n <row w='fill' justify='between'><text>l</text><text>m</text><text>r</text></row>\n <row w='200' wrap='true' gap='4'>\n <each in='{range(6)}' as='i'><text w='60' class='{i == picked ? \"on\" : \"\"}'>t{i}</text></each>\n </row>\n <row h='40' align='stretch' w='fill'><col w='30' bg='#00ff00'/><text self='end' class='big' size='12'>x</text></row>\n</screen>\n</ui>\n"
|
||||
entry {
|
||||
ui_load_text(BASE, "ui/base.lss")
|
||||
ui_load_text(THEME, "ui/theme.lss")
|
||||
ui_load_text(PAGE, "ui/flex.xml")
|
||||
let root: UiNode = ui_nodes("flex", view_tags())
|
||||
ui_place(root, 0.0, 0.0, 400.0, 300.0)
|
||||
let bar = root.children[0]
|
||||
let wrap = root.children[2]
|
||||
let tall = root.children[3]
|
||||
print(`grow {int(bar.children[0].cw)}:{int(bar.children[1].cw)}; between at {int(root.children[1].children[1].x)}; wrap lines at {int(wrap.children[0].y)}, {int(wrap.children[3].y)}; red {wrap.children[1].fg}; stretched {int(tall.children[0].ch)}; own size {int(tall.children[1].rsize)}`)
|
||||
}
|
||||
}
|
||||
28
examples/library/ui_html.ludic
Normal file
28
examples/library/ui_html.ludic
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# ui_html.ludic — ludic.ui read as HTML and CSS: HTML's elements with a browser's defaults, ids,
|
||||
# classes, style="...", hidden and disabled, onclick; selectors with ids, descendants, children,
|
||||
# attributes and pseudo-classes, weighed by specificity; CSS's own property names and the box model.
|
||||
import "ludic.ui"
|
||||
program UiHtml {
|
||||
numbers float
|
||||
var clicks: int = 0
|
||||
view Page {
|
||||
clicks = clicks
|
||||
on clicked() { clicks += 1 }
|
||||
}
|
||||
const LOOK: string = "/* look.lss */\n.card { padding: 4 8 12 16; margin: 3; border: 2px solid #0000ff; width: 50% }\n#title { font-size: 40 }\nh1.big { font-size: 20 }\n.card p { color: #00ff00 }\nul > li { height: 30 }\nli:first-child { color: #ff0000 }\nli:nth-child(even) { color: #0000ff }\nli:last-child:not(.keep) { display: none }\n[kind=warn] { background-color: #ffff00 }\nbutton:disabled { opacity: 0.5 }\n"
|
||||
const PAGE: string = "<ui>\n<import src='look.lss'/>\n<screen name='page'>\n <h1 id='title' class='big'>Title</h1>\n <div class='card'><p>inside</p><span kind='warn'>careful</span></div>\n <p>outside</p>\n <ul><li>one</li><li>two</li><li>three</li><li>gone</li></ul>\n <hr/>\n <p style='text-align: center; width: 200px'>mid</p>\n <button disabled='{clicks > 0}' onclick='clicked()'>Go</button>\n <p hidden='true'>never</p>\n</screen>\n</ui>\n"
|
||||
entry {
|
||||
ui_load_text(LOOK, "ui/look.lss")
|
||||
ui_load_text(PAGE, "ui/page.ludic.xml")
|
||||
var root: UiNode = ui_nodes("page", view_page())
|
||||
ui_place(root, 0.0, 0.0, 400.0, 600.0)
|
||||
let card = root.children[1]
|
||||
let items = root.children[3].children
|
||||
print(`id beats class: {int(root.children[0].rsize)}; card {int(card.cw)} wide at {int(card.x)},{int(card.y)} pad {int(card.pt)} {int(card.pr)} {int(card.pb)} {int(card.pl)} border {int(card.border)}`)
|
||||
print(`inside {card.children[0].fg}, outside {root.children[2].fg}; warn bg {card.children[1].bg}; items {len(items)} of 30: {int(items[0].ch)}; colours {items[0].fg} {items[1].fg} {items[2].fg}; hr {int(root.children[4].ch)}; children {len(root.children)}`)
|
||||
ui_press(root.children[6])
|
||||
root = ui_nodes("page", view_page())
|
||||
ui_place(root, 0.0, 0.0, 400.0, 600.0)
|
||||
print(`clicks {clicks}; the button now disabled {not root.children[6].enabled} at opacity {root.children[6].alpha}; text-align {root.children[5].talign}`)
|
||||
}
|
||||
}
|
||||
48
examples/library/ui_templates.ludic
Normal file
48
examples/library/ui_templates.ludic
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# ui_templates.ludic — ludic.ui without a renderer: a shop written as a template, reading a view
|
||||
# of the program's state and answering with its events. A press is made as a pointer would make
|
||||
# it; it runs the view's event once the frame is built, and a component's own state keeps. The
|
||||
# row comes from a component library, whose unexported Price no other file can use.
|
||||
import "ludic.ui"
|
||||
program UiTemplates {
|
||||
numbers float
|
||||
property Item {
|
||||
name: string = ""
|
||||
cost: int = 0
|
||||
}
|
||||
var stock: []Item = new []Item
|
||||
var purse: int = 100
|
||||
var bought: []int = new []int
|
||||
function add(name: string, cost: int) -> void {
|
||||
let it = new Item
|
||||
it.name = name
|
||||
it.cost = cost
|
||||
push(stock, it)
|
||||
}
|
||||
view Shop {
|
||||
purse = purse
|
||||
stock = stock
|
||||
owned: int = len(bought)
|
||||
function afford(i: int) -> bool { return purse >= stock[i].cost }
|
||||
on buy(i: int) {
|
||||
purse -= stock[i].cost
|
||||
push(bought, i)
|
||||
}
|
||||
}
|
||||
const KIT: string = "<ui>\n<component name='Price'><text>${cost}</text></component>\n<component name='Line' export='true'>\n <state picks='{0}'/>\n <row gap='8' w='fill' align='center'>\n <text color='{picked ? \"#ffcc00\" : \"#eeeeee\"}'>{item.name} -</text>\n <Price cost='{item.cost}'/>\n <spacer w='fill'/>\n <text>{picks}</text>\n <button enabled='{afford(i)}' on-press='buy(i); set picks = picks + 1; emit chose'>Buy</button>\n </row>\n</component>\n</ui>\n"
|
||||
const PAGE: string = "<ui>\n<import src='kit.xml' as='kit'/>\n<screen name='shop' gap='4' pad='10'>\n <state pick='{-1}'/>\n <text size='24'>The purse: ${purse}</text>\n <each in='{stock}' as='item' index='i'>\n <kit:Line item='{item}' i='{i}' picked='{pick == i}' on-chose='set pick = i'/>\n </each>\n <if test='{owned == 0}'><text>Nothing bought yet</text></if>\n <else><text>{owned} bought, the last {pick + 1}</text></else>\n</screen>\n</ui>\n"
|
||||
const PRIVATE: string = "<ui><import src='kit.xml' as='kit'/><screen name='peek'><kit:Price cost='{1}'/></screen></ui>"
|
||||
entry {
|
||||
add("Rope", 30)
|
||||
add("Lantern", 80)
|
||||
ui_load_text(KIT, "ui/kit.xml")
|
||||
ui_load_text(PAGE, "ui/shop.xml")
|
||||
ui_load_text(PRIVATE, "ui/peek.xml")
|
||||
var root: UiNode = ui_nodes("shop", view_shop())
|
||||
ui_place(root, 0.0, 0.0, 600.0, 400.0)
|
||||
let press = root.children[1].children[4]
|
||||
ui_press(ui_hit(root, press.x + 1.0, press.y + 1.0))
|
||||
root = ui_nodes("shop", view_shop())
|
||||
ui_place(root, 0.0, 0.0, 600.0, 400.0)
|
||||
print(`purse {purse}, bought {len(bought)}; the lantern is enabled: {root.children[2].children[4].enabled}`)
|
||||
}
|
||||
}
|
||||
5
examples/rejected/default_missing.ludic
Normal file
5
examples/rejected/default_missing.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# L11: a call leaves out only what has a default
|
||||
program DefaultMissing {
|
||||
function f(a: int, b: int = 2) -> int { return a + b }
|
||||
entry { print(f(b: 3)) }
|
||||
}
|
||||
5
examples/rejected/default_unknown.ludic
Normal file
5
examples/rejected/default_unknown.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# L11: a named argument names a parameter the function has
|
||||
program DefaultUnknown {
|
||||
function f(a: int, b: int = 2) -> int { return a + b }
|
||||
entry { print(f(1, c: 3)) }
|
||||
}
|
||||
5
examples/rejected/positional_after_named.ludic
Normal file
5
examples/rejected/positional_after_named.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# L11: positional arguments come first, then named ones
|
||||
program PositionalAfterNamed {
|
||||
function f(a: int, b: int = 2) -> int { return a + b }
|
||||
entry { print(f(a: 1, 2)) }
|
||||
}
|
||||
9
examples/rejected/view_untyped.ludic
Normal file
9
examples/rejected/view_untyped.ludic
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# a view field that is not a bare global has to say its type
|
||||
import "ludic.ui"
|
||||
program ViewUntyped {
|
||||
var a: int = 1
|
||||
view Sums {
|
||||
total = a + 2
|
||||
}
|
||||
entry { print(view_sums().name) }
|
||||
}
|
||||
8
examples/rejected/view_without_ui.ludic
Normal file
8
examples/rejected/view_without_ui.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# a view is for templates, which ludic.ui shows
|
||||
program ViewWithoutUi {
|
||||
var a: int = 1
|
||||
view Sums {
|
||||
a = a
|
||||
}
|
||||
entry { print(a) }
|
||||
}
|
||||
62
packages/ludic.ui/act.ludic
Normal file
62
packages/ludic.ui/act.ludic
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# act.ludic - what a press does: `buy(i)` asks the view, `set pick = i` changes a state, and
|
||||
# `emit picked` runs what the component's user said with on-picked. Several are `;` apart.
|
||||
const A_CALL: int = 0
|
||||
const A_SET: int = 1
|
||||
const A_EMIT: int = 2
|
||||
export property UiAct {
|
||||
kinds: []int = null
|
||||
names: []string = null
|
||||
exprs: []UiExpr = null
|
||||
}
|
||||
function act_parse(src: string, err: []string) -> UiAct {
|
||||
let a = new UiAct
|
||||
a.kinds = new []int
|
||||
a.names = new []string
|
||||
a.exprs = new []UiExpr
|
||||
let r = new UiRd
|
||||
r.s = src
|
||||
while r.err == "" {
|
||||
rd_ws(r)
|
||||
if r.i >= len(r.s) { break }
|
||||
if rd_word(r, "set") {
|
||||
push(a.kinds, A_SET)
|
||||
push(a.names, rd_name(r))
|
||||
if not rd_op(r, "=") { rd_fail(r, "'='") }
|
||||
push(a.exprs, px_cond(r))
|
||||
} else if rd_word(r, "emit") {
|
||||
push(a.kinds, A_EMIT)
|
||||
push(a.names, rd_name(r))
|
||||
push(a.exprs, null)
|
||||
} else {
|
||||
push(a.kinds, A_CALL)
|
||||
push(a.names, "")
|
||||
push(a.exprs, px_cond(r))
|
||||
}
|
||||
if not rd_op(r, ";") { break }
|
||||
}
|
||||
rd_ws(r)
|
||||
if r.err == "" and r.i < len(r.s) { rd_fail(r, "';' or the end") }
|
||||
if r.err != "" { push(err, r.err) }
|
||||
return a
|
||||
}
|
||||
function act_run(a: UiAct, e: UiEnv) -> void {
|
||||
for i in 0 .. len(a.kinds) {
|
||||
if a.kinds[i] == A_SET { env_set(e, a.names[i], ui_eval(a.exprs[i], e)) }
|
||||
if a.kinds[i] == A_CALL { ui_eval(a.exprs[i], e) }
|
||||
if a.kinds[i] == A_EMIT { act_emit(a.names[i], e) }
|
||||
}
|
||||
}
|
||||
function act_emit(name: string, e: UiEnv) -> void {
|
||||
var at = e
|
||||
while at != null {
|
||||
if at.on_names != null {
|
||||
for i in 0 .. len(at.on_names) {
|
||||
if at.on_names[i] == name {
|
||||
act_run(at.on_acts[i], at.on_envs[i])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
at = at.parent
|
||||
}
|
||||
}
|
||||
58
packages/ludic.ui/attr.ludic
Normal file
58
packages/ludic.ui/attr.ludic
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# 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 = " border-radius font-weight font-family font-style box-shadow text-shadow cursor transition line-height letter-spacing text-decoration overflow z-index position "
|
||||
# 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 }
|
||||
if lk_in(AT_UNDRAWN, at_alias(key)) { ui_err(`{key}: this renderer does not draw it`) } else { ui_err(`{key}: there is no such property`) }
|
||||
}
|
||||
# an element's attribute: a property when it names one, otherwise only there for [selectors]
|
||||
function el_apply(n: UiNode, key: string, v: Val) -> bool {
|
||||
let k = at_alias(key)
|
||||
return el_size(n, k, v) or el_box(n, k, v) or el_flex(n, k, v) or el_look(n, k, v)
|
||||
}
|
||||
function at_alias(key: string) -> string {
|
||||
let pat = ` {key}=`
|
||||
let n = len(pat)
|
||||
for i in 0 .. len(AT_ALIAS) - n + 1 {
|
||||
if AT_ALIAS[i..i + n] == pat {
|
||||
var j = i + n
|
||||
while AT_ALIAS[j] != 32 { j += 1 }
|
||||
return AT_ALIAS[i + n..j]
|
||||
}
|
||||
}
|
||||
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 }
|
||||
return true
|
||||
}
|
||||
function el_flex(n: UiNode, key: string, v: Val) -> bool {
|
||||
let s = vl_text(v)
|
||||
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 }
|
||||
return true
|
||||
}
|
||||
function el_look(n: UiNode, key: string, v: Val) -> bool {
|
||||
let s = vl_text(v)
|
||||
if key == "size" { n.size = el_len(v) } else if key == "color" { n.fg = ui_color(s) } else if key == "bg" { n.bg = ui_color(s) } else if key == "alpha" { n.alpha = el_num(s) } else if key == "enabled" { n.enabled = vl_truthy(v) and s != "false" } else if key == "border-color" { n.border_c = ui_color(s) } else if key == "border-width" { n.border = el_len(v) } else if key == "border" { el_border(n, s) } else { return false }
|
||||
return true
|
||||
}
|
||||
# border: 1 #ffcc00, or 1px solid accent
|
||||
function el_border(n: UiNode, s: string) -> void {
|
||||
let parts = lss_split(s, 32)
|
||||
for i in 0 .. len(parts) {
|
||||
let p = parts[i]
|
||||
if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { n.border = ui_px(el_num(p)) } else if p == "none" { n.border = 0.0 } else if len(p) > 0 and p != "solid" { n.border_c = ui_color(p) }
|
||||
}
|
||||
if n.border_c == UI_NONE { n.border_c = n.rfg }
|
||||
}
|
||||
81
packages/ludic.ui/attr_box.ludic
Normal file
81
packages/ludic.ui/attr_box.ludic
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# attr_box.ludic - padding and margin (one to four lengths, as CSS writes them, or one side by
|
||||
# name), lengths with px or %, and the words that place things
|
||||
function el_box(n: UiNode, key: string, v: Val) -> bool {
|
||||
if key == "pad" {
|
||||
el_sides(v)
|
||||
n.pt = el_s0
|
||||
n.pr = el_s1
|
||||
n.pb = el_s2
|
||||
n.pl = el_s3
|
||||
} else if key == "margin" {
|
||||
el_sides(v)
|
||||
n.mt = el_s0
|
||||
n.mr = el_s1
|
||||
n.mb = el_s2
|
||||
n.ml = el_s3
|
||||
} else if key == "padding-top" { n.pt = el_len(v) } else if key == "padding-right" { n.pr = el_len(v) } else if key == "padding-bottom" { n.pb = el_len(v) } else if key == "padding-left" { n.pl = el_len(v) } else if key == "margin-top" { n.mt = el_len(v) } else if key == "margin-right" { n.mr = el_len(v) } else if key == "margin-bottom" { n.mb = el_len(v) } else if key == "margin-left" { n.ml = el_len(v) } else { return false }
|
||||
return true
|
||||
}
|
||||
var el_s0: float = 0.0
|
||||
var el_s1: float = 0.0
|
||||
var el_s2: float = 0.0
|
||||
var el_s3: float = 0.0
|
||||
# top right bottom left out of 1 to 4 lengths
|
||||
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)
|
||||
for i in 0 .. len(words) {
|
||||
if len(words[i]) > 0 { push(parts, ui_px(el_num(words[i]))) }
|
||||
}
|
||||
}
|
||||
while len(parts) < 4 {
|
||||
if len(parts) == 0 { push(parts, 0.0) } else if len(parts) == 1 { push(parts, parts[0]) } else if len(parts) == 2 { push(parts, parts[0]) } else { push(parts, parts[1]) }
|
||||
}
|
||||
el_s0 = parts[0]
|
||||
el_s1 = parts[1]
|
||||
el_s2 = parts[2]
|
||||
el_s3 = parts[3]
|
||||
}
|
||||
# a length: a number, 10px, 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 s == "fit" or s == "auto" or (len(s) > 0 and s[len(s) - 1] == 37) { return 0.0 }
|
||||
return ui_px(el_num(s))
|
||||
}
|
||||
function el_pct(v: Val) -> float {
|
||||
let s = vl_text(v)
|
||||
if len(s) > 1 and s[len(s) - 1] == 37 { return el_num(s) }
|
||||
return 0.0
|
||||
}
|
||||
# the number at the front of `12.5px`
|
||||
function el_num(s: string) -> float {
|
||||
var f = 0.0
|
||||
var unit = 0.0
|
||||
var neg = false
|
||||
for i in 0 .. len(s) {
|
||||
let c = s[i]
|
||||
if c == 45 and i == 0 { neg = true } else if c == 46 { unit = 0.1 } else if c >= 48 and c <= 57 {
|
||||
if unit > 0.0 {
|
||||
f = f + float(c - 48) * unit
|
||||
unit = unit * 0.1
|
||||
} else { f = f * 10.0 + float(c - 48) }
|
||||
} else { break }
|
||||
}
|
||||
if neg { return 0.0 - f }
|
||||
return f
|
||||
}
|
||||
# start, center, end, stretch; for justify also between, around, evenly - with CSS's spellings
|
||||
function el_place(v: Val) -> int {
|
||||
var s = vl_text(v)
|
||||
if len(s) > 6 and s[0..6] == "space-" { s = s[6..len(s)] }
|
||||
if len(s) > 5 and s[0..5] == "flex-" { s = s[5..len(s)] }
|
||||
if s == "center" { return UI_CENTER }
|
||||
if s == "end" or s == "right" { return UI_END }
|
||||
if s == "stretch" or s == "between" { return 3 }
|
||||
if s == "around" { return UI_AROUND }
|
||||
if s == "evenly" { return UI_EVENLY }
|
||||
return UI_START
|
||||
}
|
||||
26
packages/ludic.ui/backend.ludic
Normal file
26
packages/ludic.ui/backend.ludic
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# backend.ludic - the renderer seam
|
||||
# the renderer. Only rect, text and measure are needed; the rest improve on a default:
|
||||
# button draws, focuses and answers a press itself; color names a colour (else #rrggbb);
|
||||
# image draws an <img src>; px scales a template's lengths; scroll_begin answers how far a region is scrolled; read
|
||||
# reads a template file (else Fs.read_text)
|
||||
export property UiBackend {
|
||||
rect: fn(float, float, float, float, int, float) -> void = null
|
||||
text: fn(float, float, string, float, int) -> void = null
|
||||
measure: fn(string, float) -> float = null
|
||||
pointer_x: fn() -> float = null
|
||||
pointer_y: fn() -> float = null
|
||||
pointer_down: fn() -> bool = null
|
||||
button: fn(float, float, float, float, string, bool) -> bool = null
|
||||
image: fn(string, float, float, float, float, int, float) -> void = null # src x y w h tint alpha
|
||||
color: fn(string) -> int = null
|
||||
px: fn(float) -> float = null
|
||||
scroll_begin: fn(string, float, float, float, float, float) -> float = null
|
||||
scroll_end: fn() -> void = null
|
||||
read: fn(string) -> string = null
|
||||
}
|
||||
var ui_be: UiBackend = null
|
||||
export function ui_backend(b: UiBackend) -> void { ui_be = b }
|
||||
function ui_px(v: float) -> float {
|
||||
if ui_be != null and ui_be.px != null { return ui_be.px(v) }
|
||||
return v
|
||||
}
|
||||
68
packages/ludic.ui/build.ludic
Normal file
68
packages/ludic.ui/build.ludic
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# build.ludic - a template and the model, made into this frame's nodes. <if>/<else> and
|
||||
# <each in= as= index=> are the template's own; a tag its file linked to a component is that one,
|
||||
# given its attributes as props, its on-* as what `emit` runs, and its content as <slot/>.
|
||||
function bd_kids(kids: []UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
|
||||
var last_if = true
|
||||
for i in 0 .. len(kids) {
|
||||
let k = kids[i]
|
||||
let ck = `{key}/{i}`
|
||||
if k.tag == "state" { continue }
|
||||
if k.tag == "if" {
|
||||
last_if = bd_test(k, e)
|
||||
if last_if { bd_kids(k.kids, e, ck, out) }
|
||||
} else if k.tag == "else" {
|
||||
if not last_if { bd_kids(k.kids, e, ck, out) }
|
||||
last_if = true
|
||||
} else if k.tag == "each" { bd_each(k, e, ck, out) } else if k.tag == "slot" { bd_slot(e, ck, out) } else if not bd_comp(k, e, ck, out) { push(out, bd_elem(k, e, ck)) }
|
||||
}
|
||||
}
|
||||
function bd_test(k: UiTpl, e: UiEnv) -> bool {
|
||||
let t = tpl_attr(k, "test")
|
||||
if t == null {
|
||||
ui_err("<if> needs a test=\"{...}\"")
|
||||
return false
|
||||
}
|
||||
return ui_eval_bool(t, e)
|
||||
}
|
||||
function bd_name(k: UiTpl, attr: string, dflt: string) -> string {
|
||||
let x = tpl_attr(k, attr)
|
||||
if x == null or x.op != X_LIT { return dflt }
|
||||
return Value.as_str(x.v)
|
||||
}
|
||||
function bd_each(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
|
||||
let src = tpl_attr(k, "in")
|
||||
if src == null {
|
||||
ui_err("<each> needs an in=\"{...}\"")
|
||||
return
|
||||
}
|
||||
let list = ui_eval(src, e)
|
||||
let as_name = bd_name(k, "as", "item")
|
||||
let at_name = bd_name(k, "index", "index")
|
||||
for j in 0 .. Value.count(list) {
|
||||
let le = env_new(e)
|
||||
env_bind(le, as_name, Value.at(list, j))
|
||||
env_bind(le, at_name, Value.int(j))
|
||||
bd_kids(k.kids, le, `{key}#{j}`, out)
|
||||
}
|
||||
}
|
||||
function bd_slot(e: UiEnv, key: string, out: []UiNode) -> void {
|
||||
var at = e
|
||||
while at != null and at.slot == null { at = at.parent }
|
||||
if at != null { bd_kids(at.slot.kids, at.slot_env, key, out) }
|
||||
}
|
||||
function bd_comp(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
|
||||
if k.comp == null { return false }
|
||||
let c = k.comp
|
||||
let ce = env_new(null)
|
||||
ce.model = e.model
|
||||
for i in 0 .. len(k.keys) { env_bind(ce, k.keys[i], ui_eval(k.vals[i], e)) }
|
||||
ce.on_names = k.on_keys
|
||||
ce.on_acts = k.on_acts
|
||||
ce.on_envs = new []UiEnv
|
||||
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
|
||||
ce.slot = k
|
||||
ce.slot_env = e
|
||||
ce.state = st_for(key, c, ce)
|
||||
bd_kids(c.kids, ce, key, out)
|
||||
return true
|
||||
}
|
||||
44
packages/ludic.ui/cascade.ludic
Normal file
44
packages/ludic.ui/cascade.ludic
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# cascade.ludic - once the frame's tree is built, every element is styled as a browser would:
|
||||
# the default sheet, then the file's rules from least to most specific, then its style="...", then
|
||||
# its own attributes. Then what is display: none leaves the tree.
|
||||
function cs_tree(n: UiNode) -> void {
|
||||
for i in 0 .. len(n.children) {
|
||||
let c = n.children[i]
|
||||
c.parent = n
|
||||
c.index = i
|
||||
}
|
||||
if n.tag != "" { cs_node(n) }
|
||||
for i in 0 .. len(n.children) { cs_tree(n.children[i]) }
|
||||
var shown = new []UiNode
|
||||
for i in 0 .. len(n.children) {
|
||||
if not n.children[i].hidden { push(shown, n.children[i]) }
|
||||
}
|
||||
n.children = shown
|
||||
}
|
||||
function cs_node(n: UiNode) -> void {
|
||||
cs_sheet(n, ua_sheet())
|
||||
if n.sheet != null { cs_sheet(n, n.sheet) }
|
||||
if n.style != null { cs_rule(n, n.style) }
|
||||
for i in 0 .. len(n.akeys) { el_apply(n, n.akeys[i], n.vals[i]) }
|
||||
}
|
||||
function cs_sheet(n: UiNode, sh: UiSheet) -> void {
|
||||
for i in 0 .. len(sh.rules) {
|
||||
if sel_match(sh.rules[i].sel, n) { cs_rule(n, sh.rules[i]) }
|
||||
}
|
||||
}
|
||||
function cs_rule(n: UiNode, r: UiRule) -> void {
|
||||
for j in 0 .. len(r.keys) { el_attr(n, r.keys[j], ui_eval(r.vals[j], n.env)) }
|
||||
}
|
||||
# what every element looks like before anyone styles it, as a browser's own sheet does
|
||||
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 } button { padding: 5 10 } hr { height: 1; width: fill; background: #808080; margin: 4 0 } scroll { width: fill; height: fill } img { width: 24; height: 24 }"
|
||||
var ua_sh: UiSheet = null
|
||||
function ua_sheet() -> UiSheet {
|
||||
if ua_sh != null { return ua_sh }
|
||||
let f = new UiFile
|
||||
f.path = "the default sheet"
|
||||
f.imp = new []UiFile
|
||||
f.own = new []UiRule
|
||||
lss_text(f, UA_LSS)
|
||||
ua_sh = sheet_of(f)
|
||||
return ua_sh
|
||||
}
|
||||
66
packages/ludic.ui/draw.ludic
Normal file
66
packages/ludic.ui/draw.ludic
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# draw.ludic - the laid-out nodes through the renderer: background, border, then content
|
||||
function fr_draw(n: UiNode) -> void {
|
||||
if n.kind == UI_BUTTON {
|
||||
fr_button(n)
|
||||
return
|
||||
}
|
||||
if n.bg >= 0 and ui_be.rect != null { ui_be.rect(n.x, n.y, n.cw, n.ch, n.bg, n.alpha) }
|
||||
fr_border(n)
|
||||
if n.kind == UI_TEXT { ui_be.text(fr_text_x(n), n.y + n.pt + n.border, n.text, n.rsize, n.rfg) }
|
||||
if n.kind == UI_IMAGE and ui_be.image != null { ui_be.image(n.src, n.x + n.pl + n.border, n.y + n.pt + n.border, n.cw - ly_hin(n), n.ch - ly_vin(n), n.rfg, n.alpha) }
|
||||
var off = 0.0
|
||||
if n.kind == UI_SCROLL and ui_be.scroll_begin != null { off = ui_be.scroll_begin(n.key, n.x, n.y, n.cw, n.ch, n.mh) }
|
||||
if off != 0.0 {
|
||||
for i in 0 .. len(n.children) { fr_shift(n.children[i], off) }
|
||||
}
|
||||
if n.children != null {
|
||||
for i in 0 .. len(n.children) { fr_draw(n.children[i]) }
|
||||
}
|
||||
if n.kind == UI_SCROLL and ui_be.scroll_end != null { ui_be.scroll_end() }
|
||||
}
|
||||
# where a text starts: text-align within the room the box gives it
|
||||
function fr_text_x(n: UiNode) -> float {
|
||||
let x = n.x + n.pl + n.border
|
||||
if n.talign == UI_START { return x }
|
||||
let spare = n.cw - ly_hin(n) - ly_text_w(n.text, n.rsize)
|
||||
if n.talign == UI_CENTER { return x + spare / 2.0 }
|
||||
return x + spare
|
||||
}
|
||||
function fr_border(n: UiNode) -> void {
|
||||
if n.border <= 0.0 or ui_be.rect == null { return }
|
||||
var c = n.border_c
|
||||
if c < 0 { c = n.rfg }
|
||||
let b = n.border
|
||||
ui_be.rect(n.x, n.y, n.cw, b, c, n.alpha)
|
||||
ui_be.rect(n.x, n.y + n.ch - b, n.cw, b, c, n.alpha)
|
||||
ui_be.rect(n.x, n.y, b, n.ch, c, n.alpha)
|
||||
ui_be.rect(n.x + n.cw - b, n.y, b, n.ch, c, n.alpha)
|
||||
}
|
||||
# the renderer's own button when it has one: it draws, focuses and answers the press
|
||||
function fr_button(n: UiNode) -> void {
|
||||
if ui_be.button != null {
|
||||
if ui_be.button(n.x, n.y, n.cw, n.ch, n.text, n.enabled) and n.enabled { push(fr_pressed, n) }
|
||||
return
|
||||
}
|
||||
var bg = n.bg
|
||||
if bg < 0 { bg = 4210752 }
|
||||
if ui_be.rect != null { ui_be.rect(n.x, n.y, n.cw, n.ch, bg, n.alpha) }
|
||||
fr_border(n)
|
||||
ui_be.text(fr_text_x(n), n.y + n.pt + n.border, n.text, n.rsize, n.rfg)
|
||||
}
|
||||
function fr_shift(n: UiNode, off: float) -> void {
|
||||
n.y = n.y - off
|
||||
if n.children != null {
|
||||
for i in 0 .. len(n.children) { fr_shift(n.children[i], off) }
|
||||
}
|
||||
}
|
||||
# every node and where it went, one line each - for a test, or a look at a layout
|
||||
export function ui_dump(n: UiNode, depth: int) -> string {
|
||||
var pad = ""
|
||||
for d in 0 .. depth { pad = pad + " " }
|
||||
var out = `{pad}{n.kind} {int(n.x)},{int(n.y)} {int(n.cw)}x{int(n.ch)} {n.text}\n`
|
||||
if n.children != null {
|
||||
for i in 0 .. len(n.children) { out = out + ui_dump(n.children[i], depth + 1) }
|
||||
}
|
||||
return out
|
||||
}
|
||||
77
packages/ludic.ui/elem.ludic
Normal file
77
packages/ludic.ui/elem.ludic
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# elem.ludic - an element made from its template: what kind of node its tag is, its id, classes
|
||||
# and attributes (kept for selectors, applied after every rule), its text, and its children
|
||||
const EL_BOXES: string = " col div section header footer nav main article aside ul ol li form box "
|
||||
const EL_TEXTS: string = " text span p label h1 h2 h3 h4 h5 h6 strong em small b i a "
|
||||
function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
|
||||
let n = new UiNode
|
||||
n.key = key
|
||||
n.tag = k.tag
|
||||
n.env = e
|
||||
n.sheet = k.sheet
|
||||
n.style = k.style
|
||||
n.children = new []UiNode
|
||||
n.classes = new []string
|
||||
n.akeys = new []string
|
||||
n.avals = new []string
|
||||
n.vals = new []Val
|
||||
el_kind(n, k.tag)
|
||||
for i in 0 .. len(k.keys) { el_keep(n, k.keys[i], ui_eval(k.vals[i], e)) }
|
||||
n.hovered = fr_was_hovered(key)
|
||||
for i in 0 .. len(k.on_keys) {
|
||||
if k.on_keys[i] == "press" {
|
||||
n.act = k.on_acts[i]
|
||||
n.env = e
|
||||
} else { ui_err(`on-{k.on_keys[i]}: an element answers on-press (or onclick); a component's other events are its user's`) }
|
||||
}
|
||||
bd_kids(k.kids, e, key, n.children)
|
||||
if k.text != null { el_text(n, ui_eval_text(k.text, e)) }
|
||||
return n
|
||||
}
|
||||
function el_kind(n: UiNode, tag: string) -> void {
|
||||
if lk_in(EL_TEXTS, tag) { n.kind = UI_TEXT } else if tag == "row" { n.dir = UI_ROW } else if tag == "button" { n.kind = UI_BUTTON } else if tag == "spacer" { n.kind = UI_SPACER } else if tag == "scroll" { n.kind = UI_SCROLL } else if tag == "img" { n.kind = UI_IMAGE } else if tag == "hr" { n.kind = UI_RULE } else if not lk_in(EL_BOXES, tag) { ui_err(`<{tag}>: there is no such element`) }
|
||||
}
|
||||
# a box with words in it holds them as a text of its own, as <div>words</div> does
|
||||
function el_text(n: UiNode, s: string) -> void {
|
||||
if n.kind != UI_BOX and n.kind != UI_SCROLL {
|
||||
n.text = s
|
||||
return
|
||||
}
|
||||
let t = new UiNode
|
||||
t.kind = UI_TEXT
|
||||
t.text = s
|
||||
t.env = n.env
|
||||
t.children = new []UiNode
|
||||
push(n.children, t)
|
||||
}
|
||||
# id, class, hidden, disabled and src say what the element is; the rest are applied after the rules
|
||||
function el_keep(n: UiNode, key: string, v: Val) -> void {
|
||||
let s = vl_text(v)
|
||||
if key == "id" { n.id = s } else if key == "class" {
|
||||
let parts = lss_split(s, 32)
|
||||
for i in 0 .. len(parts) {
|
||||
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
|
||||
}
|
||||
} else if key == "hidden" { n.hidden = vl_truthy(v) and s != "false" } else if key == "disabled" { n.enabled = not (vl_truthy(v) and s != "false") } else if key == "src" { n.src = s } else {
|
||||
if key == "enabled" { n.enabled = vl_truthy(v) and s != "false" }
|
||||
push(n.akeys, key)
|
||||
push(n.avals, s)
|
||||
push(n.vals, v)
|
||||
}
|
||||
}
|
||||
# a colour by the renderer's name for it, else #rrggbb (or #rgb)
|
||||
function ui_color(name: string) -> int {
|
||||
if name == "none" or name == "" or name == "transparent" { return UI_NONE }
|
||||
if name[0] != 35 {
|
||||
if ui_be != null and ui_be.color != null { return ui_be.color(name) }
|
||||
return 15658734
|
||||
}
|
||||
var c = 0
|
||||
for i in 1 .. len(name) {
|
||||
var d = name[i] - 48
|
||||
if name[i] >= 97 { d = name[i] - 87 }
|
||||
if name[i] >= 65 and name[i] < 97 { d = name[i] - 55 }
|
||||
c = c * 16 + d
|
||||
if len(name) == 4 { c = c * 16 + d }
|
||||
}
|
||||
return c
|
||||
}
|
||||
65
packages/ludic.ui/env.ludic
Normal file
65
packages/ludic.ui/env.ludic
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# env.ludic - what a name means where an expression is read: the loop's own names, then the
|
||||
# component's props and state, then the view's model. A component sees its props, its state
|
||||
# and the model - never the names of whoever used it.
|
||||
export property UiEnv {
|
||||
names: []string = null
|
||||
vals: []Val = null
|
||||
state: Val = null
|
||||
parent: UiEnv = null
|
||||
model: Val = null
|
||||
slot: UiTpl = null
|
||||
slot_env: UiEnv = null
|
||||
on_names: []string = null # a component's on-* attributes, which its `emit` runs
|
||||
on_acts: []UiAct = null
|
||||
on_envs: []UiEnv = null
|
||||
}
|
||||
var ui_view: UiView = null
|
||||
var ui_errs: []string = new []string
|
||||
|
||||
function env_new(parent: UiEnv) -> UiEnv {
|
||||
let e = new UiEnv
|
||||
e.names = new []string
|
||||
e.vals = new []Val
|
||||
e.parent = parent
|
||||
if parent != null { e.model = parent.model }
|
||||
return e
|
||||
}
|
||||
function env_bind(e: UiEnv, name: string, v: Val) -> void {
|
||||
push(e.names, name)
|
||||
push(e.vals, v)
|
||||
}
|
||||
function env_get(e: UiEnv, name: string) -> Val {
|
||||
var at = e
|
||||
while at != null {
|
||||
var i = len(at.names) - 1
|
||||
while i >= 0 {
|
||||
if at.names[i] == name { return at.vals[i] }
|
||||
i -= 1
|
||||
}
|
||||
if at.state != null and Value.has(at.state, name) != 0 { return Value.get(at.state, name) }
|
||||
at = at.parent
|
||||
}
|
||||
if e.model != null and Value.has(e.model, name) != 0 { return Value.get(e.model, name) }
|
||||
ui_err(`no {name} here: not a loop's, a prop, a state or in the view`)
|
||||
return Value.null()
|
||||
}
|
||||
# `set name = value`: the nearest state that holds the name
|
||||
function env_set(e: UiEnv, name: string, v: Val) -> void {
|
||||
var at = e
|
||||
while at != null {
|
||||
if at.state != null and Value.has(at.state, name) != 0 {
|
||||
Value.put(at.state, name, v)
|
||||
return
|
||||
}
|
||||
at = at.parent
|
||||
}
|
||||
ui_err(`set {name}: no state here holds it - declare it with <state {name}="..."/>`)
|
||||
}
|
||||
# an error is said once, however many frames make it
|
||||
function ui_err(msg: string) -> void {
|
||||
for i in 0 .. len(ui_errs) {
|
||||
if ui_errs[i] == msg { return }
|
||||
}
|
||||
push(ui_errs, msg)
|
||||
print(`ui: {msg}`)
|
||||
}
|
||||
52
packages/ludic.ui/eval.ludic
Normal file
52
packages/ludic.ui/eval.ludic
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# eval.ludic - an expression's value, here
|
||||
function ui_eval(x: UiExpr, e: UiEnv) -> Val {
|
||||
if x.op == X_LIT { return x.v }
|
||||
if x.op == X_NAME { return env_get(e, x.s) }
|
||||
if x.op == X_MEMBER {
|
||||
let o = ui_eval(x.a, e)
|
||||
if Value.kind(o) == 6 and Value.has(o, x.s) != 0 { return Value.get(o, x.s) }
|
||||
if Value.kind(o) == 5 and x.s == "length" { return Value.int(Value.count(o)) }
|
||||
ui_err(`.{x.s}: there is no such field`)
|
||||
return Value.null()
|
||||
}
|
||||
if x.op == X_INDEX { return Value.at(ui_eval(x.a, e), Value.as_int(ui_eval(x.b, e))) }
|
||||
if x.op == X_CALL { return ev_call(x, e) }
|
||||
if x.op == X_NOT { return vl_bool(not vl_truthy(ui_eval(x.a, e))) }
|
||||
if x.op == X_NEG { return vl_arith("-", Value.int(0), ui_eval(x.a, e)) }
|
||||
if x.op == X_AND {
|
||||
if not vl_truthy(ui_eval(x.a, e)) { return vl_bool(false) }
|
||||
return vl_bool(vl_truthy(ui_eval(x.b, e)))
|
||||
}
|
||||
if x.op == X_OR {
|
||||
if vl_truthy(ui_eval(x.a, e)) { return vl_bool(true) }
|
||||
return vl_bool(vl_truthy(ui_eval(x.b, e)))
|
||||
}
|
||||
if x.op == X_COND {
|
||||
if vl_truthy(ui_eval(x.a, e)) { return ui_eval(x.b, e) }
|
||||
return ui_eval(x.c, e)
|
||||
}
|
||||
if x.op == X_TEXT {
|
||||
var out = ""
|
||||
for i in 0 .. len(x.args) { out = out + vl_text(ui_eval(x.args[i], e)) }
|
||||
return Value.str(out)
|
||||
}
|
||||
return vl_binary(x.s, ui_eval(x.a, e), ui_eval(x.b, e))
|
||||
}
|
||||
# a call: len() and range() are the template's own, anything else is the view's
|
||||
function ev_call(x: UiExpr, e: UiEnv) -> Val {
|
||||
let args = Value.list()
|
||||
for i in 0 .. len(x.args) { Value.add(args, ui_eval(x.args[i], e)) }
|
||||
if x.s == "len" and len(x.args) == 1 { return Value.int(Value.count(Value.at(args, 0))) }
|
||||
if x.s == "range" and len(x.args) == 1 {
|
||||
let out = Value.list()
|
||||
for i in 0 .. Value.as_int(Value.at(args, 0)) { Value.add(out, Value.int(i)) }
|
||||
return out
|
||||
}
|
||||
if ui_view == null or ui_view.call == null {
|
||||
ui_err(`{x.s}(): no view is showing to ask`)
|
||||
return Value.null()
|
||||
}
|
||||
return ui_view.call(x.s, args)
|
||||
}
|
||||
function ui_eval_text(x: UiExpr, e: UiEnv) -> string { return vl_text(ui_eval(x, e)) }
|
||||
function ui_eval_bool(x: UiExpr, e: UiEnv) -> bool { return vl_truthy(ui_eval(x, e)) }
|
||||
82
packages/ludic.ui/expr.ludic
Normal file
82
packages/ludic.ui/expr.ludic
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# expr.ludic - a template's expressions: `{pick == i}`, `{f.cost}`, `{placed(i) > 0 ? 'out' : ''}`.
|
||||
# Read once when the template loads, evaluated against the view's model every frame.
|
||||
const X_LIT: int = 0
|
||||
const X_NAME: int = 1
|
||||
const X_MEMBER: int = 2
|
||||
const X_INDEX: int = 3
|
||||
const X_CALL: int = 4
|
||||
const X_NOT: int = 5
|
||||
const X_NEG: int = 6
|
||||
const X_BIN: int = 7
|
||||
const X_AND: int = 8
|
||||
const X_OR: int = 9
|
||||
const X_COND: int = 10
|
||||
const X_TEXT: int = 11
|
||||
|
||||
export property UiExpr {
|
||||
op: int = 0
|
||||
s: string = ""
|
||||
v: Val = null
|
||||
a: UiExpr = null
|
||||
b: UiExpr = null
|
||||
c: UiExpr = null
|
||||
args: []UiExpr = null
|
||||
}
|
||||
# the text being read, and where the reading is
|
||||
property UiRd {
|
||||
s: string = ""
|
||||
i: int = 0
|
||||
err: string = ""
|
||||
}
|
||||
function ex_new(op: int) -> UiExpr {
|
||||
let e = new UiExpr
|
||||
e.op = op
|
||||
e.args = new []UiExpr
|
||||
return e
|
||||
}
|
||||
function ex_lit(v: Val) -> UiExpr {
|
||||
let e = ex_new(X_LIT)
|
||||
e.v = v
|
||||
return e
|
||||
}
|
||||
function rd_ws(r: UiRd) -> void {
|
||||
while r.i < len(r.s) and (r.s[r.i] == 32 or r.s[r.i] == 9 or r.s[r.i] == 10 or r.s[r.i] == 13) { r.i += 1 }
|
||||
}
|
||||
function rd_c(r: UiRd, k: int) -> int {
|
||||
if r.i + k < len(r.s) { return r.s[r.i + k] }
|
||||
return 0
|
||||
}
|
||||
# the operator `op` is next: take it
|
||||
function rd_op(r: UiRd, op: string) -> bool {
|
||||
rd_ws(r)
|
||||
let n = len(op)
|
||||
if r.i + n > len(r.s) { return false }
|
||||
if r.s[r.i..r.i + n] != op { return false }
|
||||
r.i += n
|
||||
return true
|
||||
}
|
||||
function rd_is_word(c: int) -> bool { return (c >= 97 and c <= 122) or (c >= 65 and c <= 90) or c == 95 or (c >= 48 and c <= 57) }
|
||||
# the word `w` is next, whole: take it
|
||||
function rd_word(r: UiRd, w: string) -> bool {
|
||||
rd_ws(r)
|
||||
let at = r.i
|
||||
if not rd_op(r, w) { return false }
|
||||
if rd_is_word(rd_c(r, 0)) {
|
||||
r.i = at
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
function rd_name(r: UiRd) -> string {
|
||||
rd_ws(r)
|
||||
let at = r.i
|
||||
while r.i < len(r.s) and rd_is_word(r.s[r.i]) { r.i += 1 }
|
||||
if r.i == at {
|
||||
rd_fail(r, "a name")
|
||||
return ""
|
||||
}
|
||||
return r.s[at..r.i]
|
||||
}
|
||||
function rd_fail(r: UiRd, want: string) -> void {
|
||||
if r.err == "" { r.err = `expected {want} at {r.i + 1} in "{r.s}"` }
|
||||
}
|
||||
96
packages/ludic.ui/expr_parse.ludic
Normal file
96
packages/ludic.ui/expr_parse.ludic
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# expr_parse.ludic - a condition, or/and/not, a comparison, sums and products, then a name
|
||||
# with its members, indexes and calls
|
||||
function px_cond(r: UiRd) -> UiExpr {
|
||||
let a = px_or(r)
|
||||
if not rd_op(r, "?") { return a }
|
||||
let e = ex_new(X_COND)
|
||||
e.a = a
|
||||
e.b = px_cond(r)
|
||||
if not rd_op(r, ":") { rd_fail(r, "':'") }
|
||||
e.c = px_cond(r)
|
||||
return e
|
||||
}
|
||||
function px_or(r: UiRd) -> UiExpr {
|
||||
var a = px_and(r)
|
||||
while rd_word(r, "or") or rd_op(r, "||") {
|
||||
let e = ex_new(X_OR)
|
||||
e.a = a
|
||||
e.b = px_and(r)
|
||||
a = e
|
||||
}
|
||||
return a
|
||||
}
|
||||
function px_and(r: UiRd) -> UiExpr {
|
||||
var a = px_not(r)
|
||||
while rd_word(r, "and") or rd_op(r, "&&") {
|
||||
let e = ex_new(X_AND)
|
||||
e.a = a
|
||||
e.b = px_not(r)
|
||||
a = e
|
||||
}
|
||||
return a
|
||||
}
|
||||
function px_not(r: UiRd) -> UiExpr {
|
||||
rd_ws(r)
|
||||
if rd_word(r, "not") or (rd_c(r, 0) == 33 and rd_c(r, 1) != 61 and rd_op(r, "!")) {
|
||||
let e = ex_new(X_NOT)
|
||||
e.a = px_not(r)
|
||||
return e
|
||||
}
|
||||
return px_cmp(r)
|
||||
}
|
||||
function px_bin(op: string, a: UiExpr, b: UiExpr) -> UiExpr {
|
||||
let e = ex_new(X_BIN)
|
||||
e.s = op
|
||||
e.a = a
|
||||
e.b = b
|
||||
return e
|
||||
}
|
||||
function px_cmp(r: UiRd) -> UiExpr {
|
||||
let a = px_sum(r)
|
||||
let ops = ["==", "!=", "<=", ">=", "<", ">"]
|
||||
for k in 0 .. len(ops) {
|
||||
if rd_op(r, ops[k]) { return px_bin(ops[k], a, px_sum(r)) }
|
||||
}
|
||||
return a
|
||||
}
|
||||
function px_sum(r: UiRd) -> UiExpr {
|
||||
var a = px_prod(r)
|
||||
while true {
|
||||
if rd_op(r, "+") { a = px_bin("+", a, px_prod(r)) } else if rd_op(r, "-") { a = px_bin("-", a, px_prod(r)) } else { return a }
|
||||
}
|
||||
return a
|
||||
}
|
||||
function px_prod(r: UiRd) -> UiExpr {
|
||||
var a = px_unary(r)
|
||||
while true {
|
||||
if rd_op(r, "*") { a = px_bin("*", a, px_unary(r)) } else if rd_op(r, "/") { a = px_bin("/", a, px_unary(r)) } else if rd_op(r, "%") { a = px_bin("%", a, px_unary(r)) } else { return a }
|
||||
}
|
||||
return a
|
||||
}
|
||||
function px_unary(r: UiRd) -> UiExpr {
|
||||
if rd_op(r, "-") {
|
||||
let e = ex_new(X_NEG)
|
||||
e.a = px_unary(r)
|
||||
return e
|
||||
}
|
||||
return px_post(r)
|
||||
}
|
||||
function px_post(r: UiRd) -> UiExpr {
|
||||
var a = px_primary(r)
|
||||
while r.err == "" {
|
||||
if rd_op(r, ".") {
|
||||
let e = ex_new(X_MEMBER)
|
||||
e.a = a
|
||||
e.s = rd_name(r)
|
||||
a = e
|
||||
} else if rd_op(r, "[") {
|
||||
let e = ex_new(X_INDEX)
|
||||
e.a = a
|
||||
e.b = px_cond(r)
|
||||
if not rd_op(r, "]") { rd_fail(r, "']'") }
|
||||
a = e
|
||||
} else { return a }
|
||||
}
|
||||
return a
|
||||
}
|
||||
88
packages/ludic.ui/expr_prim.ludic
Normal file
88
packages/ludic.ui/expr_prim.ludic
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# expr_prim.ludic - a number, a quoted string, true/false/null, a name or a call, a bracket
|
||||
function px_primary(r: UiRd) -> UiExpr {
|
||||
rd_ws(r)
|
||||
let c = rd_c(r, 0)
|
||||
if c >= 48 and c <= 57 { return px_number(r) }
|
||||
if c == 39 or c == 34 { return px_string(r, c) }
|
||||
if rd_op(r, "(") {
|
||||
let e = px_cond(r)
|
||||
if not rd_op(r, ")") { rd_fail(r, "')'") }
|
||||
return e
|
||||
}
|
||||
if rd_word(r, "true") { return ex_lit(Value.bool(1)) }
|
||||
if rd_word(r, "false") { return ex_lit(Value.bool(0)) }
|
||||
if rd_word(r, "null") { return ex_lit(Value.null()) }
|
||||
let name = rd_name(r)
|
||||
if not rd_op(r, "(") {
|
||||
let e = ex_new(X_NAME)
|
||||
e.s = name
|
||||
return e
|
||||
}
|
||||
let e = ex_new(X_CALL)
|
||||
e.s = name
|
||||
if rd_op(r, ")") { return e }
|
||||
while r.err == "" {
|
||||
push(e.args, px_cond(r))
|
||||
if rd_op(r, ")") { return e }
|
||||
if not rd_op(r, ",") { rd_fail(r, "',' or ')'") }
|
||||
}
|
||||
return e
|
||||
}
|
||||
function px_number(r: UiRd) -> UiExpr {
|
||||
let at = r.i
|
||||
var whole = 0
|
||||
while r.i < len(r.s) and r.s[r.i] >= 48 and r.s[r.i] <= 57 {
|
||||
whole = whole * 10 + (r.s[r.i] - 48)
|
||||
r.i += 1
|
||||
}
|
||||
if rd_c(r, 0) != 46 or rd_c(r, 1) < 48 or rd_c(r, 1) > 57 { return ex_lit(Value.int(whole)) }
|
||||
r.i += 1
|
||||
var f = float(whole)
|
||||
var unit = 0.1
|
||||
while r.i < len(r.s) and r.s[r.i] >= 48 and r.s[r.i] <= 57 {
|
||||
f = f + float(r.s[r.i] - 48) * unit
|
||||
unit = unit * 0.1
|
||||
r.i += 1
|
||||
}
|
||||
return ex_lit(Value.float(f))
|
||||
}
|
||||
function px_string(r: UiRd, q: int) -> UiExpr {
|
||||
r.i += 1
|
||||
let at = r.i
|
||||
while r.i < len(r.s) and r.s[r.i] != q { r.i += 1 }
|
||||
let s: string = r.s[at..r.i]
|
||||
if r.i >= len(r.s) { rd_fail(r, "the string's closing quote") } else { r.i += 1 }
|
||||
return ex_lit(Value.str(s))
|
||||
}
|
||||
# one expression, the whole of `src`
|
||||
function ex_parse(src: string, err: []string) -> UiExpr {
|
||||
let r = new UiRd
|
||||
r.s = src
|
||||
let e = px_cond(r)
|
||||
rd_ws(r)
|
||||
if r.err == "" and r.i < len(r.s) { rd_fail(r, "the end") }
|
||||
if r.err != "" { push(err, r.err) }
|
||||
return e
|
||||
}
|
||||
# text with {holes}: a literal when it has none, the expression itself when it is one hole and
|
||||
# nothing else (so it keeps its type), otherwise the pieces joined as text
|
||||
function ex_text(src: string, err: []string) -> UiExpr {
|
||||
let e = ex_new(X_TEXT)
|
||||
var at = 0
|
||||
var i = 0
|
||||
while i < len(src) {
|
||||
if src[i] == 123 {
|
||||
var j = i + 1
|
||||
while j < len(src) and src[j] != 125 { j += 1 }
|
||||
if i > at { push(e.args, ex_lit(Value.str(src[at..i]))) }
|
||||
push(e.args, ex_parse(src[i + 1..j], err))
|
||||
at = j + 1
|
||||
i = j
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
if at < len(src) { push(e.args, ex_lit(Value.str(src[at..len(src)]))) }
|
||||
if len(e.args) == 0 { return ex_lit(Value.str("")) }
|
||||
if len(e.args) == 1 { return e.args[0] }
|
||||
return e
|
||||
}
|
||||
95
packages/ludic.ui/frame.ludic
Normal file
95
packages/ludic.ui/frame.ludic
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# frame.ludic - a screen shown: built from the view's model, laid out, drawn, and its presses
|
||||
# run once the frame is drawn, so nothing the view does can change a frame half way through it
|
||||
var fr_was_down: bool = false
|
||||
var fr_pressed: []UiNode = new []UiNode
|
||||
|
||||
export function ui_show(screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
|
||||
let root = ui_nodes(screen, view)
|
||||
if root == null { return }
|
||||
ui_place(root, x, y, w, h)
|
||||
fr_pressed = new []UiNode
|
||||
fr_pointer(root)
|
||||
if ui_be != null and ui_be.text != null { fr_draw(root) }
|
||||
let pressed = fr_pressed
|
||||
for i in 0 .. len(pressed) { ui_press(pressed[i]) }
|
||||
}
|
||||
# the screen's nodes for this frame, not yet laid out - for a test, or a host that places them
|
||||
export function ui_nodes(screen: string, view: UiView) -> UiNode {
|
||||
let si = lib_find(lib_screen_names, screen)
|
||||
if si < 0 {
|
||||
ui_err(`there is no screen {screen} - ui_load the file that has it`)
|
||||
return null
|
||||
}
|
||||
let t = lib_screens[si]
|
||||
ui_view = view
|
||||
let e = env_new(null)
|
||||
if view != null and view.model != null { e.model = view.model() } else { e.model = Value.object() }
|
||||
e.state = st_for(screen, t, e)
|
||||
let root = new UiNode
|
||||
root.key = screen
|
||||
root.children = new []UiNode
|
||||
root.env = e
|
||||
root.sheet = t.sheet
|
||||
root.classes = new []string
|
||||
root.akeys = new []string
|
||||
root.avals = new []string
|
||||
root.vals = new []Val
|
||||
for i in 0 .. len(t.keys) {
|
||||
if t.keys[i] != "name" { el_keep(root, t.keys[i], ui_eval(t.vals[i], e)) }
|
||||
}
|
||||
for i in 0 .. len(root.akeys) { el_apply(root, root.akeys[i], root.vals[i]) }
|
||||
bd_kids(t.kids, e, screen, root.children)
|
||||
cs_tree(root)
|
||||
return root
|
||||
}
|
||||
export function ui_place(root: UiNode, x: float, y: float, w: float, h: float) -> void {
|
||||
root.w = UI_FILL
|
||||
root.h = UI_FILL
|
||||
var fg = 15658734
|
||||
if ui_be != null and ui_be.color != null { fg = ui_be.color("text") }
|
||||
ly_measure(root, w, ui_px(16.0), fg)
|
||||
ly_place(root, x, y, w, h)
|
||||
}
|
||||
# a press, as if the pointer had made it
|
||||
export function ui_press(n: UiNode) -> void {
|
||||
if n.act != null and n.enabled { act_run(n.act, n.env) }
|
||||
}
|
||||
# the pointer: what it is over is :hover next frame (the element and all around it, as in CSS),
|
||||
# and without a renderer's own button a press on the deepest pressable thing is a press
|
||||
var fr_hover: []string = new []string
|
||||
function fr_was_hovered(key: string) -> bool { return sel_has(fr_hover, key) }
|
||||
function fr_pointer(root: UiNode) -> void {
|
||||
if ui_be == null or ui_be.pointer_x == null {
|
||||
fr_hover = new []string
|
||||
return
|
||||
}
|
||||
let px = ui_be.pointer_x()
|
||||
let py = ui_be.pointer_y()
|
||||
let keys = new []string
|
||||
fr_under(root, px, py, keys)
|
||||
fr_hover = keys
|
||||
if ui_be.button != null or ui_be.pointer_down == null { return }
|
||||
let hit = fr_hit(root, px, py)
|
||||
let down = ui_be.pointer_down()
|
||||
if hit != null and down and not fr_was_down { push(fr_pressed, hit) }
|
||||
fr_was_down = down
|
||||
}
|
||||
function fr_under(n: UiNode, px: float, py: float, keys: []string) -> void {
|
||||
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return }
|
||||
if n.key != "" { push(keys, n.key) }
|
||||
for i in 0 .. len(n.children) { fr_under(n.children[i], px, py, keys) }
|
||||
}
|
||||
function fr_hit(n: UiNode, px: float, py: float) -> UiNode {
|
||||
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return null }
|
||||
if n.children != null {
|
||||
var i = len(n.children) - 1
|
||||
while i >= 0 {
|
||||
let h = fr_hit(n.children[i], px, py)
|
||||
if h != null { return h }
|
||||
i -= 1
|
||||
}
|
||||
}
|
||||
if n.act != null { return n }
|
||||
return null
|
||||
}
|
||||
export function ui_hit(root: UiNode, px: float, py: float) -> UiNode { return fr_hit(root, px, py) }
|
||||
33
packages/ludic.ui/index.ludic
Normal file
33
packages/ludic.ui/index.ludic
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# ludic.ui - templates: screens and components written as markup in files the program loads at
|
||||
# run time, reading only what a `view` declaration lets them, and answering with its events
|
||||
module ui
|
||||
numbers float
|
||||
import "node.ludic"
|
||||
import "backend.ludic"
|
||||
import "expr.ludic"
|
||||
import "expr_parse.ludic"
|
||||
import "expr_prim.ludic"
|
||||
import "values.ludic"
|
||||
import "env.ludic"
|
||||
import "eval.ludic"
|
||||
import "act.ludic"
|
||||
import "tpl.ludic"
|
||||
import "lib.ludic"
|
||||
import "lib_util.ludic"
|
||||
import "link.ludic"
|
||||
import "state.ludic"
|
||||
import "build.ludic"
|
||||
import "elem.ludic"
|
||||
import "attr.ludic"
|
||||
import "attr_box.ludic"
|
||||
import "style.ludic"
|
||||
import "style_rule.ludic"
|
||||
import "select.ludic"
|
||||
import "select_parts.ludic"
|
||||
import "select_match.ludic"
|
||||
import "cascade.ludic"
|
||||
import "layout.ludic"
|
||||
import "layout_run.ludic"
|
||||
import "layout_wrap.ludic"
|
||||
import "frame.ludic"
|
||||
import "draw.ludic"
|
||||
96
packages/ludic.ui/layout.ludic
Normal file
96
packages/ludic.ui/layout.ludic
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# layout.ludic - flex layout on the CSS box model, top-down: a box is told how wide it may be,
|
||||
# measures its content at that width (so a wrapping row knows how tall it is), then places each
|
||||
# child. A size is the border box: padding and border inside it, margin outside it.
|
||||
function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void {
|
||||
n.rsize = size
|
||||
if n.size > 0.0 { n.rsize = n.size }
|
||||
n.rfg = fg
|
||||
if n.fg != UI_INHERIT { n.rfg = n.fg }
|
||||
n.mw = ly_hin(n)
|
||||
n.mh = ly_vin(n)
|
||||
if n.kind == UI_TEXT or n.kind == UI_BUTTON {
|
||||
n.mw = n.mw + ly_text_w(n.text, n.rsize)
|
||||
n.mh = n.mh + n.rsize * 1.25
|
||||
}
|
||||
if n.kind == UI_IMAGE {
|
||||
n.mw = n.mw + n.rsize
|
||||
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.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 }
|
||||
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) }
|
||||
}
|
||||
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)
|
||||
}
|
||||
function ly_hin(n: UiNode) -> float { return n.pl + n.pr + n.border * 2.0 }
|
||||
function ly_vin(n: UiNode) -> float { return n.pt + n.pb + n.border * 2.0 }
|
||||
function ly_clamp(v: float, lo: float, hi: float) -> float {
|
||||
var out = v
|
||||
if hi > 0.0 and out > hi { out = hi }
|
||||
if out < lo { out = lo }
|
||||
return out
|
||||
}
|
||||
function ly_measure_box(n: UiNode, inner: float) -> void {
|
||||
var main = 0.0
|
||||
var cross = 0.0
|
||||
for i in 0 .. len(n.children) {
|
||||
let c = n.children[i]
|
||||
ly_measure(c, inner, n.rsize, n.rfg)
|
||||
main = main + ly_main(c, n.dir)
|
||||
if i > 0 { main = main + n.gap }
|
||||
cross = Math.max(cross, ly_cross(c, n.dir))
|
||||
}
|
||||
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_measure(c, inner, n.rsize, n.rfg)
|
||||
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
|
||||
}
|
||||
function ly_text_w(s: string, size: float) -> float {
|
||||
if ui_be != null and ui_be.measure != null { return ui_be.measure(s, size) }
|
||||
return float(len(s)) * size * 0.5
|
||||
}
|
||||
# a child's size along and across a direction, margins included
|
||||
function ly_main(c: UiNode, dir: int) -> float {
|
||||
if dir == UI_ROW { return c.mw + c.ml + c.mr }
|
||||
return c.mh + c.mt + c.mb
|
||||
}
|
||||
function ly_cross(c: UiNode, dir: int) -> float {
|
||||
if dir == UI_ROW { return c.mh + c.mt + c.mb }
|
||||
return c.mw + c.ml + c.mr
|
||||
}
|
||||
99
packages/ludic.ui/layout_run.ludic
Normal file
99
packages/ludic.ui/layout_run.ludic
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# layout_run.ludic - a box's children given their places: along its direction by size, grow and
|
||||
# justify, across it by align (or each child's own `self`), a wrapping row one line at a time
|
||||
var ly_lead: float = 0.0
|
||||
var ly_extra: float = 0.0
|
||||
function ly_place(n: UiNode, x: float, y: float, w: float, h: float) -> void {
|
||||
n.x = x
|
||||
n.y = y
|
||||
n.cw = w
|
||||
n.ch = h
|
||||
if (n.kind != UI_BOX and n.kind != UI_SCROLL) or n.children == null { return }
|
||||
let inner_w = w - ly_hin(n)
|
||||
var inner_h = h - ly_vin(n)
|
||||
if n.kind == UI_SCROLL { inner_h = Math.max(n.mh, h) - ly_vin(n) }
|
||||
let top = n.y + n.pt + n.border
|
||||
if n.dir == UI_ROW and n.wrap {
|
||||
ly_place_wrap(n, top, inner_w)
|
||||
return
|
||||
}
|
||||
if n.dir == UI_ROW { ly_place_line(n, 0, len(n.children), top, inner_w, inner_h) } else { ly_place_line(n, 0, len(n.children), top, inner_h, inner_w) }
|
||||
}
|
||||
function ly_fills(c: UiNode, dir: int) -> bool {
|
||||
if dir == UI_ROW { return c.w == UI_FILL }
|
||||
return c.h == UI_FILL
|
||||
}
|
||||
function ly_grow(c: UiNode, dir: int) -> float {
|
||||
if ly_fills(c, dir) { return Math.max(c.grow, 1.0) }
|
||||
return c.grow
|
||||
}
|
||||
# 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 }
|
||||
return ly_main(c, dir)
|
||||
}
|
||||
function ly_stretches(n: UiNode, c: UiNode) -> bool {
|
||||
if ly_fills(c, 1 - n.dir) { return true }
|
||||
var a = n.align
|
||||
if c.self >= 0 { a = c.self }
|
||||
return a == UI_STRETCH
|
||||
}
|
||||
# children from..to along one line starting at `top`: sizes, the spare room, then each place
|
||||
function ly_place_line(n: UiNode, from: int, to: int, top: float, main_room: float, cross_room: float) -> void {
|
||||
var used = 0.0
|
||||
var grows = 0.0
|
||||
for i in from .. to {
|
||||
used = used + ly_basis(n.children[i], n.dir, main_room)
|
||||
grows = grows + ly_grow(n.children[i], n.dir)
|
||||
}
|
||||
used = used + n.gap * float(Math.max(to - from - 1, 0))
|
||||
let free = main_room - used
|
||||
ly_justify(n, free, to - from, grows)
|
||||
let extra = ly_extra
|
||||
var at = ly_lead
|
||||
for i in from .. to {
|
||||
let c = n.children[i]
|
||||
var m = ly_basis(c, n.dir, main_room)
|
||||
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)
|
||||
at = at + m + n.gap + extra
|
||||
}
|
||||
}
|
||||
# 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 ly_stretches(n, c) { cr = cross_room }
|
||||
var a = n.align
|
||||
if c.self >= 0 { a = c.self }
|
||||
var off = 0.0
|
||||
if a == UI_CENTER { off = (cross_room - cr) / 2.0 }
|
||||
if a == UI_END { off = cross_room - cr }
|
||||
let left = n.x + n.pl + n.border
|
||||
if n.dir == UI_ROW {
|
||||
let cw = ly_clamp(m - c.ml - c.mr, c.min_w, c.max_w)
|
||||
ly_place(c, left + at + c.ml, top + off + c.mt, cw, ly_clamp(cr - c.mt - c.mb, c.min_h, c.max_h))
|
||||
} else {
|
||||
let chh = ly_clamp(m - c.mt - c.mb, c.min_h, c.max_h)
|
||||
ly_place(c, left + off + c.ml, top + at + c.mt, ly_clamp(cr - c.ml - c.mr, c.min_w, c.max_w), chh)
|
||||
}
|
||||
}
|
||||
# where the first child starts and what goes between, when nothing grows into the spare room
|
||||
function ly_justify(n: UiNode, free: float, count: int, grows: float) -> void {
|
||||
ly_lead = 0.0
|
||||
ly_extra = 0.0
|
||||
if grows > 0.0 or free <= 0.0 or count == 0 { return }
|
||||
if n.justify == UI_CENTER { ly_lead = free / 2.0 }
|
||||
if n.justify == UI_END { ly_lead = free }
|
||||
if n.justify == UI_BETWEEN and count > 1 { ly_extra = free / float(count - 1) }
|
||||
if n.justify == UI_AROUND {
|
||||
ly_extra = free / float(count)
|
||||
ly_lead = ly_extra / 2.0
|
||||
}
|
||||
if n.justify == UI_EVENLY {
|
||||
ly_extra = free / float(count + 1)
|
||||
ly_lead = ly_extra
|
||||
}
|
||||
}
|
||||
19
packages/ludic.ui/layout_wrap.ludic
Normal file
19
packages/ludic.ui/layout_wrap.ludic
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# layout_wrap.ludic - a wrapping row, line by line: each line is as tall as its tallest child
|
||||
function ly_place_wrap(n: UiNode, top0: float, inner_w: float) -> void {
|
||||
var top = top0
|
||||
var from = 0
|
||||
while from < len(n.children) {
|
||||
var to = from
|
||||
var line_w = 0.0
|
||||
var line_h = 0.0
|
||||
while to < len(n.children) and (to == from or line_w + n.gap + ly_main(n.children[to], UI_ROW) <= inner_w) {
|
||||
if to > from { line_w = line_w + n.gap }
|
||||
line_w = line_w + ly_main(n.children[to], UI_ROW)
|
||||
line_h = Math.max(line_h, ly_cross(n.children[to], UI_ROW))
|
||||
to += 1
|
||||
}
|
||||
ly_place_line(n, from, to, top, inner_w, line_h)
|
||||
top = top + line_h + n.gap
|
||||
from = to
|
||||
}
|
||||
}
|
||||
78
packages/ludic.ui/lib.ludic
Normal file
78
packages/ludic.ui/lib.ludic
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# lib.ludic - template files. A file's <component>s are its own unless marked export="true";
|
||||
# <import src="kit.xml"/> brings a file's exported components in by their names, and
|
||||
# <import src="kit.xml" as="kit"/> as <kit:Name>, so a component library is a file of them and two
|
||||
# libraries never collide. A file's <style>s, and its imports' before them, style what it writes. A <screen> is the program's, by name, from whichever file has it.
|
||||
export property UiFile {
|
||||
path: string = ""
|
||||
names: []string = null
|
||||
comps: []UiTpl = null
|
||||
exported: []bool = null
|
||||
imp_as: []string = null
|
||||
imp: []UiFile = null
|
||||
own: []UiRule = null
|
||||
sheet: UiSheet = null
|
||||
}
|
||||
var lib_files: []UiFile = new []UiFile
|
||||
var lib_screen_names: []string = new []string
|
||||
var lib_screens: []UiTpl = new []UiTpl
|
||||
# the file a path names, read once
|
||||
export function ui_load(path: string) -> bool { return lib_file(path) != null }
|
||||
function lib_file(path: string) -> UiFile {
|
||||
for i in 0 .. len(lib_files) {
|
||||
if lib_files[i].path == path { return lib_files[i] }
|
||||
}
|
||||
let src = lib_text(path)
|
||||
if len(src) == 0 {
|
||||
ui_err(`{path}: cannot read it`)
|
||||
return null
|
||||
}
|
||||
if lss_is(path) { return lss_file(src, path) }
|
||||
return lib_read(src, path)
|
||||
}
|
||||
# a file's text, as if read from `path` (its imports are found beside it)
|
||||
export function ui_load_text(src: string, path: string) -> void {
|
||||
if lss_is(path) { lss_file(src, path) } else { lib_read(src, path) }
|
||||
}
|
||||
function lib_read(src: string, path: string) -> UiFile {
|
||||
let f = lib_new(path)
|
||||
let root = tpl_read(Xml.parse(src), path)
|
||||
var tops = root.kids
|
||||
if root.tag != "ui" { tops = [root] }
|
||||
for i in 0 .. len(tops) { lib_top(f, tops[i]) }
|
||||
f.sheet = sheet_of(f)
|
||||
for i in 0 .. len(f.comps) { lib_link(f, f.comps[i]) }
|
||||
for i in 0 .. len(tops) {
|
||||
if tops[i].tag == "screen" { lib_link(f, tops[i]) }
|
||||
}
|
||||
return f
|
||||
}
|
||||
function lib_top(f: UiFile, t: UiTpl) -> void {
|
||||
if t.tag == "style" {
|
||||
sheet_read(f, t)
|
||||
return
|
||||
}
|
||||
if t.tag == "import" {
|
||||
let other = lib_file(lib_beside(f.path, bd_name(t, "src", "")))
|
||||
if other != null {
|
||||
push(f.imp, other)
|
||||
push(f.imp_as, bd_name(t, "as", ""))
|
||||
}
|
||||
return
|
||||
}
|
||||
let name = bd_name(t, "name", "")
|
||||
if name == "" or (t.tag != "component" and t.tag != "screen") {
|
||||
ui_err(`{f.path}: <{t.tag}> - a file holds <import>s, <style>s, <component name=...>s and <screen name=...>s`)
|
||||
return
|
||||
}
|
||||
if t.tag == "screen" {
|
||||
let at = lib_find(lib_screen_names, name)
|
||||
if at >= 0 { lib_screens[at] = t } else {
|
||||
push(lib_screen_names, name)
|
||||
push(lib_screens, t)
|
||||
}
|
||||
return
|
||||
}
|
||||
push(f.names, name)
|
||||
push(f.comps, t)
|
||||
push(f.exported, bd_name(t, "export", "") == "true")
|
||||
}
|
||||
34
packages/ludic.ui/lib_util.ludic
Normal file
34
packages/ludic.ui/lib_util.ludic
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# lib_util.ludic - a file made, found, read, and a path beside another
|
||||
function lib_find(names: []string, name: string) -> int {
|
||||
for i in 0 .. len(names) {
|
||||
if names[i] == name { return i }
|
||||
}
|
||||
return -1
|
||||
}
|
||||
function lib_new(path: string) -> UiFile {
|
||||
let f = new UiFile
|
||||
f.path = path
|
||||
f.names = new []string
|
||||
f.comps = new []UiTpl
|
||||
f.exported = new []bool
|
||||
f.imp_as = new []string
|
||||
f.imp = new []UiFile
|
||||
f.own = new []UiRule
|
||||
push(lib_files, f)
|
||||
return f
|
||||
}
|
||||
function lib_text(path: string) -> string {
|
||||
var src = ""
|
||||
if ui_be != null and ui_be.read != null { src = ui_be.read(path) } else { src = Fs.read_text(path) }
|
||||
if src == null { return "" }
|
||||
return src
|
||||
}
|
||||
# `src` read from beside the file that imports it
|
||||
function lib_beside(path: string, src: string) -> string {
|
||||
var cut = -1
|
||||
for i in 0 .. len(path) {
|
||||
if path[i] == 47 { cut = i }
|
||||
}
|
||||
if cut < 0 or (len(src) > 0 and src[0] == 47) { return src }
|
||||
return path[0..cut + 1] + src
|
||||
}
|
||||
42
packages/ludic.ui/link.ludic
Normal file
42
packages/ludic.ui/link.ludic
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# link.ludic - each tag in a file that is not an element names a component: the file's own, an
|
||||
# unnamed import's exported one, or `as:Name` from the import called `as`
|
||||
const LK_OWN: string = " row button spacer scroll img hr if else each slot state style component screen import "
|
||||
function lib_link(f: UiFile, t: UiTpl) -> void {
|
||||
t.sheet = f.sheet
|
||||
if not lk_element(t.tag) {
|
||||
t.comp = lk_find(f, t.tag, false)
|
||||
if t.comp == null and lk_find(f, t.tag, true) != null { ui_err(`{f.path}: <{t.tag}>: that component is not exported - export="true" on it lets other files use it`) } else if t.comp == null { ui_err(`{f.path}: <{t.tag}>: there is no such element, and no component of that name here or exported by an import`) }
|
||||
}
|
||||
for i in 0 .. len(t.kids) { lib_link(f, t.kids[i]) }
|
||||
}
|
||||
function lk_element(tag: string) -> bool { return lk_in(LK_OWN, tag) or lk_in(EL_BOXES, tag) or lk_in(EL_TEXTS, tag) }
|
||||
# a word in a list of words written " a b c "
|
||||
function lk_in(words: string, w: string) -> bool {
|
||||
let pat = ` {w} `
|
||||
let n = len(pat)
|
||||
for i in 0 .. len(words) - n + 1 {
|
||||
if words[i..i + n] == pat { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
# `any` finds an unexported one too, to say why it cannot be used
|
||||
function lk_find(f: UiFile, tag: string, any: bool) -> UiTpl {
|
||||
var colon = -1
|
||||
for i in 0 .. len(tag) {
|
||||
if tag[i] == 58 { colon = i }
|
||||
}
|
||||
if colon < 0 {
|
||||
let own = lib_find(f.names, tag)
|
||||
if own >= 0 { return f.comps[own] }
|
||||
}
|
||||
for i in 0 .. len(f.imp) {
|
||||
var want = tag
|
||||
if colon >= 0 {
|
||||
if f.imp_as[i] != tag[0..colon] { continue }
|
||||
want = tag[colon + 1..len(tag)]
|
||||
} else if f.imp_as[i] != "" { continue }
|
||||
let at = lib_find(f.imp[i].names, want)
|
||||
if at >= 0 and (any or f.imp[i].exported[at]) { return f.imp[i].comps[at] }
|
||||
}
|
||||
return null
|
||||
}
|
||||
89
packages/ludic.ui/node.ludic
Normal file
89
packages/ludic.ui/node.ludic
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# node.ludic - what a template becomes each frame, the view it reads, and the renderer seam
|
||||
export const UI_BOX: int = 0
|
||||
export const UI_TEXT: int = 1
|
||||
export const UI_SPACER: int = 2
|
||||
export const UI_BUTTON: int = 3
|
||||
export const UI_SCROLL: int = 4
|
||||
export const UI_IMAGE: int = 5
|
||||
export const UI_RULE: int = 6 # <hr>: a line across
|
||||
export const UI_COL: int = 0
|
||||
export const UI_ROW: int = 1
|
||||
export const UI_FILL: float = -1.0
|
||||
export const UI_START: int = 0
|
||||
export const UI_CENTER: int = 1
|
||||
export const UI_END: int = 2
|
||||
export const UI_STRETCH: int = 3 # align: the whole cross axis
|
||||
export const UI_BETWEEN: int = 3 # justify: the spare room between, around or evenly
|
||||
export const UI_AROUND: int = 4
|
||||
export const UI_EVENLY: int = 5
|
||||
export const UI_INHERIT: int = -1
|
||||
export const UI_NONE: int = -2
|
||||
|
||||
# what a program lets its templates see: `view Name { ... }` writes view_name() -> UiView
|
||||
export property UiView {
|
||||
name: string = ""
|
||||
model: fn() -> Val = null
|
||||
call: fn(string, Val) -> Val = null
|
||||
}
|
||||
|
||||
export property UiNode {
|
||||
kind: int = 0
|
||||
dir: int = 0
|
||||
text: string = ""
|
||||
key: string = ""
|
||||
tag: string = "" # what it is called in the template, and what a selector names
|
||||
id: string = ""
|
||||
classes: []string = null
|
||||
akeys: []string = null # its attributes, as text, for [attr=value] selectors
|
||||
avals: []string = null
|
||||
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
|
||||
parent: UiNode = null
|
||||
index: int = 0 # its place among its parent's children, for :first-child and the rest
|
||||
src: string = "" # an <img>'s picture
|
||||
size: float = 0.0
|
||||
w: float = 0.0
|
||||
h: float = 0.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
|
||||
pr: float = 0.0
|
||||
pb: float = 0.0
|
||||
pl: float = 0.0
|
||||
mt: float = 0.0 # margin
|
||||
mr: float = 0.0
|
||||
mb: float = 0.0
|
||||
ml: float = 0.0
|
||||
border: float = 0.0
|
||||
border_c: int = -2
|
||||
gap: float = 0.0
|
||||
align: int = 0
|
||||
justify: int = 0
|
||||
talign: int = 0 # text-align
|
||||
self: int = -1 # align-self; -1 takes the parent's align
|
||||
grow: float = 0.0 # a share of the spare room along the parent's direction
|
||||
wrap: bool = false # a row breaks into lines where it runs out of room
|
||||
min_w: float = 0.0
|
||||
max_w: float = 0.0 # 0 is no limit
|
||||
min_h: float = 0.0
|
||||
max_h: float = 0.0
|
||||
hidden: bool = false # display: none, or hidden
|
||||
fg: int = -1
|
||||
bg: int = -2
|
||||
alpha: float = 1.0
|
||||
enabled: bool = true
|
||||
act: UiAct = null
|
||||
env: UiEnv = null
|
||||
children: []UiNode = null
|
||||
mw: float = 0.0
|
||||
mh: float = 0.0
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
cw: float = 0.0
|
||||
ch: float = 0.0
|
||||
rsize: float = 0.0
|
||||
rfg: int = 0
|
||||
hovered: bool = false
|
||||
}
|
||||
|
||||
5
packages/ludic.ui/package.ludic
Normal file
5
packages/ludic.ui/package.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# ludic.ui — components: a UI declared as a tree of function calls, rebuilt from the program's
|
||||
# state every frame, laid out, and drawn through whatever renderer the program registers.
|
||||
package "ludic.ui"
|
||||
version "0.1.0"
|
||||
kind source
|
||||
17
packages/ludic.ui/screen.ludic
Normal file
17
packages/ludic.ui/screen.ludic
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# screen.ludic - the 2D screen as a renderer, for a program that draws with Screen.*
|
||||
function sc_rect(x: float, y: float, w: float, h: float, c: int, a: float) -> void { Screen.fill_rectangle(int(x), int(y), int(w), int(h), c) }
|
||||
function sc_text(x: float, y: float, s: string, size: float, c: int) -> void { Screen.draw_text(int(x), int(y), s, c, Math.max(1, int(size / 8.0))) }
|
||||
function sc_measure(s: string, size: float) -> float { return float(Screen.measure_text(s) * Math.max(1, int(size / 8.0))) }
|
||||
function sc_px() -> float { return float(Input.mouse_x()) }
|
||||
function sc_py() -> float { return float(Input.mouse_y()) }
|
||||
function sc_down() -> bool { return Input.mouse_down(0) }
|
||||
export function ui_screen_backend() -> UiBackend {
|
||||
let b = new UiBackend
|
||||
b.rect = fn sc_rect
|
||||
b.text = fn sc_text
|
||||
b.measure = fn sc_measure
|
||||
b.pointer_x = fn sc_px
|
||||
b.pointer_y = fn sc_py
|
||||
b.pointer_down = fn sc_down
|
||||
return b
|
||||
}
|
||||
70
packages/ludic.ui/select.ludic
Normal file
70
packages/ludic.ui/select.ludic
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# select.ludic - CSS selectors: a compound (`button.primary#buy[type=big]:hover:not(.off)`) joined
|
||||
# to the one on its left by a space (anywhere inside it) or `>` (straight inside it)
|
||||
export property UiSel {
|
||||
tag: string = ""
|
||||
id: string = ""
|
||||
classes: []string = null
|
||||
akeys: []string = null
|
||||
avals: []string = null # "" with aops 0 is [attr]; aops 1 is [attr=value]
|
||||
aops: []int = null
|
||||
pseudos: []string = null
|
||||
pargs: []string = null
|
||||
nots: []UiSel = null
|
||||
prev: UiSel = null
|
||||
comb: int = 0 # 0: prev is an ancestor, 1: prev is the parent
|
||||
}
|
||||
function sel_new() -> UiSel {
|
||||
let s = new UiSel
|
||||
s.classes = new []string
|
||||
s.akeys = new []string
|
||||
s.avals = new []string
|
||||
s.aops = new []int
|
||||
s.pseudos = new []string
|
||||
s.pargs = new []string
|
||||
s.nots = new []UiSel
|
||||
return s
|
||||
}
|
||||
function sel_parse(src: string, err: []string) -> UiSel {
|
||||
let r = new UiRd
|
||||
r.s = src
|
||||
var cur: UiSel = null
|
||||
var comb = 0
|
||||
while r.err == "" {
|
||||
rd_ws(r)
|
||||
if r.i >= len(r.s) { break }
|
||||
if rd_op(r, ">") {
|
||||
comb = 1
|
||||
continue
|
||||
}
|
||||
let c = sel_compound(r)
|
||||
c.prev = cur
|
||||
c.comb = comb
|
||||
cur = c
|
||||
comb = 0
|
||||
}
|
||||
if r.err != "" { push(err, r.err) }
|
||||
if cur == null { push(err, `"{src}" selects nothing`) }
|
||||
return cur
|
||||
}
|
||||
function sel_is_name(c: int) -> bool { return rd_is_word(c) or c == 45 }
|
||||
function sel_name(r: UiRd) -> string {
|
||||
let at = r.i
|
||||
while r.i < len(r.s) and sel_is_name(r.s[r.i]) { r.i += 1 }
|
||||
if r.i == at { rd_fail(r, "a name") }
|
||||
return r.s[at..r.i]
|
||||
}
|
||||
function sel_compound(r: UiRd) -> UiSel {
|
||||
let s = sel_new()
|
||||
if rd_c(r, 0) == 42 { r.i += 1 } else if sel_is_name(rd_c(r, 0)) { s.tag = sel_name(r) }
|
||||
while r.err == "" and r.i < len(r.s) {
|
||||
let c = rd_c(r, 0)
|
||||
if c == 35 {
|
||||
r.i += 1
|
||||
s.id = sel_name(r)
|
||||
} else if c == 46 {
|
||||
r.i += 1
|
||||
push(s.classes, sel_name(r))
|
||||
} else if c == 91 { sel_attr(r, s) } else if c == 58 { sel_pseudo(r, s) } else if c == 42 { r.i += 1 } else { return s }
|
||||
}
|
||||
return s
|
||||
}
|
||||
74
packages/ludic.ui/select_match.ludic
Normal file
74
packages/ludic.ui/select_match.ludic
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# select_match.ludic - whether a selector picks a node, and how specific it is (as CSS counts)
|
||||
function sel_spec(s: UiSel) -> int {
|
||||
var n = 0
|
||||
var at = s
|
||||
while at != null {
|
||||
if at.id != "" { n = n + 100 }
|
||||
n = n + (len(at.classes) + len(at.akeys) + len(at.pseudos)) * 10
|
||||
if at.tag != "" { n = n + 1 }
|
||||
for i in 0 .. len(at.nots) { n = n + sel_spec(at.nots[i]) }
|
||||
at = at.prev
|
||||
}
|
||||
return n
|
||||
}
|
||||
function sel_match(s: UiSel, n: UiNode) -> bool {
|
||||
if not sel_one(s, n) { return false }
|
||||
if s.prev == null { return true }
|
||||
if s.comb == 1 { return n.parent != null and sel_match(s.prev, n.parent) }
|
||||
var a = n.parent
|
||||
while a != null {
|
||||
if sel_match(s.prev, a) { return true }
|
||||
a = a.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
function sel_one(s: UiSel, n: UiNode) -> bool {
|
||||
if s.tag != "" and s.tag != n.tag { return false }
|
||||
if s.id != "" and s.id != n.id { return false }
|
||||
for i in 0 .. len(s.classes) {
|
||||
if not sel_has(n.classes, s.classes[i]) { return false }
|
||||
}
|
||||
for i in 0 .. len(s.akeys) {
|
||||
let v = sel_attr_of(n, s.akeys[i])
|
||||
if v == null or (s.aops[i] == 1 and v != s.avals[i]) { return false }
|
||||
}
|
||||
for i in 0 .. len(s.pseudos) {
|
||||
if not sel_pseudo_ok(s.pseudos[i], s.pargs[i], n) { return false }
|
||||
}
|
||||
for i in 0 .. len(s.nots) {
|
||||
if sel_one(s.nots[i], n) { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
function sel_has(xs: []string, x: string) -> bool {
|
||||
if xs == null { return false }
|
||||
for i in 0 .. len(xs) {
|
||||
if xs[i] == x { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
function sel_attr_of(n: UiNode, key: string) -> string {
|
||||
if n.akeys == null { return null }
|
||||
for i in 0 .. len(n.akeys) {
|
||||
if n.akeys[i] == key { return n.avals[i] }
|
||||
}
|
||||
return null
|
||||
}
|
||||
function sel_pseudo_ok(p: string, arg: string, n: UiNode) -> bool {
|
||||
if p == "hover" { return n.hovered }
|
||||
if p == "disabled" { return not n.enabled }
|
||||
if p == "enabled" { return n.enabled }
|
||||
if p == "first-child" { return n.index == 0 }
|
||||
if p == "last-child" { return n.parent != null and n.index == len(n.parent.children) - 1 }
|
||||
if p == "only-child" { return n.parent != null and len(n.parent.children) == 1 }
|
||||
if p == "empty" { return len(n.children) == 0 and n.text == "" }
|
||||
if p == "root" { return n.parent == null or n.parent.tag == "" }
|
||||
if p == "nth-child" {
|
||||
let k = n.index + 1
|
||||
if arg == "odd" { return k % 2 == 1 }
|
||||
if arg == "even" { return k % 2 == 0 }
|
||||
return string(k) == arg
|
||||
}
|
||||
ui_err(`:{p}: there is no such pseudo-class here`)
|
||||
return false
|
||||
}
|
||||
35
packages/ludic.ui/select_parts.ludic
Normal file
35
packages/ludic.ui/select_parts.ludic
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# select_parts.ludic - a compound's [attribute] and :pseudo-class parts
|
||||
function sel_attr(r: UiRd, s: UiSel) -> void {
|
||||
r.i += 1
|
||||
push(s.akeys, sel_name(r))
|
||||
if not rd_op(r, "=") {
|
||||
push(s.aops, 0)
|
||||
push(s.avals, "")
|
||||
} else {
|
||||
push(s.aops, 1)
|
||||
rd_ws(r)
|
||||
let q = rd_c(r, 0)
|
||||
if q == 34 or q == 39 { push(s.avals, Value.as_str(px_string(r, q).v)) } else { push(s.avals, sel_name(r)) }
|
||||
}
|
||||
if not rd_op(r, "]") { rd_fail(r, "']'") }
|
||||
}
|
||||
function sel_pseudo(r: UiRd, s: UiSel) -> void {
|
||||
r.i += 1
|
||||
let name = sel_name(r)
|
||||
var arg = ""
|
||||
if rd_c(r, 0) == 40 {
|
||||
r.i += 1
|
||||
let at = r.i
|
||||
while r.i < len(r.s) and r.s[r.i] != 41 { r.i += 1 }
|
||||
arg = r.s[at..r.i]
|
||||
r.i += 1
|
||||
}
|
||||
if name == "not" {
|
||||
let inner = new UiRd
|
||||
inner.s = tpl_words(arg)
|
||||
push(s.nots, sel_compound(inner))
|
||||
return
|
||||
}
|
||||
push(s.pseudos, name)
|
||||
push(s.pargs, tpl_words(arg))
|
||||
}
|
||||
34
packages/ludic.ui/state.ludic
Normal file
34
packages/ludic.ui/state.ludic
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# state.ludic - a component's or a screen's own values, <state name="{first}"/>, kept from frame
|
||||
# to frame by where the instance is in the tree, and set by `set name = value`
|
||||
var st_keys: []string = new []string
|
||||
var st_vals: []Val = new []Val
|
||||
function st_for(key: string, t: UiTpl, e: UiEnv) -> Val {
|
||||
var decl: UiTpl = null
|
||||
for i in 0 .. len(t.kids) {
|
||||
if t.kids[i].tag == "state" { decl = t.kids[i] }
|
||||
}
|
||||
if decl == null { return null }
|
||||
for i in 0 .. len(st_keys) {
|
||||
if st_keys[i] == key { return st_vals[i] }
|
||||
}
|
||||
let o = Value.object()
|
||||
for i in 0 .. len(decl.keys) { Value.put(o, decl.keys[i], ui_eval(decl.vals[i], e)) }
|
||||
push(st_keys, key)
|
||||
push(st_vals, o)
|
||||
return o
|
||||
}
|
||||
# a screen's state starts again the next time it opens
|
||||
export function ui_forget(screen: string) -> void {
|
||||
let keys = new []string
|
||||
let vals = new []Val
|
||||
for i in 0 .. len(st_keys) {
|
||||
let k = st_keys[i]
|
||||
let mine = len(k) >= len(screen) and k[0..len(screen)] == screen and (len(k) == len(screen) or k[len(screen)] == 47)
|
||||
if not mine {
|
||||
push(keys, k)
|
||||
push(vals, st_vals[i])
|
||||
}
|
||||
}
|
||||
st_keys = keys
|
||||
st_vals = vals
|
||||
}
|
||||
84
packages/ludic.ui/style.ludic
Normal file
84
packages/ludic.ui/style.ludic
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# style.ludic - stylesheets: `.chip { bg: bg2; pad: 6 } button { h: 40 }` in a <style>, or an .lss
|
||||
# file brought in by <import src="look.lss"/> (which may `@import "base.lss";` in turn). A selector is
|
||||
# a tag, a .class, both, or *; a property is any attribute, {holes} and all. The more specific rule
|
||||
# wins, then the later; an attribute beats them all.
|
||||
export property UiRule {
|
||||
sel: UiSel = null
|
||||
spec: int = 0
|
||||
keys: []string = null
|
||||
vals: []UiExpr = null
|
||||
}
|
||||
export property UiSheet {
|
||||
rules: []UiRule = null
|
||||
}
|
||||
function sheet_read(f: UiFile, t: UiTpl) -> void {
|
||||
var src = t.raw
|
||||
let from = bd_name(t, "src", "")
|
||||
if from != "" { src = lib_text(lib_beside(f.path, from)) }
|
||||
lss_text(f, src)
|
||||
}
|
||||
function lss_is(path: string) -> bool { return len(path) > 4 and path[len(path) - 4..len(path)] == ".lss" }
|
||||
# a stylesheet file: a file with rules and imports and nothing else
|
||||
function lss_file(src: string, path: string) -> UiFile {
|
||||
let f = lib_new(path)
|
||||
lss_text(f, src)
|
||||
f.sheet = sheet_of(f)
|
||||
return f
|
||||
}
|
||||
function lss_text(f: UiFile, src: string) -> void {
|
||||
let err = new []string
|
||||
let r = new UiRd
|
||||
r.s = lss_strip(src)
|
||||
while r.err == "" {
|
||||
rd_ws(r)
|
||||
if r.i >= len(r.s) { break }
|
||||
if rd_op(r, "@import") {
|
||||
lss_import(f, lss_upto(r, 59))
|
||||
continue
|
||||
}
|
||||
let sels = lss_upto(r, 123)
|
||||
let body = lss_upto(r, 125)
|
||||
lss_rule(f, sels, body, err)
|
||||
}
|
||||
for i in 0 .. len(err) { ui_err(`{f.path}: <style> {err[i]}`) }
|
||||
}
|
||||
# @import "base.lss"; - its rules come before this file's own
|
||||
function lss_import(f: UiFile, what: string) -> void {
|
||||
var p = tpl_words(what)
|
||||
if len(p) > 5 and p[0..4] == "url(" { p = p[4..len(p) - 1] }
|
||||
if len(p) >= 2 and (p[0] == 34 or p[0] == 39) { p = p[1..len(p) - 1] }
|
||||
let other = lib_file(lib_beside(f.path, p))
|
||||
if other != null {
|
||||
push(f.imp, other)
|
||||
push(f.imp_as, "")
|
||||
}
|
||||
}
|
||||
# the text up to the character `c`, which is taken too; a {hole} inside a body is skipped whole
|
||||
function lss_upto(r: UiRd, c: int) -> string {
|
||||
let at = r.i
|
||||
var depth = 0
|
||||
while r.i < len(r.s) and (r.s[r.i] != c or depth > 0) {
|
||||
if c == 125 and r.s[r.i] == 123 { depth += 1 }
|
||||
if c == 125 and r.s[r.i] == 125 { depth -= 1 }
|
||||
r.i += 1
|
||||
}
|
||||
let out: string = r.s[at..r.i]
|
||||
if r.i < len(r.s) { r.i += 1 } else { rd_fail(r, "a closing brace") }
|
||||
return out
|
||||
}
|
||||
function lss_strip(src: string) -> string {
|
||||
var out = ""
|
||||
var at = 0
|
||||
var i = 0
|
||||
while i + 1 < len(src) {
|
||||
if src[i] == 47 and src[i + 1] == 42 {
|
||||
out = out + src[at..i]
|
||||
var j = i + 2
|
||||
while j + 1 < len(src) and not (src[j] == 42 and src[j + 1] == 47) { j += 1 }
|
||||
i = j + 2
|
||||
at = i
|
||||
} else { i += 1 }
|
||||
}
|
||||
if at < len(src) { out = out + src[at..len(src)] }
|
||||
return out
|
||||
}
|
||||
71
packages/ludic.ui/style_rule.ludic
Normal file
71
packages/ludic.ui/style_rule.ludic
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# style_rule.ludic - a rule read, the cascade a file sees, and a node's rules applied to it
|
||||
function lss_rule(f: UiFile, sels: string, body: string, err: []string) -> void {
|
||||
let decls = lss_decls(body, err)
|
||||
let each = lss_split(sels, 44)
|
||||
for i in 0 .. len(each) {
|
||||
let sel = tpl_words(each[i])
|
||||
if len(sel) == 0 { continue }
|
||||
let r = new UiRule
|
||||
r.keys = decls.keys
|
||||
r.vals = decls.vals
|
||||
r.sel = sel_parse(sel, err)
|
||||
if r.sel != null {
|
||||
r.spec = sel_spec(r.sel)
|
||||
push(f.own, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
# `key: value; key: value`, a rule's body or a style="..." attribute
|
||||
function lss_decls(body: string, err: []string) -> UiRule {
|
||||
let r = new UiRule
|
||||
r.keys = new []string
|
||||
r.vals = new []UiExpr
|
||||
let decls = lss_split(body, 59)
|
||||
for i in 0 .. len(decls) {
|
||||
let d = decls[i]
|
||||
var colon = -1
|
||||
for k in 0 .. len(d) {
|
||||
if colon < 0 and d[k] == 58 { colon = k }
|
||||
}
|
||||
if colon < 0 {
|
||||
if len(tpl_words(d)) > 0 { push(err, `"{tpl_words(d)}" is not a property: value`) }
|
||||
continue
|
||||
}
|
||||
push(r.keys, tpl_words(d[0..colon]))
|
||||
push(r.vals, tpl_value(tpl_words(d[colon + 1..len(d)]), err))
|
||||
}
|
||||
return r
|
||||
}
|
||||
function lss_split(s: string, c: int) -> []string {
|
||||
let out = new []string
|
||||
var at = 0
|
||||
for i in 0 .. len(s) {
|
||||
if s[i] == c {
|
||||
push(out, s[at..i])
|
||||
at = i + 1
|
||||
}
|
||||
}
|
||||
push(out, s[at..len(s)])
|
||||
return out
|
||||
}
|
||||
# the imports' rules, then the file's own, ordered by specificity and otherwise as written
|
||||
function sheet_of(f: UiFile) -> UiSheet {
|
||||
let sh = new UiSheet
|
||||
sh.rules = new []UiRule
|
||||
for i in 0 .. len(f.imp) {
|
||||
if f.imp[i].sheet != null {
|
||||
for k in 0 .. len(f.imp[i].sheet.rules) { sheet_add(sh, f.imp[i].sheet.rules[k]) }
|
||||
}
|
||||
}
|
||||
for k in 0 .. len(f.own) { sheet_add(sh, f.own[k]) }
|
||||
return sh
|
||||
}
|
||||
function sheet_add(sh: UiSheet, r: UiRule) -> void {
|
||||
push(sh.rules, r)
|
||||
var i = len(sh.rules) - 1
|
||||
while i > 0 and sh.rules[i - 1].spec > r.spec {
|
||||
sh.rules[i] = sh.rules[i - 1]
|
||||
i -= 1
|
||||
}
|
||||
sh.rules[i] = r
|
||||
}
|
||||
84
packages/ludic.ui/tpl.ludic
Normal file
84
packages/ludic.ui/tpl.ludic
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# tpl.ludic - a template file read into what the builder walks: every attribute and every
|
||||
# element's text an expression, every on-* attribute an action. A file holds <component>s and
|
||||
# <screen>s under one root; a component is used as a tag of its own name anywhere after.
|
||||
export property UiTpl {
|
||||
tag: string = ""
|
||||
keys: []string = null
|
||||
vals: []UiExpr = null
|
||||
on_keys: []string = null
|
||||
on_acts: []UiAct = null
|
||||
text: UiExpr = null
|
||||
kids: []UiTpl = null
|
||||
comp: UiTpl = null # the component this tag names, found when its file loads
|
||||
sheet: UiSheet = null # the styles of the file it was written in
|
||||
style: UiRule = null # its style="..." attribute, read as declarations
|
||||
raw: string = "" # a <style>'s text, which is not a template's
|
||||
}
|
||||
|
||||
function tpl_read(x: Xml, file: string) -> UiTpl {
|
||||
let t = new UiTpl
|
||||
t.tag = Xml.tag(x)
|
||||
t.keys = new []string
|
||||
t.vals = new []UiExpr
|
||||
t.on_keys = new []string
|
||||
t.on_acts = new []UiAct
|
||||
t.kids = new []UiTpl
|
||||
let err = new []string
|
||||
for i in 0 .. len(x.akeys) {
|
||||
let k: string = x.akeys[i]
|
||||
let v: string = x.avals[i]
|
||||
if len(k) > 3 and k[0..3] == "on-" {
|
||||
push(t.on_keys, tpl_event(k[3..len(k)]))
|
||||
push(t.on_acts, act_parse(v, err))
|
||||
} else if k == "onclick" {
|
||||
push(t.on_keys, "press")
|
||||
push(t.on_acts, act_parse(v, err))
|
||||
} else if k == "style" { t.style = lss_decls(v, err) } else {
|
||||
push(t.keys, k)
|
||||
push(t.vals, tpl_value(v, err))
|
||||
}
|
||||
}
|
||||
for i in 0 .. Xml.child_count(x) { push(t.kids, tpl_read(Xml.child(x, i), file)) }
|
||||
if t.tag == "style" {
|
||||
t.raw = Xml.text(x)
|
||||
return t
|
||||
}
|
||||
let words = tpl_words(Xml.text(x))
|
||||
if len(words) > 0 and len(t.kids) == 0 { t.text = ex_text(words, err) }
|
||||
for i in 0 .. len(err) { ui_err(`{file}: <{t.tag}> {err[i]}`) }
|
||||
return t
|
||||
}
|
||||
# on-click is on-press, as a browser would have it
|
||||
function tpl_event(name: string) -> string {
|
||||
if name == "click" { return "press" }
|
||||
return name
|
||||
}
|
||||
# an attribute: a plain number is a number, anything else text with {holes}
|
||||
function tpl_value(v: string, err: []string) -> UiExpr {
|
||||
var num = len(v) > 0
|
||||
for i in 0 .. len(v) {
|
||||
if not ((v[i] >= 48 and v[i] <= 57) or v[i] == 46 or (i == 0 and v[i] == 45)) { num = false }
|
||||
}
|
||||
if num { return ex_parse(v, err) }
|
||||
return ex_text(v, err)
|
||||
}
|
||||
# text as a reader sees it: the ends trimmed and every run of white space one space
|
||||
function tpl_words(s: string) -> string {
|
||||
var out = ""
|
||||
var gap = false
|
||||
for i in 0 .. len(s) {
|
||||
let c = s[i]
|
||||
if c == 32 or c == 9 or c == 10 or c == 13 { gap = len(out) > 0 } else {
|
||||
if gap { out = out + " " }
|
||||
gap = false
|
||||
out = out + s[i..i + 1]
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
function tpl_attr(t: UiTpl, key: string) -> UiExpr {
|
||||
for i in 0 .. len(t.keys) {
|
||||
if t.keys[i] == key { return t.vals[i] }
|
||||
}
|
||||
return null
|
||||
}
|
||||
70
packages/ludic.ui/values.ludic
Normal file
70
packages/ludic.ui/values.ludic
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# values.ludic - arithmetic, comparison, truth and text over a template's values. A sum of two
|
||||
# whole numbers stays whole; anything with a fraction in it is a float; a string in a sum joins.
|
||||
function vl_bool(b: bool) -> Val {
|
||||
if b { return Value.bool(1) }
|
||||
return Value.bool(0)
|
||||
}
|
||||
function vl_num(v: Val) -> bool { return Value.kind(v) == 1 or Value.kind(v) == 2 or Value.kind(v) == 7 }
|
||||
function vl_truthy(v: Val) -> bool {
|
||||
let k = Value.kind(v)
|
||||
if k == 0 { return false }
|
||||
if k == 1 or k == 3 { return Value.as_int(v) != 0 }
|
||||
if k == 2 or k == 7 { return Value.as_float(v) != 0.0 }
|
||||
if k == 4 { return len(Value.as_str(v)) > 0 }
|
||||
return true
|
||||
}
|
||||
function vl_text(v: Val) -> string {
|
||||
let k = Value.kind(v)
|
||||
if k == 0 { return "" }
|
||||
if k == 1 { return string(Value.as_int(v)) }
|
||||
if k == 3 {
|
||||
if Value.as_int(v) != 0 { return "true" }
|
||||
return "false"
|
||||
}
|
||||
if k == 4 { return Value.as_str(v) }
|
||||
if k == 2 or k == 7 {
|
||||
let f = Value.as_float(v)
|
||||
if f == float(int(f)) { return string(int(f)) }
|
||||
return string(f)
|
||||
}
|
||||
return Json.encode(v)
|
||||
}
|
||||
function vl_arith(op: string, a: Val, b: Val) -> Val {
|
||||
if Value.kind(a) == 1 and Value.kind(b) == 1 {
|
||||
let x = Value.as_int(a)
|
||||
let y = Value.as_int(b)
|
||||
if op == "+" { return Value.int(x + y) }
|
||||
if op == "-" { return Value.int(x - y) }
|
||||
if op == "*" { return Value.int(x * y) }
|
||||
if y == 0 { return Value.int(0) }
|
||||
if op == "/" { return Value.int(x / y) }
|
||||
return Value.int(x % y)
|
||||
}
|
||||
let x = Value.as_float(a)
|
||||
let y = Value.as_float(b)
|
||||
if op == "+" { return Value.float(x + y) }
|
||||
if op == "-" { return Value.float(x - y) }
|
||||
if op == "*" { return Value.float(x * y) }
|
||||
if y == 0.0 { return Value.float(0.0) }
|
||||
if op == "/" { return Value.float(x / y) }
|
||||
return Value.float(x - y * float(int(x / y)))
|
||||
}
|
||||
function vl_equal(a: Val, b: Val) -> bool {
|
||||
if vl_num(a) and vl_num(b) { return Value.as_float(a) == Value.as_float(b) }
|
||||
if Value.kind(a) != Value.kind(b) { return false }
|
||||
if Value.kind(a) == 4 { return Value.as_str(a) == Value.as_str(b) }
|
||||
if Value.kind(a) == 3 { return Value.as_int(a) == Value.as_int(b) }
|
||||
return Value.kind(a) == 0
|
||||
}
|
||||
function vl_binary(op: string, a: Val, b: Val) -> Val {
|
||||
if op == "==" { return vl_bool(vl_equal(a, b)) }
|
||||
if op == "!=" { return vl_bool(not vl_equal(a, b)) }
|
||||
if op == "+" and (Value.kind(a) == 4 or Value.kind(b) == 4) { return Value.str(vl_text(a) + vl_text(b)) }
|
||||
if op == "+" or op == "-" or op == "*" or op == "/" or op == "%" { return vl_arith(op, a, b) }
|
||||
let x = Value.as_float(a)
|
||||
let y = Value.as_float(b)
|
||||
if op == "<" { return vl_bool(x < y) }
|
||||
if op == "<=" { return vl_bool(x <= y) }
|
||||
if op == ">" { return vl_bool(x > y) }
|
||||
return vl_bool(x >= y)
|
||||
}
|
||||
|
|
@ -462,6 +462,7 @@ namespace Value {
|
|||
alias null() = value_null
|
||||
alias int(n) = value_int
|
||||
alias fixed(f) = value_fixed
|
||||
alias float(x) = value_float
|
||||
alias bool(b) = value_bool
|
||||
alias str(s) = value_str
|
||||
alias list() = value_list
|
||||
|
|
@ -475,6 +476,7 @@ namespace Value {
|
|||
alias count(value) = value_count
|
||||
alias kind(value) = value_kind
|
||||
alias as_int(value) = value_as_int
|
||||
alias as_float(value) = value_as_float
|
||||
alias as_str(value) = value_as_str
|
||||
}
|
||||
namespace Json {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# shape a JSON save round-trips through. A node is one of:
|
||||
#
|
||||
# kind 0 null kind 1 int kind 2 fixed kind 3 bool
|
||||
# kind 4 str kind 5 list kind 6 object
|
||||
# kind 4 str kind 5 list kind 6 object kind 7 float
|
||||
#
|
||||
# ludicc splices this file when it sees Value.* / Json.* / Reflect.serialize /
|
||||
# Reflect.apply (parse.ludic). Everything is plain Ludic over heap records — no
|
||||
|
|
@ -39,6 +39,7 @@ function value_fixed(f: int) -> Val { let v = value_new(2); v.num = f; return v
|
|||
function value_bool(b: int) -> Val { let v = value_new(3); if b != 0 { v.num = 1 }; return v }
|
||||
function value_str(s: pointer) -> Val { let v = value_new(4); v.txt = s; return v }
|
||||
function value_list() -> Val { return value_new(5) }
|
||||
function value_float(x: float) -> Val { let v = value_new(7); v.num = float_bits(x); return v } # num = the IEEE bits
|
||||
function value_object() -> Val { return value_new(6) }
|
||||
|
||||
# --- builders (return the container, so calls chain) ------------------------
|
||||
|
|
@ -53,7 +54,16 @@ function value_put(obj: Val, key: pointer, item: Val) -> Val {
|
|||
# --- accessors --------------------------------------------------------------
|
||||
function value_kind(v: Val) -> int { return v.tag }
|
||||
function value_count(v: Val) -> int { return len(v.kids) }
|
||||
function value_as_int(v: Val) -> int { return v.num }
|
||||
function value_as_int(v: Val) -> int {
|
||||
if v.tag == 7 { return int(float_from_bits(v.num)) }
|
||||
return v.num
|
||||
}
|
||||
# a number as a float, whichever kind it was written as
|
||||
function value_as_float(v: Val) -> float {
|
||||
if v.tag == 7 { return float_from_bits(v.num) }
|
||||
if v.tag == 2 { return float(v.num) / 65536.0 }
|
||||
return float(v.num)
|
||||
}
|
||||
function value_as_str(v: Val) -> string { if v.txt == null { return "" }; return v.txt }
|
||||
function value_at(list: Val, i: int) -> Val {
|
||||
if i < 0 or i >= len(list.kids) { return value_null() }
|
||||
|
|
@ -118,6 +128,7 @@ function json_encode(v: Val) -> string {
|
|||
if v.tag == 2 { return json_fixed_str(v.num) }
|
||||
if v.tag == 3 { if v.num != 0 { return "true" }; return "false" }
|
||||
if v.tag == 4 { return json_quote(v.txt) }
|
||||
if v.tag == 7 { return string(float_from_bits(v.num)) }
|
||||
if v.tag == 5 {
|
||||
var out = "["
|
||||
var i = 0
|
||||
|
|
|
|||
|
|
@ -50,13 +50,16 @@ function emit_index_addr(e: Node) -> pointer {
|
|||
return r
|
||||
}
|
||||
let el = slice_elem(base.ty)
|
||||
# the index is evaluated BEFORE the elements' address is read: evaluating it may grow this
|
||||
# very slice (xs[slot_of(k)] where slot_of pushes), and a push can move the elements
|
||||
let ix = emit_expr(e.b)
|
||||
# load the data pointer from the slice header (field 0)
|
||||
let dp = nreg()
|
||||
emit(" "); emit(dp); emit(" = getelementptr inbounds %LSlice, ptr ")
|
||||
emit(base.code); emit(", i32 0, i32 0\n")
|
||||
let data = nreg()
|
||||
emit(" "); emit(data); emit(" = load ptr, ptr "); emit(dp); emit("\n")
|
||||
let ix = emit_expr(e.b) # (evaluate index BEFORE setting
|
||||
# (the index was evaluated first, above, and before g_addr_ty is set
|
||||
# THE INDEX IS CHECKED AGAINST THE LENGTH. A slice has carried { ptr, len, cap } since it
|
||||
# existed, and nothing ever read the len: every index was a bare getelementptr, so running
|
||||
# off the end of one wrote into whatever the allocator had put next and the program died
|
||||
|
|
@ -73,7 +76,7 @@ function emit_index_addr(e: Node) -> pointer {
|
|||
let lbad = lbl("ixbad")
|
||||
emit(` br i1 {okc}, label %{lok}, label %{lbad}\n`)
|
||||
emit(`{lbad}:\n`)
|
||||
let bmsg = emit_str_const(`{g_src_name}:{itoa(e.line)}: index out of range: `)
|
||||
let bmsg = emit_str_const(`{src_name_of(e)}:{itoa(e.line)}: index out of range: `)
|
||||
let bse = emit_bind(stdstream_rhs(2))
|
||||
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {bse}, ptr @.fmt_bounds, ptr {bmsg}, i32 {ix.code}, i32 {lnv})\n`)
|
||||
emit(" call void @exit(i32 1)\n unreachable\n")
|
||||
|
|
|
|||
|
|
@ -523,7 +523,7 @@ function emit_call(e: Node) -> Val {
|
|||
if (name == "self") { if nself == 0 { return val("0", "entity") }; return val(emit_bind(`load i32, ptr {self_stk[nself - 1]}`), "entity") }
|
||||
if (name == "key") { return val(emit_bind("load i32, ptr @L_key"), "int") }
|
||||
if (name == "save") { emit(" call void @L_save()\n"); return val("0", "void") }
|
||||
if (name == "ui_build") { emit(" call void @ui_build()\n"); return val("0", "void") }
|
||||
if (name == "ui_build") and (find_fn(name) == null) { emit(" call void @ui_build()\n"); return val("0", "void") }
|
||||
if (name == "load") { return val(emit_bind("call i32 @L_load()"), "bool") }
|
||||
if (name == "world_size") { return val(emit_bind("call i32 @L_world_size()"), "int") }
|
||||
if (name == "world_save") { # world_save(buf) -> bytes written
|
||||
|
|
@ -906,6 +906,7 @@ function emit_call(e: Node) -> Val {
|
|||
cname = rtname
|
||||
}
|
||||
vis_check(fn2, name)
|
||||
call_fill_defaults(e, fn2) # L11 - for code the checker does not walk
|
||||
reorder_named(e, param_labels(fn2))
|
||||
# evaluate args first (their IR is emitted before the call instruction), coercing
|
||||
# each to the parameter's declared type so an int passed for a `long` widens.
|
||||
|
|
|
|||
|
|
@ -70,6 +70,11 @@ function emit_sized_slice(el: pointer, n: Val) -> Val {
|
|||
emit(` store i32 {n.code}, ptr {slice_field(h.code, 2)}\n`)
|
||||
return h
|
||||
}
|
||||
# the file a runtime error names: the one the expression is written in, not the program's own
|
||||
function src_name_of(e: Node) -> pointer {
|
||||
if e == null or e.file == null { return g_src_name }
|
||||
return e.file
|
||||
}
|
||||
function emit_view(e: Node) -> Val {
|
||||
let xs = emit_expr(e.kids[0])
|
||||
if not is_slice_ty(xs.ty) { perr(`view takes a slice, and this is {xs.ty}`) }
|
||||
|
|
@ -88,7 +93,7 @@ function emit_view(e: Node) -> Val {
|
|||
emit(` br i1 {ok}, label %{lok}, label %{lbad}\n`)
|
||||
emit(`{lbad}:\n`)
|
||||
g_uses_bounds = true
|
||||
let bmsg = emit_str_const(`{g_src_name}:{itoa(e.line)}: view past the end: from `)
|
||||
let bmsg = emit_str_const(`{src_name_of(e)}:{itoa(e.line)}: view past the end: from `)
|
||||
let bse = emit_bind(stdstream_rhs(2))
|
||||
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {bse}, ptr @.fmt_bounds, ptr {bmsg}, i32 {endv}, i32 {ln})\n`)
|
||||
emit(" call void @exit(i32 1)\n unreachable\n")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function ck_params(f: Node, labels: []pointer, tys: []pointer) -> void {
|
|||
}
|
||||
}
|
||||
function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer {
|
||||
call_fill_defaults(e, f)
|
||||
let labels = new []pointer
|
||||
let tys = new []pointer
|
||||
ck_params(f, labels, tys)
|
||||
|
|
@ -21,6 +22,7 @@ function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer {
|
|||
return f.ty
|
||||
}
|
||||
function ck_call_alias(e: Node, name: pointer, al: int, f: Node) -> pointer {
|
||||
if g_al_labels[al] == null { call_fill_defaults(e, f) } else { call_mixed_to_named(e, ns_alias_labels(al)) }
|
||||
let labels = new []pointer
|
||||
let tys = new []pointer
|
||||
ck_params(f, labels, tys)
|
||||
|
|
@ -50,11 +52,18 @@ function ck_args(e: Node, name: pointer, labels: []pointer, tys: []pointer) -> v
|
|||
let vt = ck_expr(fi.a)
|
||||
let at = ck_label_at(labels, fi.s)
|
||||
if at >= 0 { ck_give(tys[at], vt, fi.a, `argument {fi.s} of {name}`) }
|
||||
if at < 0 and len(labels) > 0 { ck_err("arity", fi.a, `{name} has no parameter {fi.s}`) }
|
||||
j += 1
|
||||
}
|
||||
var m = 0
|
||||
while m < len(labels) {
|
||||
if not ck_named_has(e, labels[m]) { ck_err("arity", e, `this call to {name} leaves out {labels[m]}, which has no default`) }
|
||||
m += 1
|
||||
}
|
||||
return
|
||||
}
|
||||
if n != len(tys) { ck_err("arity", e, `{name} takes {itoa(len(tys))} argument(s) and this call gives {itoa(n)}`) }
|
||||
if n < len(tys) and len(labels) == len(tys) { ck_err("arity", e, `this call to {name} leaves out {labels[n]}, which has no default`) }
|
||||
else { if n != len(tys) { ck_err("arity", e, `{name} takes {itoa(len(tys))} argument(s) and this call gives {itoa(n)}`) } }
|
||||
var i = 0
|
||||
while i < n {
|
||||
if i < len(tys) { ck_expect = tys[i] }
|
||||
|
|
@ -63,6 +72,14 @@ function ck_args(e: Node, name: pointer, labels: []pointer, tys: []pointer) -> v
|
|||
i += 1
|
||||
}
|
||||
}
|
||||
function ck_named_has(e: Node, label: pointer) -> bool {
|
||||
var i = 0
|
||||
while i < len(e.kids) {
|
||||
if (e.kids[i].s == label) { return true }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
function ck_walk_args(e: Node) -> void {
|
||||
var i = 0
|
||||
while i < len(e.kids) {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ function gen_arg_nodes(e: Node, labels: []pointer) -> []Node {
|
|||
}
|
||||
function gen_call(e: Node, name: pointer, t: Node) -> pointer {
|
||||
let expect = ck_call_expect
|
||||
call_fill_defaults(e, t)
|
||||
let tps = gen_split(t.tps)
|
||||
let labels = new []pointer
|
||||
let ptys = new []pointer
|
||||
|
|
|
|||
|
|
@ -147,10 +147,24 @@ function ck_listener(l: Node) -> void {
|
|||
ck_block(l.a)
|
||||
ck_pop(m)
|
||||
}
|
||||
# a program's function named like one of the runtime's takes every call the runtime makes to its
|
||||
# own - the runtime's ui_measure started answering to a package's - so the name is refused
|
||||
function ck_runtime_shadows() -> void {
|
||||
var r = g_prog_user_end
|
||||
while r < len(prog) {
|
||||
let d = prog[r]
|
||||
if d.kind == N_FN {
|
||||
let u = ck_fn(d.s)
|
||||
if u != null and u != d { ck_err("shadow", u, `{d.s} is also a function of the engine runtime ({d.file}); choose another name`) }
|
||||
}
|
||||
r += 1
|
||||
}
|
||||
}
|
||||
# the pass also makes the generics real (check_gen.ludic), so it always runs
|
||||
function check_program() -> void {
|
||||
gen_collect()
|
||||
ck_index()
|
||||
ck_runtime_shadows()
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ function ns_alias_labels(i: int) -> []pointer {
|
|||
return out
|
||||
}
|
||||
function emit_alias_call(i: int, e: Node) -> Val {
|
||||
call_mixed_to_named(e, ns_alias_labels(i))
|
||||
reorder_named(e, ns_alias_labels(i))
|
||||
let id = node(E_ID)
|
||||
id.s = g_al_target[i]
|
||||
|
|
|
|||
96
selfhost/frontend/defaults.ludic
Normal file
96
selfhost/frontend/defaults.ludic
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# defaults.ludic — L11: a parameter may have a default, `pad: float = 8.0`, and a call may leave it
|
||||
# out - positionally from the end, or by not naming it. That is what makes a component's props
|
||||
# usable: `button(label: "Buy", on_press: fn buy)` rather than every prop of a button spelt out.
|
||||
# The default is an expression written where the function is and evaluated for each call.
|
||||
# `f(a, b, size: 2)`: the leading arguments are the first parameters, by position, and the rest
|
||||
# are named - the natural call of a component, whose first prop is the one it is about
|
||||
function call_mixed_to_named(e: Node, labels: []pointer) -> void {
|
||||
var first_named = -1
|
||||
var i = 0
|
||||
while i < len(e.kids) {
|
||||
if e.kids[i].kind == E_FINIT and first_named < 0 { first_named = i }
|
||||
if e.kids[i].kind != E_FINIT and first_named >= 0 {
|
||||
g_err_file = e.file
|
||||
g_err_line = e.line
|
||||
perr("a positional argument after a named one")
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
if first_named <= 0 { return }
|
||||
let out = new []Node
|
||||
var k = 0
|
||||
while k < len(e.kids) {
|
||||
var a = e.kids[k]
|
||||
if k < first_named and k < len(labels) {
|
||||
let fi = node(E_FINIT)
|
||||
fi.s = labels[k]
|
||||
fi.a = a
|
||||
fi.line = a.line
|
||||
fi.file = a.file
|
||||
a = fi
|
||||
}
|
||||
push(out, a)
|
||||
k += 1
|
||||
}
|
||||
e.kids = out
|
||||
}
|
||||
function call_fill_defaults(e: Node, f: Node) -> void {
|
||||
if f == null { return }
|
||||
let labels = new []pointer
|
||||
let defs = new []Node
|
||||
var any = false
|
||||
var i = 0
|
||||
while i < len(f.kids) {
|
||||
let p = f.kids[i]
|
||||
if p.kind == N_PARAM {
|
||||
push(labels, p.s)
|
||||
push(defs, p.a)
|
||||
if p.a != null { any = true }
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
call_mixed_to_named(e, labels)
|
||||
if not any { return }
|
||||
if len(e.kids) > 0 and e.kids[0].kind == E_FINIT {
|
||||
let out = new []Node
|
||||
var li = 0
|
||||
while li < len(labels) {
|
||||
var found: Node = null
|
||||
var k = 0
|
||||
while k < len(e.kids) {
|
||||
if (e.kids[k].s == labels[li]) { found = e.kids[k] }
|
||||
k += 1
|
||||
}
|
||||
if found == null and defs[li] != null {
|
||||
found = node(E_FINIT)
|
||||
found.s = labels[li]
|
||||
found.a = default_copy(defs[li], e)
|
||||
}
|
||||
if found != null { push(out, found) }
|
||||
li += 1
|
||||
}
|
||||
# a label the function does not have stays, so the call is refused for it
|
||||
var k2 = 0
|
||||
while k2 < len(e.kids) {
|
||||
if not default_label_in(labels, e.kids[k2].s) { push(out, e.kids[k2]) }
|
||||
k2 += 1
|
||||
}
|
||||
e.kids = out
|
||||
return
|
||||
}
|
||||
while len(e.kids) < len(labels) and defs[len(e.kids)] != null { push(e.kids, default_copy(defs[len(e.kids)], e)) }
|
||||
}
|
||||
function default_label_in(labels: []pointer, s: pointer) -> bool {
|
||||
var i = 0
|
||||
while i < len(labels) {
|
||||
if (labels[i] == s) { return true }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
# each call gets its own copy of the default, at the call's line for its errors
|
||||
function default_copy(d: Node, at: Node) -> Node {
|
||||
let c = gen_clone(d, new []pointer, new []pointer)
|
||||
c.line = at.line
|
||||
return c
|
||||
}
|
||||
|
|
@ -577,7 +577,9 @@ function parse_fn() -> Node {
|
|||
eat_op("(")
|
||||
if is_reserved_word(n.s) { perr(`'{n.s}' is a reserved word and cannot name a function`) }
|
||||
while not is_op(")") {
|
||||
let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype(); push(n.kids, p)
|
||||
let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype()
|
||||
if is_op("=") { pi += 1; p.a = expr() } # L11: a default, for a call that leaves it out
|
||||
push(n.kids, p)
|
||||
if is_op(",") { pi += 1 }
|
||||
}
|
||||
eat_op(")")
|
||||
|
|
@ -1014,6 +1016,7 @@ function parse_one_decl() -> void {
|
|||
if is_id("namespace") { parse_namespace(); return } # #76 namespace block
|
||||
if is_id("registry") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "of") { parse_registry(); return } # L8
|
||||
if is_id("def") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].kind == TK_ID) { parse_def(); return } # L8
|
||||
if is_id("view") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_view(); return } # L11
|
||||
if is_id("var") { push(prog, parse_var()); return }
|
||||
if is_id("const") { push(prog, parse_const()); return }
|
||||
var is_unsafe = false
|
||||
|
|
@ -1510,4 +1513,5 @@ function parse_program() -> void {
|
|||
parse_one_decl()
|
||||
}
|
||||
registries_finish() # L8: each registry gets its defs
|
||||
views_finish() # L11: each view gets its model and call
|
||||
}
|
||||
|
|
|
|||
113
selfhost/frontend/view.ludic
Normal file
113
selfhost/frontend/view.ludic
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# view.ludic — L11: a view is the one bridge between a program and its UI templates. The
|
||||
# program says what a template may read and what it may do, and nothing else crosses:
|
||||
#
|
||||
# view Yard {
|
||||
# purse = hm_purse # a field: the global's own type
|
||||
# spots: int = yard_spots() # any expression, with its type said
|
||||
# function placed(hf: int) -> int { return yard_placed(hf) } # a query a template may ask
|
||||
# on buy(hf: int) { yard_buy(hf) } # an event a template may send
|
||||
# }
|
||||
#
|
||||
# gives view_yard() -> UiView, whose model() is a Value object of every field (a record is an
|
||||
# object of its fields, a list and a registry a list) and whose call(name, args) runs a query or
|
||||
# an event by name with its arguments out of a Value list. Everything generated belongs to the
|
||||
# view's own file, so it is in the view's module and exported only when the view is.
|
||||
var g_vw_name: []pointer = new []pointer
|
||||
var g_vw_node: []Node = new []Node # view_<name>(), which carries the file, line and export
|
||||
var g_vw_fields: [][]Node = new [][]Node # the getters, one per field
|
||||
var g_vw_fnames: [][]pointer = new [][]pointer
|
||||
var g_vw_calls: [][]Node = new [][]Node # the queries and the events
|
||||
var g_vw_cnames: [][]pointer = new [][]pointer
|
||||
var g_vw_helpers: []pointer = new []pointer # conversion helpers already written, program-wide
|
||||
|
||||
function vw_prefix(name: pointer) -> pointer { return `view_{reg_lower(name)}_` }
|
||||
|
||||
# view NAME { members }
|
||||
function parse_view() -> void {
|
||||
pi += 1
|
||||
let name = eat_id()
|
||||
let px = vw_prefix(name)
|
||||
g_uses_value = true
|
||||
let fields = new []Node
|
||||
let fnames = new []pointer
|
||||
let calls = new []Node
|
||||
let cnames = new []pointer
|
||||
skipnl()
|
||||
eat_op("{")
|
||||
while true {
|
||||
skipnl()
|
||||
if is_op("}") { break }
|
||||
if is_id("function") or is_id("on") {
|
||||
let ev = is_id("on")
|
||||
if ev { toks[pi].text = "function" }
|
||||
let f = parse_fn()
|
||||
push(cnames, f.s)
|
||||
if ev { f.s = px + "on_" + f.s } else { f.s = px + f.s }
|
||||
push(calls, f)
|
||||
push(prog, f)
|
||||
continue
|
||||
}
|
||||
push(fnames, eat_id())
|
||||
push(fields, vw_field(px, fnames[len(fnames) - 1]))
|
||||
}
|
||||
eat_op("}")
|
||||
let head = vw_head(name, px)
|
||||
push(g_vw_name, name)
|
||||
push(g_vw_node, head)
|
||||
push(g_vw_fields, fields)
|
||||
push(g_vw_fnames, fnames)
|
||||
push(g_vw_calls, calls)
|
||||
push(g_vw_cnames, cnames)
|
||||
}
|
||||
# one field: NAME [: TYPE] = EXPR, as a getter function whose type the finish fills in if unsaid
|
||||
function vw_field(px: pointer, fname: pointer) -> Node {
|
||||
let g = node(N_FN)
|
||||
g.s = px + "get_" + fname
|
||||
g.ty = null
|
||||
if is_op(":") {
|
||||
pi += 1
|
||||
g.ty = ptype()
|
||||
}
|
||||
if not is_op("=") { perr(`view field {fname}: say what it is - {fname} = some_global`) }
|
||||
pi += 1
|
||||
let r = node(S_RETURN)
|
||||
r.a = expr()
|
||||
let body = node(N_BLOCK)
|
||||
push(body.kids, r)
|
||||
g.a = body
|
||||
push(prog, g)
|
||||
return g
|
||||
}
|
||||
# view_<name>() -> UiView, written now so it takes the export the declaration is under
|
||||
function vw_head(name: pointer, px: pointer) -> Node {
|
||||
let src = `function view_{reg_lower(name)}() -> UiView {{\n let v = new UiView\n v.name = "{name}"\n v.model = fn {px}model\n v.call = fn {px}call\n return v\n}}\n`
|
||||
let at = len(prog)
|
||||
vw_parse(src, g_parse_file, toks[pi].line, 0)
|
||||
return prog[at]
|
||||
}
|
||||
# parse generated source into the program, as if it were written in `file` at `line`
|
||||
function vw_parse(src: pointer, file: pointer, line: int, vis: int) -> void {
|
||||
let saved_toks = toks
|
||||
let saved_pi = pi
|
||||
let saved_file = g_parse_file
|
||||
let saved_parsing = g_parsing
|
||||
g_parse_file = file
|
||||
g_parsing = true
|
||||
lex_at(src, line)
|
||||
pi = 0
|
||||
let at = len(prog)
|
||||
while true {
|
||||
skipnl()
|
||||
if toks[pi].kind == TK_EOF { break }
|
||||
parse_one_decl()
|
||||
}
|
||||
var k = at
|
||||
while k < len(prog) {
|
||||
prog[k].vis = vis
|
||||
k += 1
|
||||
}
|
||||
toks = saved_toks
|
||||
pi = saved_pi
|
||||
g_parse_file = saved_file
|
||||
g_parsing = saved_parsing
|
||||
}
|
||||
68
selfhost/frontend/view_conv.ludic
Normal file
68
selfhost/frontend/view_conv.ludic
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# view_conv.ludic — L11: a value of the program as a Value a template reads. A number, a bool
|
||||
# and a string are themselves; a list is a list of its items; a record is an object of every
|
||||
# field a template could read (a field it could not - a function, a buffer - is left out).
|
||||
# Anything else answers null, and the caller says so.
|
||||
function vw_conv(v: int, ty: pointer, x: pointer, head: Node) -> pointer {
|
||||
if (ty == "int") { return `value_int({x})` }
|
||||
if (ty == "float") { return `value_float({x})` }
|
||||
if (ty == "string") { return `value_str({x})` }
|
||||
if (ty == "bool") {
|
||||
vw_helper(`function view_val_bool(b: bool) -> Val {{\n if b {{ return value_bool(1) }}\n return value_bool(0)\n}}\n`, "view_val_bool", head)
|
||||
return `view_val_bool({x})`
|
||||
}
|
||||
if len(ty) > 2 and ty[0] == '[' and ty[1] == ']' {
|
||||
let et: pointer = ty[2..len(ty)]
|
||||
let name = `view_val_list_{vw_mangle(et)}`
|
||||
if vw_have(name) { return `{name}({x})` }
|
||||
let item = vw_conv(v, et, "xs[i]", head)
|
||||
if item == null { return null }
|
||||
push(g_vw_helpers, name)
|
||||
vw_parse(`function {name}(xs: {ty}) -> Val {{\n let out = value_list()\n var i = 0\n while i < len(xs) {{\n value_add(out, {item})\n i += 1\n }}\n return out\n}}\n`, head.file, head.line, 1)
|
||||
return `{name}({x})`
|
||||
}
|
||||
let comp = find_comp(ty)
|
||||
if comp == null { return null }
|
||||
let name = `view_val_{vw_mangle(ty)}`
|
||||
if vw_have(name) { return `{name}({x})` }
|
||||
push(g_vw_helpers, name)
|
||||
var src = `function {name}(r: {ty}) -> Val {{\n if r == null {{ return value_null() }}\n let o = value_object()\n`
|
||||
var i = 0
|
||||
while i < len(comp.kids) {
|
||||
let f = comp.kids[i]
|
||||
if f.ty != null {
|
||||
let c = vw_conv(v, f.ty, `r.{f.s}`, head)
|
||||
if c != null { src = src + ` value_put(o, "{f.s}", {c})\n` }
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
vw_parse(src + " return o\n}\n", head.file, head.line, 1)
|
||||
return `{name}({x})`
|
||||
}
|
||||
function vw_have(name: pointer) -> bool {
|
||||
var i = 0
|
||||
while i < len(g_vw_helpers) {
|
||||
if (g_vw_helpers[i] == name) { return true }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
function vw_helper(src: pointer, name: pointer, head: Node) -> void {
|
||||
if vw_have(name) { return }
|
||||
push(g_vw_helpers, name)
|
||||
vw_parse(src, head.file, head.line, 1)
|
||||
}
|
||||
# a type as part of a name: []int -> list_int, a.B -> a_B
|
||||
function vw_mangle(ty: pointer) -> pointer {
|
||||
let n = len(ty)
|
||||
let out = bytes(n + 1)
|
||||
var i = 0
|
||||
while i < n {
|
||||
var c = ty[i]
|
||||
let ok = (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9')
|
||||
if not ok { c = '_' }
|
||||
out[i] = c
|
||||
i += 1
|
||||
}
|
||||
out[n] = 0
|
||||
return out
|
||||
}
|
||||
84
selfhost/frontend/view_finish.ludic
Normal file
84
selfhost/frontend/view_finish.ludic
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# view_finish.ludic — L11: when the program is read, each view gets its model and its call.
|
||||
# A field's type is the one it was given or its global's; converting it to a Value is a helper
|
||||
# written once for the whole program per type (view_val_<type>), so two views of one record
|
||||
# share it.
|
||||
function views_finish() -> void {
|
||||
var v = 0
|
||||
while v < len(g_vw_name) {
|
||||
vw_finish(v)
|
||||
v += 1
|
||||
}
|
||||
}
|
||||
function vw_err(v: int, msg: pointer) -> void {
|
||||
g_err_file = g_vw_node[v].file
|
||||
g_err_line = g_vw_node[v].line
|
||||
g_parsing = false
|
||||
perr(`view {g_vw_name[v]}: {msg}`)
|
||||
}
|
||||
function vw_finish(v: int) -> void {
|
||||
let head = g_vw_node[v]
|
||||
if find_comp("UiView") == null { vw_err(v, `a view is shown by ludic.ui - import "ludic.ui"`) }
|
||||
let px = vw_prefix(g_vw_name[v])
|
||||
var model = `function {px}model() -> Val {{\n let o = value_object()\n`
|
||||
let fields = g_vw_fields[v]
|
||||
var i = 0
|
||||
while i < len(fields) {
|
||||
let g = fields[i]
|
||||
let fname = g_vw_fnames[v][i]
|
||||
if g.ty == null { g.ty = vw_infer(v, g, fname) }
|
||||
let conv = vw_conv(v, g.ty, `{g.s}()`, head)
|
||||
if conv == null { vw_err(v, `{fname} is a {g.ty}, which a template cannot read`) }
|
||||
model = model + ` value_put(o, "{fname}", {conv})\n`
|
||||
i += 1
|
||||
}
|
||||
model = model + " return o\n}\n"
|
||||
vw_parse(model, head.file, head.line, head.vis)
|
||||
vw_parse(vw_call_src(v, px, head), head.file, head.line, head.vis)
|
||||
}
|
||||
# a field written `name = some_global` has that global's declared type
|
||||
function vw_infer(v: int, g: Node, fname: pointer) -> pointer {
|
||||
let e = g.a.kids[0].a
|
||||
if e.kind == E_ID {
|
||||
let d = find_global(e.s)
|
||||
if d != null and d.ty != null { return d.ty }
|
||||
}
|
||||
g_vw_node[v].line = g.line
|
||||
vw_err(v, `say what type {fname} is - {fname}: int = ...`)
|
||||
return "int"
|
||||
}
|
||||
# call(name, args): each query and event by its name, its arguments out of the list
|
||||
function vw_call_src(v: int, px: pointer, head: Node) -> pointer {
|
||||
var src = `function {px}call(name: string, args: Val) -> Val {{\n`
|
||||
let calls = g_vw_calls[v]
|
||||
var i = 0
|
||||
while i < len(calls) {
|
||||
let f = calls[i]
|
||||
var args = ""
|
||||
var p = 0
|
||||
while p < len(f.kids) {
|
||||
if p > 0 { args = args + ", " }
|
||||
args = args + vw_arg(v, f.kids[p], p)
|
||||
p += 1
|
||||
}
|
||||
let call = `{f.s}({args})`
|
||||
src = src + ` if name == "{g_vw_cnames[v][i]}" {{\n`
|
||||
if f.ty == "void" { src = src + ` {call}\n return value_null()\n` } else {
|
||||
let conv = vw_conv(v, f.ty, call, head)
|
||||
if conv == null { vw_err(v, `{g_vw_cnames[v][i]} answers a {f.ty}, which a template cannot read`) }
|
||||
src = src + ` return {conv}\n`
|
||||
}
|
||||
src = src + " }\n"
|
||||
i += 1
|
||||
}
|
||||
return src + " return value_null()\n}\n"
|
||||
}
|
||||
# a parameter out of the template's argument list
|
||||
function vw_arg(v: int, p: Node, i: int) -> pointer {
|
||||
let at = `value_at(args, {i})`
|
||||
if (p.ty == "int") { return `value_as_int({at})` }
|
||||
if (p.ty == "float") { return `value_as_float({at})` }
|
||||
if (p.ty == "bool") { return `value_as_int({at}) != 0` }
|
||||
if (p.ty == "string") { return `value_as_str({at})` }
|
||||
vw_err(v, `{p.s} is a {p.ty}; a template can pass an int, a float, a bool or a string`)
|
||||
return ""
|
||||
}
|
||||
67990
selfhost/ludicc.seed.ll
67990
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -23,6 +23,10 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/frontend/registry.ludic")
|
||||
push(f, "selfhost/frontend/registry_finish.ludic")
|
||||
push(f, "selfhost/frontend/resource.ludic")
|
||||
push(f, "selfhost/frontend/defaults.ludic")
|
||||
push(f, "selfhost/frontend/view.ludic")
|
||||
push(f, "selfhost/frontend/view_conv.ludic")
|
||||
push(f, "selfhost/frontend/view_finish.ludic")
|
||||
push(f, "selfhost/backend/emit_core.ludic")
|
||||
push(f, "selfhost/backend/emit_head.ludic")
|
||||
push(f, "selfhost/backend/emit_addr.ludic")
|
||||
|
|
|
|||
|
|
@ -724,7 +724,7 @@ function cmd_dev_test() -> int {
|
|||
feat_case("modules/visible", "", "42 42", "visible.ludic (L3: a module's exported function and event used from outside it)")
|
||||
reject_case("rejected/private_module", "add is private to module bank", "a private function of another module is refused")
|
||||
feat_case("lang/checked", "", "7.5 3 12 hi 2 ok", "checked.ludic (L4: literals take their slot's kind, string(p), []string as []pointer, null, named args)")
|
||||
reject_case("rejected/wrong_arity", "area takes 2 argument(s) and this call gives 1", "a call with the wrong number of arguments is refused")
|
||||
reject_case("rejected/wrong_arity", "this call to area leaves out h, which has no default", "a call with the wrong number of arguments is refused")
|
||||
reject_case("rejected/float_into_int", "metres wants an int and this is a float", "a float computed into an int is refused")
|
||||
reject_case("rejected/record_mixup", "d wants a Door and this is a Key", "one record type is not another")
|
||||
reject_case("rejected/record_as_text", "argument 1 of sign wants a string and this is a Trail", "a record is not text")
|
||||
|
|
@ -736,7 +736,7 @@ function cmd_dev_test() -> int {
|
|||
reject_case("rejected/generic_arity", "Pool takes 1 type argument(s) and Pool<int, string> gives 2", "a generic takes as many type arguments as it has parameters")
|
||||
reject_case("rejected/private_generic", "grow is private to module kit", "an instance of a private generic is private to its module")
|
||||
feat_case("lang/aliases", "", "10 5 3", "aliases.ludic (L6: a namespace method declared as an alias of a function, labelled or by its parameters)")
|
||||
reject_case("rejected/alias_arity", "Trail.length takes 2 argument(s) and this call gives 1", "an alias's arguments are checked against its target")
|
||||
reject_case("rejected/alias_arity", "this call to Trail.length leaves out to, which has no default", "an alias's arguments are checked against its target")
|
||||
reject_case("rejected/alias_twice", "Trail.length is declared twice", "a namespace method is declared once")
|
||||
feat_case("lang/registry", "", "3 Crate true 2 fire_ring -1", "registry.ludic (L8: a registry filled by def declarations - index constants, a count, the key, a lookup)")
|
||||
reject_case("rejected/registry_unknown", "there is no registry Tables", "a def names a registry that exists")
|
||||
|
|
@ -745,6 +745,15 @@ function cmd_dev_test() -> int {
|
|||
feat_case("lang/resources", "", "2 Axe 2 Split 10 0 1", "resources.ludic (L9: a registry read from a resource file at compile time, nested records typed by the schema)")
|
||||
reject_case("rejected/resource_field", "bad_tools.lres:3: error: field weight of Tool wants a float", "a resource file's entry is checked at its own line")
|
||||
reject_case("rejected/resource_missing", "cannot read the resource file data/no_such.lres", "a registry's resource file must exist")
|
||||
feat_case("lang/defaults", "", "a 300.0 8.0 / b 120.0 8.0 / c! 300.0 8.0 / d 300.0 2.0 / x / y", "defaults.ludic (L11: default parameters, positional then named arguments, in a generic too)")
|
||||
reject_case("rejected/default_missing", "this call to f leaves out a, which has no default", "a call leaves out only what has a default")
|
||||
reject_case("rejected/default_unknown", "f has no parameter c", "a named argument names a parameter the function has")
|
||||
reject_case("rejected/positional_after_named", "a positional argument after a named one", "positional arguments come first")
|
||||
controller_case("library/ui_templates", "", "ui: ui/peek.xml: <kit:Price>: that component is not exported - export=\"true\" on it lets other files use it purse 70, bought 1; the lantern is enabled: 0", "ui_templates.ludic (L11: a template reads a view, a press sends its event, component state and emit)")
|
||||
controller_case("library/ui_flex", "", "grow 97:282; between at 197; wrap lines at 51, 67; red 16711680; stretched 40; own size 12", "ui_flex.ludic (L11: flex layout - grow, justify, wrap, stretch - and stylesheets with imports and specificity)")
|
||||
controller_case("library/ui_html", "", "id beats class: 40; card 200 wide at 3,53 pad 4 8 12 16 border 2 inside 65280, outside -1; warn bg 16776960; items 3 of 30: 30; colours 16711680 255 -1; hr 1; children 7 clicks 1; the button now disabled 1 at opacity 0.5; text-align 1", "ui_html.ludic (L11: HTML elements, ids, classes, attributes, style=, CSS selectors and properties, the box model)")
|
||||
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")
|
||||
|
||||
# EV2 the world table: the mod reflection ABI, callable from Ludic by name.
|
||||
net_case("ecs/world_get", "50 1 7")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue