H3lp Yours3lfDocs
example

UI Recipes

Copyable H3UI compositions for common OpenMW UI surfaces.

These recipes use H3UI's passive builders. Put them in a registered menu or player script; requiring a component does not run a script. Keep application state and the mounted root in your script, and call element:update() when a callback changes what should be displayed. Each content-only root uses ui.TYPE.Container so it sizes itself to its children.

H3UI settings panel

The high-level settings recipe removes the repeated row/label/control assembly while leaving values and redraw ownership in your script:

local ui = require 'openmw.ui'
local I = require 'openmw.interfaces'

local H3UI = I.H3UI
local enabled = true
local intensity = 50
local pageSize = 20
local element

local function build()
    return H3UI.build {
        recipe = 'settings',
        title = 'Settings',
        fields = {
            {
                kind = 'toggle',
                label = 'Enabled',
                value = enabled,
                onChange = function(value)
                    enabled = value
                    element:update()
                end,
            },
            {
                kind = 'slider',
                label = 'Intensity',
                value = intensity,
                min = 0,
                max = 100,
                step = 5,
                onChange = function(value)
                    intensity = value
                    element:update()
                end,
            },
            {
                kind = 'number',
                label = 'Page size',
                value = pageSize,
                min = 5,
                max = 100,
                step = 5,
                integer = true,
                onChange = function(value)
                    pageSize = value
                    element:update()
                end,
            },
        },
    }
end

element = ui.create {
    type = ui.TYPE.Container,
    layer = 'Windows',
    content = ui.content { build() },
}

The recipe does not persist any setting. It simply composes existing H3 controls.

Registered styling

local util = require 'openmw.util'
local H3UI = require('openmw.interfaces').H3UI

H3UI.registerTheme {
    id = 'myMod:danger-demo',
    name = 'danger-demo',
    tokens = {
        color = {
            danger = util.color.rgb(1, 0.25, 0.2),
        },
    },
    rules = {
        {
            selector = {
                component = 'button',
                tone = 'danger',
                slot = 'label',
            },
            style = {
                props = {
                    textColor = H3UI.token('color.danger'),
                },
            },
        },
    },
}

local deleteButton = H3UI.build {
    component = 'button',
    tone = 'danger',
    args = { label = 'Delete' },
}

Registered themes appear in the player's H3UI settings. Scripts do not select a theme; every H3UI scope uses the player's configured appearance. See H3UI for selectors, traits, tokens, classes, style slots, cascade order, and diagnostics.

Settings panel

Compose a small settings surface from a column, labels, toggle, slider, and numeric input. Interactive callbacks update the mounted root after changing their state.

local ui = require 'openmw.ui'
local util = require 'openmw.util'
local column = require 'scripts.s3.components.column'
local numberInput = require 'scripts.s3.components.numberInput'
local row = require 'scripts.s3.components.row'
local slider = require 'scripts.s3.components.slider'
local text = require 'scripts.s3.components.text'
local toggle = require 'scripts.s3.components.toggle'

local enabled = true
local intensity = 50
local pageSize = 20
local element

element = ui.create {
  type = ui.TYPE.Container,
  layer = 'Windows',
  content = ui.content {
    column {
      children = {
        text {
          text = 'Settings',
        },
        row {
          children = {
            text {
              text = 'Enabled',
            },
            toggle {
              value = enabled,
              onChange = function(value)
                enabled = value
                element:update()
              end,
            },
          },
        },
        text {
          text = 'Intensity',
        },
        slider {
          value = intensity,
          min = 0,
          max = 100,
          step = 5,
          props = {
            size = util.vector2(260, 18),
          },
          onChange = function(value)
            intensity = value
            element:update()
          end,
        },
        row {
          children = {
            text {
              text = 'Page size',
            },
            numberInput {
              value = pageSize,
              min = 5,
              max = 100,
              step = 5,
              integer = true,
              onChange = function(value)
                pageSize = value
                element:update()
              end,
            },
          },
        },
      },
    },
  },
}

See toggle, slider, and numberInput.

Inventory-ish grid

grid does not know anything about inventory. Give it item-slot layouts, then use focus events to show a caller-owned tooltip beside the grid.

local async = require 'openmw.async'
local ui = require 'openmw.ui'
local util = require 'openmw.util'
local grid = require 'scripts.s3.components.grid'
local itemSlot = require 'scripts.s3.components.itemSlot'
local text = require 'scripts.s3.components.text'
local tooltip = require 'scripts.s3.components.tooltip'

local iconSize = util.vector2(72, 72)
local red = util.color.rgb(1, 0, 0)
local green = util.color.rgb(0, 1, 0)
local blue = util.color.rgb(0, 0, 1)

local tooltipText = text {
  text = '',
}
local tooltipLayout = tooltip {
  props = {
    position = util.vector2(320, 80),
    visible = false,
  },
  children = {
    tooltipText,
  },
}
local element

local function slot(label, count, color)
  return itemSlot {
    resource = {
      path = 'white',
    },
    count = count,
    iconProps = {
      size = iconSize,
      color = color,
    },
    events = {
      focusGain = async:callback(function()
        tooltipText.props.text = label
        tooltipLayout.props.visible = true
        element:update()
      end),
      focusLoss = async:callback(function()
        tooltipLayout.props.visible = false
        element:update()
      end),
    },
  }
end

element = ui.create {
  type = ui.TYPE.Container,
  layer = 'Windows',
  content = ui.content {
    grid {
      columns = 3,
      items = {
        slot('Restore potion', 3, red),
        slot('Torch', 1, green),
        slot('Lockpick', 12, blue),
      },
    },
    tooltipLayout,
  },
}

For real inventory data, rebuild or update the owner when item data changes. See grid, itemSlot, and tooltip.

Tabbed window

Tabs select a label and report the item; they do not own page content. Keep the page layout beside the tab strip and update it in onSelect.

local ui = require 'openmw.ui'
local column = require 'scripts.s3.components.column'
local tabs = require 'scripts.s3.components.tabs'
local text = require 'scripts.s3.components.text'
local window = require 'scripts.s3.components.window'

local pages = {
  'General',
  'Advanced',
}
local pageText = text {
  text = 'General settings',
}
local element

element = ui.create {
  type = ui.TYPE.Container,
  layer = 'Windows',
  content = ui.content {
    window {
      title = 'Preferences',
      children = {
        column {
          children = {
            tabs {
              items = pages,
              onSelect = function(_, label)
                pageText.props.text = label .. ' settings'
                element:update()
              end,
            },
            pageText,
          },
        },
      },
    },
  },
}

See tabs and window.

Searchable list

Keep filter state outside the components. Replace the root's content and update the existing element when the query changes; do not destroy and recreate the root during the input callback.

local ui = require 'openmw.ui'
local column = require 'scripts.s3.components.column'
local list = require 'scripts.s3.components.list'
local listItem = require 'scripts.s3.components.listItem'
local searchInput = require 'scripts.s3.components.searchInput'

local names = {
  'Almalexia',
  'Baurus',
  'Caius Cosades',
}
local element

local function matches(name, query)
  return query == ''
    or string.find(string.lower(name), string.lower(query), 1, true) ~= nil
end

local function build(query)
  local items = {}
  for _, name in ipairs(names) do
    if matches(name, query) then
      items[#items + 1] = listItem {
        label = name,
      }
    end
  end

  return column {
    children = {
      searchInput {
        value = query,
        onChange = function(nextQuery)
          element.layout.content = ui.content {
            build(nextQuery),
          }
          element:update()
        end,
      },
      list {
        items = items,
      },
    },
  }
end

element = ui.create {
  type = ui.TYPE.Container,
  layer = 'Windows',
  content = ui.content {
    build(''),
  },
}

See searchInput and list.