An immediate-mode paradigm for Minetest formspecs.
Find a file
2026-01-23 19:17:20 -05:00
init.lua Initial commit. 2026-01-23 19:17:20 -05:00
LICENSE.txt Initial commit. 2026-01-23 19:17:20 -05:00
mod.conf Initial commit. 2026-01-23 19:17:20 -05:00
README.md Initial commit. 2026-01-23 19:17:20 -05:00

What is this?

Imfs is a reactive, immediate-mode functional abstration over Minetest's often cumbersome string-based formspec format. What this means is that:

  • An interface (or "formspec") is represented by a builder function, which creates and returns an imfs element tree.
  • Every time state changes, the builder function is called again to create a new tree reflecting the new state.
  • Elements and containers are represented as linear function calls. You don't have to care about child management or tree manipulation; imfs does that internally.
  • You don't have to manually interpolate variables.
  • You don't have to remember to update the UI every time you make a state change. By wrapping data in states, you can make a fully reactive UI contained almost entirely in one builder function.
  • You don't have to care about element names and event handling. With imfs, event handlers can be registered inline, while the library does the rest.

Concepts

Builder functions

A builder function is a function that creates and returns an element tree to be rendered. There is thus only one requirement of such a function: it must call imfs.begin(window_width, window_height), then return imfs.end_(). Elements may then be added between the calls to start and end_. The most important advantage of this approach is that it allows you to perform conditional rendering merely by using if statements, in addition to any other operations you may see fit.

Builder functions are used by either imfs.show (to show an interface to a player) or imfs.set_inventory (to set the player's inventory). These functions are described in more detail below.

You can pass a table as the last argument to an entry point like imfs.show to hold state external to the builder function; it will then be passed as an argument to the builder function during every rebuild. This way, you can declare the builder function itself in one place and use it in another without having to rely entirely on closure capture or global variables.

Imfs does not strictly require using a builder function: if an interface is purely static and need never change, you can use a prebuilt element tree instead of a builder function, which avoids rebuilding the tree every time the interface is shown.

Elements

Elements are created with function calls, like imfs.box(0, 0, 2, 2, "#faa"). This creates an element, adds it to the current container, and returns it. For simple elements like label, this is all that needs to happen; however, some elements, like field, support further configuring the element using chainable methods, like so:

imfs.field(4, 2, 4, 0.75, "Test", str)
    :multiline()
    :onchange(function(value)
        print(value)
    end)

Notice that you can register an event handler with :onchange; in this case, the event handler will be called whenever a change in the field's value is detected. This makes event handling quite straightforward, since event responses are tied directly to the element that is expected to trigger them (rather than being crammed together in a single callback registration somewhere else).

Because elements are just function calls, you can easily create your own widgets simply by creating a function that creates a predefined sequence of elements. For example:

-- Adds four colored boxes using a single call.
local function quadbox(x, y, w, h)
    imfs.box(x, y, w/2, h/2, "#faa")
    imfs.box(x + w/2, y, w/2, h/2, "#afa")
    imfs.box(x, y + h/2, w/2, h/2, "#aaf")
    imfs.box(x + w/2, y + h/2, w/2, h/2, "#ffa")
end

local function builder()
    imfs.begin(12, 10)
    
    imfs.label(0.5, 0.5, "Quadbox:")
    
    quadbox(0.5, 1, 4, 4)
    
    return imfs.end_()
end

More advanced widgets with chainable methods can be created using Lua classes (the implementations of the builtin elements typify this technique).

Containers

Containers are, in essence, lists of elements. There are three types of containers: scroll containers, layouting managers, and the formspec itself. The top-level formspec container performs minimal layouting; it only evaluates percentage units to always be relative to its own size. The scroll container functions similarly, but wraps its children in a scroll_container[] element internally.

Besides these basic containers, however, imfs also provides several layouting containers. These are:

  • imfs.group: The same type of container as the root; the positions and percentage units of its children are relative to the position and size of the container respectively.
  • imfs.row: A flex row. This automatically positions elements within it based on the provided gap and alignment. Child elements may specify their width as a grow ratio like "1x" to dynamically resize to fill any empty space in the row. Higher grow ratios will cause the element to take up a larger portion of the available space compared to other dynamically sized elements.
  • imfs.column: The same as imfs.row, but in the vertical direction.

To create a custom layouting container, the procedure is essentially as follows:

  1. Create a Lua class for the container.
  2. In the class constructor, call imfs.container_start(container) with the newly created container instance.
  3. Create an end function that wraps imfs.container_end().
  4. In the class's render(self) method, for _, child in ipairs(self) do child:render() end.
  5. Add custom layouting calculations to render(), and pass modifications to child:render().

(Be advised that certain elements, like labels, have no knowable size.)

State

Being able to imperatively create a UI is nice, and already better than formspecs, but many UIs must dynamically update to reflect state. Doing this manually can become bothersome, which is why imfs is also reactive. This means that it can trigger a rebuild automatically whenever state changes, saving you the trouble of remembering to update the UI when you update a variable. To do this, you must set any of an element's properties to a state object.

State is created by wrapping the initial value in an imfs.state, like so: local counter = imfs.state(0). To read the state's value (and register it as a dependency of any active observers, like an imfs UI), call it like a function: counter(). To set the state's value (and notify any dependents), call it with the new value: counter(3).

Imfs also provides imfs.derive, which creates a derived state from a function based on the states accessed in the funtion. This way, you can perform operations on a state's value for display while still depending on the state from the element involved. It should be noted, however, that beause full rebuilds for every change are an intrinsic property of formspecs, there is not currently much benefit to having a certain element depend on a state rather than the formspec itself, so in most situations derived states will not really be useful. An exception is if you want to create side effects; since imfs.derive takes a function, you can do anything you like in that function, so it might sometimes be useful to have a function that is called whenever a dependent state's value is changed. (Indeed, this mechanism may prove more useful in non-UI applications than in actual imfs UI.)

Each state object includes an _old_val property, which holds the value the state had before it was last changed. In addition, you can also use the _val property to access a state's value directly and bypass dependency resolution.

If you want to use the state API for a system of your own, remember that:

  • State objects store dependents in _getters.
  • 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.

Example code

Create a mod with this code and run /demo to see the example in action. Note particularly that if you join multiple users and have them all run /demo, any action by one will also update all the others' formspecs, as the state used here is global.

local counter = imfs.state(0)
local str = imfs.state("")
local pressed = imfs.state(false)

local function fs()
    imfs.begin(12, 10)
    
    imfs.label(1, 2, "Counter: "..counter()) -- We don't need to use a derived state here because the window will detect this dependency.
    imfs.label(1, 3, derive(function() return "Length: "..#str() end)) -- However, using a derived state will still work just fine.
    
    if pressed() then -- Reactive conditional rendering can be achieved using states.
        imfs.label(1, 4, "Pressed!!")
    end
    
    imfs.button(4, 0.5, 4, 0.75, "Increment")
        :style {
            bgcolor = "#aaf"
        }
        :style("hovered", {
            bgcolor = "#faa"
        })
        :style("pressed", {
            bgcolor = "#afa"
        })
        :onclick(function()
            counter(counter() +1)
        end)
        
    imfs.field(4, 2, 4, 0.75, "Test", str)
        :onchange(function(value)
            str(value)
        end)
        :onenter(function(value)
            str(value)
            pressed(true)
        end)
        
    
    imfs.scroll_container(4, 3, 8, 7)
        :named("test")
    
    for i = 0, 10 do -- One benefit of functional style over table style is that you can use loops seamlessly.
        imfs.label(0, i, i)
    end
    
    imfs.row(0, 1, 8, 4)
    
        imfs.box(0, 0, "1x", 2, "#f88") -- The "1x" unit for the width marks this element as dynamically resizing.
        imfs.box(0, 0, counter() *0.25, 2, "#8f8")
        imfs.box(0, 0, 2, 2, "#88f")
    
    imfs.row_end()
    
    imfs.column(0, 3, 4, 8)
        :align "center" -- Notice that the elements are centered vertically in the column.
    
        imfs.box(0, 0, 2, counter() *0.25, "#8f8")
        imfs.box(0, 0, 2, 2, "#88f")
    
    imfs.column_end()
    
    imfs.scroll_container_end()
    
    return imfs.end_()
end

minetest.register_chatcommand("demo", {
    func = function(name)
        imfs.show(name, fs)
    end
})

API Reference

Functions

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.

imfs.begin(window_width, window_height)

Begin a root imfs container. This returns a Window object with the following methods:

  • Window:no_prepend(): Disables formspec prepends (i.e. global theme) in this interface.
  • Window:position(x, y): Set the position of the formspec window. See position[] in the Minetest API documentation.
  • Window:anchor(x, y): Set the anchor point of the formspec window. See anchor[] in the Minetest API documentation.
  • Window:padding(x, y): Set the padding of the formspec window. See padding[] in the Minetest API documentation.
  • 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.)

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.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.

This returns a Context object, which is the the top-level dependent for all states used in the interface. Unlike in some other libraries, you don't really store state on the Context object (the builder function can't access it directly). Instead, you should store state as local variables in the function from which you call show (or somewhere else if they should be global) along with the builder function; that way, the builder function can capture the state specific to the action and the player that invoked the interface and only that. You can alternatively declare the builder function externally, then pass the new state objects in the args table, as it will be passed on to the builder function and will persist across rebuilds. This is arguably the better option since it avoids re-allocating the builder function every time it's first shown.

You can pass a static imfs tree instead of a builder if you so desire.

imfs.set_inventory(player, builder, state)

Set player's inventory to the interface defined by builder. This context will persist indefinitely until it is manually removed or the player leaves the game. Otherwise, this behaves in the same way as imfs.show.

imfs.clear_inventory(player)

Manually invalidate the imfs context associated with player's inventory formspec. Doing this will not clear the player's inventory formspec; it is intended for interoperability with mods that use raw formspecs for the player's inventory (e.g. if your main inventory uses imfs but can switch to a view from a mod that doesn't), so that imfs will not erroneously keep handling events for such formspecs.

imfs.add_to_context(element)

Add this element to the current container. This can be used in the creation of custom elements if the element needs to render itself in its container (rather than merely being an alias for a longer series of elements).

imfs.container_start()

Creates and returns a new container base table, adds it to the current container, then makes it the new context to which subsequent elements are added. This can be used to create custom containers.

imfs.container_end()

End the current container and revert to its parent. This can be used to create custom containers.

Elements

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.

imfs.label(x, y, text)

Creates a label element at the given position with the given text.

imfs.arealabel(x, y, width, height, text)

Similar to imfs.label, but the created label will line-wrap. By default, overflowing text will be clipped to the label's bounding box.

Methods:

  • :scrollable(): Causes overflowing text to make the label scrollable rather than being clipped.

imfs.box(x, y, width, height, color)

Creates a colored rectangle.

imfs.hypertext(x, y, width, height, hypertext)

Creates a hypertext element. Refer to the markup language reference in the Minetest API documentation for what hypertext may consist of.

Methods:

  • :onaction(fn): Registers fn to be called when an <action> element is triggered in this element.

imfs.style(name[, state], properties, [internal])

Creates a style element. Refer to the style reference in the Minetest API documentation for which properties may be used and on which element types.

If internal is true, this element will not be automatically added to the element tree. This is used to allow inline styles on elements, since the style element must come first and the target element's name is not known beforehand.

imfs.tooltip(x, y, width, height, text[, background_color[, text_color]])

Creates a tooltip that will be shown when the user hovers over the target area.

imfs.image(x, y, width, height, texture, [middle])

Creates an image element. If middle is provided, the image will be rendered 9-sliced with middle as the center tile.

Methods: :animated(frames, duration = 50, start = 1): The image will be interpreted as a tile animation with the given properties.

imfs.item_image(x, y, width, height, item)

Creates an item image element.

imfs.model(x, y, width, height, mesh, textures)

Creates a model element.

Methods:

  • :style([state, ]props): Applies the styling properties props to the model when in the state state. If omitted, state is "default".
  • :rotation(rotation[, continuous]): Set the rotation of the model in the view, and optionally whether it is continuous.
  • :mouse_control(state): Passing false will prevent the user from changing the model's rotation by clicking and dragging.
  • :animated(frames, speed): Sets an animation on the model defined by the given frame range and speed.

imfs.button(x, y, width, height, label)

Creates a button element.

Methods:

  • :style([state, ]props): Applies the styling properties props to the button when in the state state. If omitted, state is "default".
  • :item_image(item): Convert this button to an item image button, representing the given item.
  • :image(image[, pressed_image]): Convert this button to an image button, with the texture image. If specified, pressed_image will be the image shown by the button in pressed state. (Note that this can also be achieved using styles.) This will have no effect if :item_image is called on the same button.
  • :exit(): This button will close the formspec when pressed. This will have no effect if :item_image or :image with a pressed_image is called on the same button.
  • :onclick(fn): Registers fn to be called when this button is pressed.

imfs.list(x, y, width, height, location = "current_player", list = "main"[, start])

Creates an inventory element.

imfs.inventory is an alias for this element.

imfs.listring([location, list])

Inserts a listring[] element. Refer to the Minetest API documentation for an explanation of listrings.

imfs.field(x, y, width, height[, label], value)

Creates a field element.

Methods:

  • :style([state, ]props): Applies the styling properties props to the field when in the state state. If omitted, state is "default".
  • :onchange(fn): Registers fn to be called with the new value when the field's value changes.
  • :onenter(fn): Registers fn to be called with the field's value when Enter is pressed in the field. (This will never trigger if the field is multiline.)
  • :multiline(): Makes this field multiline. :close_on_enter() will have no effect if this is present.
  • :close_on_enter(): This field will close the formspec when Enter is pressed. (Note that the default in imfs is not to close the formspec when Enter is pressed, contrary to ordinary formspecs.)
  • :password(): Makes this field a password field. :multiline() will have no effect if this is present.

imfs.textarea(x, y, width, height[, label], value)

An alias for imfs.field(...):multiline().

imfs.checkbox(x, y, label, checked)

Creates a checkbox element.

Methods:

  • :onchange(fn): Registers fn to be called with the new value when the checkbox's value changes.

imfs.scrollbar(x, y, width, height[, orientation = "vertial"], value)

Creates a scrollbar element.

Methods:

  • :style([state, ]props): Applies the styling properties props to the scrollbar when in the state state. If omitted, state is "default". (This isn't very useful right now, but it might be if scrollbars become themable.)
  • :options(optios): Set the scrollbar options to options. Refer to the Minetest API documentation for which options are permitted.
  • :onchange(fn): Registers fn to be called with the action and value value of each scroll event for this scrollbar.

imfs.scroll_container(x, y, width, height, orientation = "vertical"[, factor][, padding])

Begins a scroll container. If you wish to manually set up the scroll container's scrollbar, you must pass "" for padding. Note that if padding is not provided, factor will serve as an alias to it.

Methods:

  • :scrollbar(fn, ...): Use the scrollbar returned by fn() instead of a generated one. Alternatively, if fn is not a function, this will just generate a scrollbar with imfs.scrollbar(fn, ...).
  • :named(name): Set the name of this scroll container. If you want to persist the container's scroll position during rebuilds without putting forth much effort into tracking state and managing the scrollbar, passing a static string here will do so.

imfs.scroll_container_end()

Ends the current scroll container.

imfs.group(x, y, width, height)

Creates a group container. Group containers exist solely to position all their children relative to themselves, rather than to the top-level formspec. Note that percentage units will also be relative to the group's size rather than the window's.

imfs.group_end()

Ends the current group.

imfs.row(x, y, width, height)

Creates a flex row container. Flex containers will automatically lay out their content according to the provided alignment and gap, ignoring user-provided position. Immediate children of flex containers may specify their width (or height) property as a grow ratio (e.g. "1x"), which will cause them to automatially resize along the container's axis to fill any empty space. If an element has a higher grow ratio than another, it will take up a greater proportion of the available space.

Methods:

  • :align(alignment): Set the alignment of elements in this row. May be "left" (default), "right", or "center".
  • :gap(gap): Set the spacing between elements in this row.
  • :direction(direction): Set the direction in which this row will lay out its children. May be "row" (X axis, default) or "column" (Y axis).

imfs.row_end()

Ends the current row.

imfs.column(x, y, width, height)

An alias for imfs.row(...):direction("column").

imfs.column_end()

An alias for imfs.row_end().