feat(ui): number and key fields, notes, anchored popovers, text-shadow, and pictures as painted

- <input type="number" min max step>: typed digits replace its value (a minus when min allows one,
  a point when step has a fraction), Enter or leaving the field commits them held between min and
  max, the left and right arrows step it.
- <input type="key">: Enter or a click starts it listening and the next key is its value, Tab, the
  arrows and Enter included; Esc stops it listening, Backspace clears it, `shown` names the value,
  and ui_capturing() tells the host to leave its keys alone (through the frame that ended it).
- note="..." under any control's label (.ui-labels > .ui-label + .ui-note); a range's (or number's)
  value with decimals, format="percent" and a unit.
- A popover with `anchor="id"` (the nearest element of that id) or a bare `anchor` (the element
  before it) sits beside it by `placement` (right, left, bottom, top), its margin the gap, opens to
  the other side where it would leave the screen, and is kept on it.
- A tooltip's title splits into lines at a newline or a written \n.
- text-shadow, inherited, drawn sharp under the text.
- ui_opacity(): the group opacity a native's draw is at.
- border-image is drawn as painted with no background colour, tinted by one, and not at all when
  that colour is transparent (it drew a white nine-slice).
- A picture file is drawn untinted, as a browser draws one; an atlas cell (prefix:name) still takes
  the color around it, and an <img> with its own color is tinted by it.
- The render3d backend loads a picture again when its file changes or appears (a failed load was
  kept for ever), and at once after ui_image_reload(path); takes a path with a drive letter (C:/...)
  as a path rather than an atlas; and slices a nine-slice by its texture's own width and height.
- ui_inputs.ludic and ui_look.ludic; LANGUAGE.md and the changeset say all of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 19:55:20 +03:00
parent 7d8f34de17
commit 9f59000f5b
39 changed files with 732 additions and 75 deletions

View file

@ -324,7 +324,12 @@ component Counter {
- **The template** has one root (use `<fragment>` for several). A component is used by its name as
a tag. `set count = 0` in an action sets its state, and `emit close` runs what its parent passed
as `on-close`. `class`, `style` and `id` on a component's tag land on its root element, styled by
the parent's sheet as well as its own.
the parent's sheet as well as its own; when that root is itself a component, every user in turn
passes theirs down (the outermost's `id` wins). The rules of all those sheets that match the root
are weighed together by specificity, as one sheet's are, and a user's rule wins a tie.
- **A component's names.** An event may be called anything, `set` included (`on set(v: int)`,
pressed as `set(4)`; `set x = ...` is still the action). A `string` prop given a number reads it
as text (`label="{3}"` is `"3"`). Two components of one name are an error that names both files.
- **Compiled in.** The compiler reads the template and the styles from beside the declaration and
inlines each `@import` (a path starting with `/` is from the project's root), so a missing
template fails the build and nothing has to ship beside the program. `ui_reload()` reads the
@ -368,12 +373,21 @@ applies to both:
- **The box model and flex.** Sizes are border boxes.
- `padding` and `margin` take one to four lengths, or one side by name (`padding-left`).
- `border` is `2px solid #ffcc00`, or `border-width` and `border-color`.
- A length is `12`, `12px`, `50%`, `fit`/`auto` or `fill`.
- A length is `12`, `12px`, `50%`, `fit`/`auto`, `fill` or `calc()`. `width: 0` and `height: 0`
are 0, not unset.
- `calc()` takes `+`, `-`, `*` and `/` and brackets over `px`, `em`, `rem`, `vw`, `vh`, plain
numbers and `var()`; in a `width` or `height` it may also hold a percentage of the room
(`calc(50% - 10px)`).
- `flex-grow` (or `flex`) shares out the spare room along `flex-direction`, and `fill` is a share
of 1. `justify-content` takes `flex-start`/`start`, `center`, `end`, `space-between`,
`space-around` or `space-evenly`.
- `align-items` and `align-self` take `start`, `center`, `end` or `stretch`. `flex-wrap: wrap`
breaks a row into lines, and `min-`/`max-width`/`-height` bound it.
- `flex-shrink` gives up room when a line's children want more than it has, each in proportion
to its shrink times its size, and none below its min size, a fixed size or its content. A scroll
box shrinks (and scrolls) and a box in a column shrinks as far as the scroll boxes in it let it,
so a list in a column takes the room its siblings leave with no height of its own. `flex: 1 0`
is the grow and the shrink. `order` rearranges a box's children without touching the tree.
- `gap`, `text-align`, `display: none`, `background(-color)`, `color`, `opacity` and `font-size`
are CSS's.
@ -411,8 +425,8 @@ applies to both:
neither takes room in the flow.
- Units: `em`, `rem`, `vw` and `vh`.
- `@media (min-width: ..) and (max-height: ..) { ... }`.
- Text wraps between words to fit its box; `white-space: nowrap` keeps one line and
`text-overflow: ellipsis` cuts it.
- Text wraps between words to fit its box - in a row, in the room its siblings leave it;
`white-space: nowrap` keeps one line and `text-overflow: ellipsis` cuts it.
- `overflow: auto|scroll|hidden` makes a box scroll.
- Selectors also take `+` and `~`, `:nth-child(2n+1)`, `:checked` and `:active`.
- **More React.**
@ -435,31 +449,51 @@ applies to both:
- A test drives all of it with `ui_input(i)`.
- **Controls are built in**, each made of plain parts a stylesheet styles (`.ui-label`,
`.ui-track`, `.ui-knob`, `.ui-fill`, `.ui-thumb`, `.ui-field`, `.ui-value`, `.ui-caret`,
`.ui-prev`, `.ui-next`):
`.ui-prev`, `.ui-next`; a track is a row, and a range's fill is as tall as its track):
- `<button>`;
- `<input type="checkbox|radio" label checked>`;
- `<input type="range" label min max step value>`, dragged or stepped with the arrows;
- `<input type="range" label min max step value>`, dragged or stepped with the arrows. Its value
shows with `decimals="{2}"`, as `format="percent"` of the way from min to max, and with a
`unit=" s"` after it;
- `<select label value>` with `<option value>`s;
- `<input type="text" label value maxlength>`, which takes typing, and Backspace takes a letter
back;
- `<input type="key" label value>`, which takes the next key pressed.
- `<input type="number" label min max step value>`: typed digits (a minus when min allows one, a
point when step has a fraction) replace the value, Enter or leaving the field commits them held
between min and max, and the left and right arrows step it (`decimals`, `unit` as a range's);
- `<input type="key" label value shown>`: Enter or a click starts it listening, and the next key
is its value - Tab, the arrows and Enter included. Esc stops it listening, Backspace clears it
(0), and `shown` is what it says for its value instead of the keyboard's label. While it
listens `ui_capturing()` is true, so the program can leave its own keys alone.
Any control's `note="..."` is a line of help under its label: `.ui-labels > .ui-label +
.ui-note`.
Each reports with `on-change` and `event.value`, and plays `sound="..."` (or "click") through
the program's `ui_sounds`.
- **Popovers, tooltips and bars.**
- `<div popover on-close="...">` sits absolutely in its parent and draws on the top layer. While
it is up, the pointer and Tab stay inside it, and a press outside it or Esc closes it.
- `title="..."` shows a tooltip (`.ui-tooltip`) after the pointer rests for half a second.
- `anchor="cell"` puts a popover beside the element with that id (the nearest, looking out from
the popover), and a bare `anchor` beside the element before it. `placement` is `right` (the
default), `left`, `bottom` or `top`, its margin on that side is the gap, and where it would
leave the screen it opens to the other side and is kept on it.
- `title="..."` shows a tooltip (`.ui-tooltip`) after the pointer rests for half a second. A
newline or a written `\n` in it starts another line (`.ui-tooltip-text` each).
- `<progress value max>` and `<meter value min max>` fill (`.ui-fill`) as far as their value.
- **Natives** are for what markup cannot say. `ui_native("minimap", measure, draw)` makes
`<minimap>` an element the program draws itself, laid out and styled like any other. It reports
with `ui_fire(n, "change", value)` and reads its attributes with `ui_attr` / `ui_attr_on`.
with `ui_fire(n, "change", value)` and reads its attributes with `ui_attr` / `ui_attr_on`. Its
draw reads `ui_opacity()`, the opacity it is drawn at (every group `opacity` around it, its own
included).
- **Renderers.** `ui_backend(b)` takes any renderer with `rect`, `text` and `measure`, and uses
`round`, `ring`, `image`, `nine`, `clip`, `scale` and `now` when it has them.
Importing `ludic.ui/render3d.ludic` makes render3d's overlay the renderer:
- images by path, and cells of an atlas named with `ui_atlas(prefix, texture, cols, rows,
names)` as `<img src="prefix:name">` (or `prefix:12`);
- nine-slices for `border-image`;
names)` as `<img src="prefix:name">` (or `prefix:12`). A path is read as sRGB, the interface's
own space, so its colours arrive as painted. A file that changes on disk (or appears) is loaded
again within half a second, and `ui_image_reload(path)` asks for it at once;
- nine-slices for `border-image`, sliced by the texture's own width and height;
- a scale from the screen's height (`ui_render3d_scale` for the player's interface size).
`ui_translator(fn)`, `ui_sounds(fn)` and `ui_clock(fn)` hand the runtime the program's
@ -468,8 +502,13 @@ applies to both:
- Colours: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()` and the basic names,
usually through `var(--...)` from a theme.
- Boxes: `border-radius`, `box-shadow` (sharp, offset), `background-image: url(...)`, and
`border-image: url(...) slice / width` as a nine-slice. A nine-slice is tinted by the
background colour, so one rounded texture serves every colour.
`border-image: url(...) slice / width` as a nine-slice. With no background colour a nine-slice
is drawn as painted, as CSS draws one; with one it is tinted by it, so one rounded texture serves
every colour, and `transparent` (or any colour with no alpha) leaves nothing to see.
- Text: `text-shadow: x y blur colour` draws the text again under itself, offset (the blur is
drawn sharp), and is inherited.
- Pictures: an `<img>` of a file is drawn as it is; a cell of an atlas is a glyph and takes the
`color` around it, as text does. An `<img>` with a `color` of its own is tinted by it.
- Motion: `opacity` fades the element and everything inside it. `@keyframes` with `animation:
name 0.25s [infinite] [alternate]` animate numeric properties from when the element first
appeared, and `transition: opacity 0.2s` eases a changed opacity.