A settings panel with components
Status: Implemented (API 1.22). A settings screen built from React-style components: tabs, toggles, a slider and a list the player adds to and removes from, in the player’s Appearance style. The complete mod is
sdk/examples/react_settings_lua.
Reference: Components, view, The UI Kit.
1. Think in components
Split the screen into parts that each own their state:
Settings: the panel with a heading, the tabs and a Done button; it owns the selected tab.General: three options; each is state that is saved to the config.Waypoints: the list and the “add” row; it owns the names and the text being typed.WaypointRow: one name and a remove button; it owns nothing (only props).
A component is a function from props to nodes:
local ui = cl.ui
local WaypointRow = ui.component("WaypointRow", function(p)
return ui.row{ gap = 4, height = 20,
ui.label{ text = p.name, grow = 1 },
ui.icon_button{ icon = "trash", tooltip = "Remove " .. p.name, on_press = p.on_remove },
}
end)
2. State and saving
ui.use_state gives a value and a setter. A setter that gets a new value renders that component again; the same value does nothing. Save with an effect, which runs after the update when its dependencies changed:
local config = cl.config.open("client")
config:define_bool("show_beams", true)
config:define_i32("max_distance", 500, 50, 5000)
local General = ui.component("General", function()
local beams, set_beams = ui.use_state(config:get_bool("show_beams"))
local max, set_max = ui.use_state(config:get_i32("max_distance"))
ui.use_effect(function()
config:set_bool("show_beams", beams)
config:set_i32("max_distance", max)
config:save()
end, { beams, max })
return ui.column{ gap = 6,
ui.toggle{ label = "Show beams", value = beams, on_change = set_beams },
ui.slider{ label = "Hide farther than", min = 50, max = 5000, step = 50, value = max, on_change = set_max },
}
end)
The toggle’s handler gets a boolean and the slider’s a number, so the setters can be the handlers.
3. Lists with keys
Give every row a key. When the list changes, rows with the same key keep their widgets: removing “Home” removes one row and moves the others, it doesn’t rebuild the list.
local Waypoints = ui.component("Waypoints", function()
local list, set_list = ui.use_state({ "Home", "Mine entrance" })
local draft, set_draft = ui.use_state("")
local function remove(name)
local rest = {}
for _, n in ipairs(list) do if n ~= name then rest[#rest + 1] = n end end
set_list(rest) -- a new table: the state changed
end
return ui.column{ gap = 4,
ui.row{ gap = 4, height = 20,
ui.text_field{ hint = "New waypoint", text = draft, on_change = set_draft },
ui.button{ label = "Add", width = 60, disabled = draft == "", on_press = function()
local next = { table.unpack(list) }
next[#next + 1] = draft
set_list(next)
set_draft("")
end },
},
#list == 0 and ui.label{ text = "No waypoints yet." } or false,
ui.map(list, function(name)
return WaypointRow{ key = name, name = name, on_remove = function() remove(name) end }
end),
}
end)
State is compared with ==: change a table by making a new one (as above), not by editing it in place.
4. Put it together and open it
local view
local Settings = ui.component("Settings", function()
local tab, set_tab = ui.use_state(0)
return ui.panel{ padding = 10, gap = 8,
ui.heading{ text = "Waypoint Settings" },
ui.tabs{ tabs = { "General", "Waypoints" }, value = tab, on_change = set_tab },
tab == 0 and General{} or Waypoints{},
ui.button{ label = "Done", on_press = function() view:close() end },
}
end)
local function open_settings()
view = ui.open{ title = "Waypoint Settings", root = Settings, x = 40, y = 30, width = 320 }
end
Switching tabs swaps General for Waypoints at the same place: a different component, so the old one unmounts (its effects clean up) and the new one mounts with fresh state.
5. Check the cost
view:stats() tells what happened: renders, updates, and the widgets created, changed and removed. Clicking a toggle renders one component and changes one widget; a screen nobody touches renders nothing.
local s = view:stats()
print(s.renders, s.created, s.updated, s.removed)
The same in C and C++
C emits the tree in a render callback and keeps state in named slots (cl_view_state_num, cl_view_set_state_num); C++ uses clight_ui.hpp with the same hooks. Both are in Components and the C / C++ course lessons.