Skip to content
Gregory Hess edited this page May 2, 2024 · 45 revisions

Lönn supports the creation of custom form fields(which is what the fieldType controls in the fieldInformation) by placing them in the ui/forms/fields folder. Form fields allow you to control what kinds of inputs your forms can accept.

table of contents

Specific Validation

If you want to have reusable validation for a textbox input, you can use the following pattern:

local stringField = require("ui.forms.fields.string")

local exampleField = {} --usually this variable would be named for what kind of field it is.
 
exampleField.fieldType = "modName.fieldName" --change this to use your mod Name and field name. Should be unique

function exampleField.getElement(name, value, options)
    -- Add extra options and pass it onto string field

    --optional. Turns a string into the kind of value you want
    options.valueTransformer = function(s)
        --[your code here]
    end
    --optional. This is applied to the internal value to determine what to display(the output should be a string)
    options.displayTransformer = function(v)
        --[your code here]
    end
    --Takes the output of value transformer and returns a truthy value when the value is valid
    --If used with the options parameter in fieldInformation, v is the provided value instead
    options.validator = function(v)
        --[your code here]
    end

    return stringField.getElement(name, value, options)
end

return exampleField

Note that the options parameter is a table containing everything in the fieldInformation but the fieldType, which allows you to parameterize the behavior of your form.

For an example of how to use this pattern, the following code from Loenn Project Manager is a field that validates that a string is a valid cross-platform filename.

local stringField = require("ui.forms.fields.string")

-- A field for valid file names on windows(also should work on mac and linux, though is needlessly restrictive)
local fileName = {}
 
fileName.fieldType = "loennProjectManager.fileName"
--we all love windows so much. The following filenames are illegal to use on windows(with or without an extension, case doesn't matter)
local reserved = {"CON", "PRN", "AUX", "NUL", "COM0",
    "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
    "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}
local whitelist="[a-zA-Z0-9%-_ ]+" -- should be fine for all POSIX compliant filesystems and windows
function fileName.getElement(name, value, options)
    -- remove leading/trailing spaces
    options.valueTransformer = function(s)
        return s:match( "^%s*(.-)%s*$" )
    end
    options.validator = function(v)
        --allow the user to configure weather they handle ""
        if #v==0 then return options.allowEmpty end
        --make sure we aren't using a reserved windows filename
        for i,s in ipairs(reserved) do
            if s==string.upper(v) then return false end
        end
        --check to make sure the filename only uses safe characters(and doesn't start with -)
        local i,j,_= string.find(v,whitelist)
        return i==1 and j==#v and string.sub(v,1,1)~="-"
    end

    return stringField.getElement(name, value, options)
end

return fileName

Integrating UI and Advanced Forms

For creating custom UI elements, see Custom UI Elements. This section will cover advanced form setup and the built-in UI elements. If you want to see the source code for the forms that ship with lönn by default, you can view the source code files on the ui-olympUI branch here.

Form fields should provide the following fields:

  • fieldType The fieldType, as used in the fieldInformation section. For mods this is usually formatted as "modIdentifier.fieldName"
  • _MT.__index The metatable for the field. Metatables are how lua does "classes", which is required here to allow individual instances of your field to have differing behaviors and data. This usually contains at least the following methods, but can have more. Note: these methods are proceeded by a : to show they are instance methods. This means you create them like function field._MT.__index:fun(). This syntax is equivalent to passing self as the first parameter
    • :setValue(value) A function which sets the value for this field. Many fields use self.currentValue to store the current value. It is often sensible to update the text of the relevant ui element(s) in this function
    • :getValue() A function which returns the current value for this field
    • :getCurrentText() A function which gets the current text for the ui element.
    • :fieldValid() A function which returns true if the current value is valid for this field
  • getElement(name, value, options) Returns a new instance of this formfield. The return value should have its metatable set to fieldVar._MT. The returned field has, at minimum, the following fields:
    • width: the width of the field. 4 units of width is 1 line when displayed
    • name: the name used for this instance of the field. Usually just the name parameter of the function
    • elements: a list of the UI elements associated with this form field

Additionally, the UI elements are usually attached individually as fields in the returned object. Usually, the getElement method is the most complicated component. For example, here is the getElement method for the boolean field

--snip--
local uiElements = require("ui.elements")

local booleanField={}
--snip--
function booleanField.getElement(name, value, options)
    local formField = {}

    local minWidth = options.minWidth or options.width or 160
    local maxWidth = options.maxWidth or options.width or 160

    local checkbox = uiElements.checkbox(options.displayName or name, value, fieldChanged(formField))
    local element = checkbox

    if options.tooltipText then
        checkbox.interactive = 1
        checkbox.tooltipText = options.tooltipText
    end

    checkbox.centerVertically = true

    formField.checkbox = checkbox
    formField.name = name
    formField.initialValue = value
    formField.currentValue = value
    formField.sortingPriority = 10
    formField.width = 1
    formField.elements = {
        checkbox
    }

    return setmetatable(formField, booleanField._MT)
end
return booleanField

This field uses 1 UI element, a checkbox. Many advanced forms might use 2 or more UI elements, which can all be added to the elements list. Additionally, notice this field relies on a higher order function fieldChanged, which returns a callback function. This is a common pattern.

There are a wide variety of UI elements available in Lönn which you can see in the OlympUI repository. There are a lot of elements available for use, including:

  • button: a button you can click. When clicked it calls a provided callback
  • checkbox: a checkbox.
  • dropdown: a dropdown list as used by string based fields when provided an options table
  • field: an editable text field. Used by most form fields(either directly or indirectly).
  • label: a text label. Most forms use one of these to display text before the editable portion of the field You can create each as follows:
  • uiElements.button(text, callback: function(button)) Creates a new button with text displayed on it. callback is called when the button is clicked, with button element as a parameter. If this returns a value, nothing is done with it
  • uiElements.checkbox(display, value, callback: function(element, new)) Creates a new checkbox with the text display next to it. The value parameter controls the initial state. callback is called when the box is checked or uncheck with the checkbox element and the new value as parameters
  • uiElements.dropdown(optionStrings,callback: function(element, new)) Creates a new dropdown with the options specified by optionStrings. callback is called when an item is selected with the dropdown element and the new value as parameters
  • uiElements.field(value, callback: function(element, new, old)) Creates a new editable text field with the initial value of value. callback is called when the field is edited with the field element, new value, and old value as parameters
  • uiElements.label(text) Creates a new label with the text specified

Additionally, many of these returned elements can be modified with the :with(config) method, which takes configuration options. This can be used to set the minimum and maximum width of elements. For example,

local field = uiElements.field("",fieldChanged(formField)):with({
        minWidth = minWidth,
        maxWidth = maxWidth
    })

Creates a new editable text field with an empty default value, a callback provided by a higher order function, and a minimum and maximum width set elsewhere

Similarly, some UI elements can be further modified with the :setEnabled(enabled) method, which can be used to disable a UI element(ie make a text field not editable), and the :setPlaceholder(text) method can be used to set the default text of a editable text field.

You can also use the :setText(text) method to update the displayed text of an editable text field(say in the _MT.__index:setValue() on your form), and the :repaint() method to force a UI element to redraw itself. Equivalently, you can set the label.text field of a button element to update its text(and label.tooltipText to update the tooltip)

You can modify the style of elements by accessing its style field, which can be used to make fields red to show invalidity.

Finally, labels can be given tooltips by setting their tooltipText field.

For an example, you can look through ui/forms/fields/string.

Using Widgets in Forms

Lönn provides many UI widgets that let you make custom UI without actually making custom UI! There are, of course, a few restrictions, but widgets are a very power feature. You can view which widgets are available as well as their source code here. These can be used in your forms by using a context menu, which allows you to open widgets on click! To use this, add local contextMenu=require("ui.context_menu") to your file, as well as the require needed for whichever widget you want to use. Then, you can add a context menu to a UI element with local elementContext=contextMenu.addContextMenu(element, widgetFactory,options) with the parameters described bellow

  • element: the UI element to add a context window to
  • widgetFactory(): a function that returns a widget
  • options: a table of options for the context menu, including
    • shouldShowMenu(element, x, y, button): a function that determines when the menu should be shown. Mandatory to get the menu to show.
    • mode: What mode to spawn the menu in. You probably want this to have the value "focused"

Then, you replace element with elementContext in the list of elements attached to your form field instance in getElement. As an example, the following code is from the dev version of Lönn Project Manager

local contextMenu = require("ui.context_menu")
local grid = require("ui.widgets.grid")
--snip--
function positionField.getElement(name, value, options)
    local formField = {}
    --snip--
    --pUtils here is just a custom library, don't worry about it
    local field = uiElements.field(pUtils.listToString(value,", "),overFieldChanged(formField)):with({
        minWidth = minWidth,
        maxWidth = maxWidth
    })
    local posX = uiElements.field(tostring(value[1]), fieldChanged(formField,1)):with({
        minWidth = nMinWidth,
        maxWidth = nMaxWidth
    })
    local posY = uiElements.field(tostring(value[2]), fieldChanged(formField,2)):with({
        minWidth = nMinWidth,
        maxWidth = nMaxWidth
    })
    local posZ = uiElements.field(tostring(value[3]),fieldChanged(formField,3)):with({
        minWidth = nMinWidth,
        maxWidth = nMaxWidth
    })
    --snip--
    local x = uiElements.label("x")
    local y = uiElements.label("y")
    local z = uiElements.label("z")
    local fieldContext = contextMenu.addContextMenu(
        field,
        function ()
            return grid.getGrid({
                x,y,z,
                posX,posY,posZ
            },3)
        end,
        {
            shouldShowMenu = shouldShowMenu, --shouldShowMenu is just a function defined elsewhere. Currently that definition always returns true
            mode = "focused"
        }
    )
    --snip--
    formField.posX = posX
    formField.posY = posY
    formField.posZ = posZ
    formField.field = field
    --snip--
    formField.elements = {
        label, fieldContext
    }

    formField = setmetatable(formField, positionField._MT)

    return formField

Notice how we create some UI elements, add a context menu to one of them, and pass the others into a widget. In this case, the widget is a grid so these elements are displayed nicely in another window. But we can still keep references around to them for our convivence.

Of the available widgets, the following seem the most useful in forms:

  • collapsable: A widget that when clicked expands into a column. Created with collapsable.getCollapsable(text, content, options) where text is used as the header label, content is a UI element or widget, and options is an optional table with the field startOpen
  • colorPicker: The color picker used by the color field. See the color field for more info
  • grid: a grid of UI elements. Basically creates new window with the provided elements in a grid. Created with grid.getGrid(elements,columnCount) where elements is the list of elements in the grid, and columnCount is the number of columns in the desired grid
  • lists: A searchable list. Created by lists.getList(callback, items, options) or lists.getMagicList(callback, items, options), where callback is called when an item is selected, items is the list of items to select from, and options is a table. The behavior of the options can be found by reading the code for this widget
  • tabbed_window: That's right, a whole window with tabs. Created with tabbedWindow.createWindow(title, tabs, initialTab) where title is the title of the window, tabs is a list of UI elements(ie grids, groups, ect) to put in each tab, and initialTab is the index of the tab that should open initially