Fix a lot of broken things.

This commit is contained in:
Signal 2026-02-02 13:58:57 -05:00
parent 6e0f653928
commit be73993547
2 changed files with 309 additions and 95 deletions

View file

@ -95,6 +95,12 @@ If you want to use the state API for a system of your own, remember that:
* When set, a state notifies each entry in `_getters` by calling that object's `:update()` method.
* To collect state dependencies from a given function, 1) create a tracking table and `table.insert(imfs.state.observers, tracker)`, 2) call the function, and 3) `table.remove(imfs.state.observers)`. The tracking table will then be filled with all states that were accessed between the `table.insert` and `table.remove`.
### IDs
One of the biggest problems with the immediate-mode approach is that it is difficult to determine when a particular element has changed, since a given build run is not guaranteed to match the output of the previous run in any way, and if element IDs were unique, it would be nigh-impossible to determine what an element's previous value was without shifting to a more retained-mode approach. To work around this, the internal IDs of elements are assigned somewhat deterministically based on the order of their creation. Thus, if a given builder always produces the same sequence of element creations in the same order, each element will always be assigned the same ID on every run. This heuristic, of course, breaks down when a builder function does a lot of conditional or iterative rendering, because the size and ordering of the element tree can change significantly across rebuilds. To solve this, imfs provides `imfs.scope()`. This will create a named sub-scope, causing elements created within it to be identified relative to the beginning of the sub-scope, rather than the global scope. Thus, if an element tree within a scope doesn't change layout, the elements will have stable IDs that allow `onchange` and the like to work properly.
With that said, it is generally not important for you to use scopes in your UI, because most elements don't depend on knowledge of their previous state. The exceptions include fields and scroll containers. Fields need to know their old value in order to determine whether to fire an `onchange` event, otherwise they would be compelled to call `onchange` every time some element triggers an action (even if that action did not affect state). Unmanaged scroll containers need to know their previous state so that they can preserve their scroll position across rebuilds despite having no external state object with which to track it. (Note also that these elements still need not be enclosed in a scope if the element tree preceding them is constant.)
# Example code
Create a mod with this code and run `/demo` to see the example in action.
@ -180,7 +186,7 @@ minetest.register_chatcommand("demo", {
### `imfs.export()`
Export the imfs API to \_G, with "fs_" prefixes. This is mainly intended for custom, from-scratch games to make life a bit easier.
Export the imfs API to `_G`, with "fs_" prefixes. This is mainly intended for custom, from-scratch games to make life a bit easier.
### `imfs.begin(window_width, window_height)`
@ -191,11 +197,20 @@ Begin a root imfs container. This returns a `Window` object with the following m
* `Window:padding(x, y)`: Set the padding of the formspec window. See `padding[]` in the [Minetest API documentation](https://api.luanti.org/formspec/).
* `Window:modal([modal])`: Make this window modal (meaning that it cannot be directly closed by the user with Esc). If `modal` is false, there will be no effect (this can be useful for making modality state-dependent).
* `Window:onclose(fn)`: Registers `fn` to be called when the window closes. (This may be triggered when the target player leaves the game.)
* `Window:bgcolor(foreground, fullscreen)`: Set the window's background color. If `fullscreen` is absent or false, `foreground` will be applied as the window's foreground color. If `fullscreen` is `true`, `foreground` will be applied as the window's background color. If `fullscreen` is a string, `foreground` will be the window's foreground color and `fullscreen` will be the window's background color.
### `imfs.end_()`
Ends the current window, and returns it. Note: The name ends with an underscore so as not to conflict with the Lua keyword (I couldn't think of a better word for ending the window than 'end').
### `imfs.scope(name)`
Begins a scope with the given name. All subsequent elements will be identified with indices relative to the beginning of the scope.
### `imfs.scope_end()`
Exits the last entered scope. Subsequent elements will resume identification relative to the parent scope.
### `imfs.show(player, builder, state)`
Show the interface defined by the provided builder function to the given player. The generated context will remain active until the player closes the formspec or leaves the game.
@ -231,7 +246,7 @@ End the current container and revert to its parent. This can be used to create c
Note that all elements have a `:render()` method that outputs their formspec representation, and accepts overrides for x, y, width, and height as extra arguments. This is used for creating custom layouting containers which interpret position and size in a non-absolute way.
Many sized elements have a `:tooltip(text[, background_color[, text_color]])` method that creates a tooltip for the element. Note that for elements without a `name` attribute in the formspec format, this will be an area tooltip and will not be occluded by any overlaying elements. Since imfs doesn't track which elements occlude which, it is up to you to not add area tooltips to occluded elements.
Many sized elements have a `:tooltip(text, enabled[, background_color[, text_color]])` method that creates a tooltip for the element. Note that for elements without a `name` attribute in the formspec format, this will be an area tooltip and will not be occluded by any overlaying elements. Since imfs doesn't track which elements occlude which, it is up to you to not add area tooltips to occluded elements. An easy way to do this is to pass a boolean as the second argument: if `false`, the tooltip will not be added. If the second argument is not a boolean, it will be interpreted as `background_color` (and `background_color` as `text_color`).
### `imfs.label(x, y, text)`