Files
SurvivorCore/plugin/FieldRow.luau
T
Samuel LisonandClaude Opus 4.8 bbfefbec38 fix(builder): chooser cards were unclickable and misrouted picks
Found by an adversarial review of the Build page before it was ever run — all
three reviewers independently flagged the first one.

1. The "what is this object?" cards were dead. chooserCard parented a
   Size=fromScale(1,1) TextButton into Theme.panel() intending an overlay, but
   Theme.panel() contains a UIListLayout, which lays out EVERY GuiObject child —
   there is no opt-out, and ZIndex does not affect layout. So the button became
   another list row: clicking a card's title/summary did nothing, and the
   oversized button spilled past the card and took the click for the card BELOW,
   applying the WRONG component (which also strips the previous component's
   attributes). This was the page's primary interaction.
   Fixed with Theme.panelButton() — the card itself is the button, matching the
   pattern OverviewPage already uses.

2. Clear did nothing on an object whose only tag was unknown to this engine,
   yet reported success. The page renders exactly that branch with a Clear
   button. BuildAdmin.clear now takes the tags to remove explicitly, and the page
   passes the unknown tags it just displayed — never removing an unlisted tag
   speculatively, since it may belong to another plugin. It also reports
   "nothing to clear" instead of a false success.

3. FieldRow's help text set Position inside a UIListLayout parent, so its indent
   was silently dropped and every help line rendered flush left. Uses padding now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 17:54:21 +10:00

163 lines
4.9 KiB
Luau

--!nonstrict
--[[
FieldRow the shared schema-field row: [○/●] label [control], plus an optional wrapped help
line underneath. The visual language is the Engine Config editor's (a filled dot means "you set
this"; an empty dot means "following the default"), so every schema-driven editor in the plugin
looks and behaves the same.
Controls by kind: boolean/enum cycle on click; everything else is a TextBox committed on focus
loss, showing the default as its placeholder. When a field declares `ref` (a content category),
the box gains a ⌄ button that cycles the ids authored in that category — so a creator picks
"oak_tree" instead of remembering it.
]]
local Theme = require(script.Parent.Theme)
local Field = require(script.Parent.Field)
local FieldRow = {}
local ROW_H = Theme.ROW_H
-- opts = {
-- spec, -- AttributeSpec
-- state, -- { value, default, hasOverride, typeMismatch? }
-- onCommit(raw) -> (), -- commit an edit (blank = reset)
-- onReset() -> (), -- the ○/● dot
-- choices: { string }?, -- resolved `ref` ids, for the ⌄ picker
-- }
function FieldRow.build(opts: any): Frame
local spec, state = opts.spec, opts.state
local hasHelp = typeof(spec.help) == "string" and spec.help ~= ""
local row = Theme.make("Frame", {
Size = UDim2.fromScale(1, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
}, {
Theme.make("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 1),
}),
}) :: Frame
local top = Theme.make("Frame", {
Size = UDim2.new(1, 0, 0, ROW_H),
BackgroundTransparency = 1,
LayoutOrder = 1,
}) :: Frame
top.Parent = row
-- Override dot: filled + accented when this instance sets the attribute.
local dot = Theme.button({
Size = UDim2.fromOffset(18, 18),
Position = UDim2.fromOffset(0, 4),
Text = if state.hasOverride then "●" else "○",
TextColor3 = if state.hasOverride then Theme.COLOR.ACCENT else Theme.COLOR.DIM,
BackgroundTransparency = 1,
TextSize = 13,
})
dot.Parent = top
dot.MouseButton1Click:Connect(function()
opts.onReset()
end)
local warn = if state.typeMismatch then " ⚠" else ""
Theme.label({
Size = UDim2.new(0.42, -22, 1, 0),
Position = UDim2.fromOffset(22, 0),
Text = spec.label .. warn,
TextColor3 = if state.hasOverride then Theme.COLOR.TEXT else Theme.COLOR.DIM,
TextTruncate = Enum.TextTruncate.AtEnd,
}).Parent =
top
local controlX = UDim2.new(0.42, 4, 0, 3)
local controlW = UDim2.new(0.58, -4, 0, ROW_H - 6)
if spec.kind == "boolean" or spec.kind == "enum" then
local current = Field.format(spec, state.value)
local control = Theme.button({
Size = controlW,
Position = controlX,
Text = if current == "" then "(blank)" else current,
TextColor3 = if state.hasOverride then Theme.COLOR.TEXT else Theme.COLOR.DIM,
TextSize = 13,
})
control.Parent = top
control.MouseButton1Click:Connect(function()
if spec.kind == "boolean" then
opts.onCommit(not (state.value == true))
return
end
local choices = spec.choices or {}
local index = 0
for i, choice in choices do
if choice == current then
index = i
break
end
end
local nextChoice = choices[(index % math.max(1, #choices)) + 1]
if nextChoice ~= nil then
opts.onCommit(nextChoice)
end
end)
else
local pickable = opts.choices and #opts.choices > 0
local box = Theme.textBox({
Size = if pickable then UDim2.new(0.58, -28, 0, ROW_H - 6) else controlW,
Position = controlX,
Text = if state.hasOverride then Field.format(spec, state.value) else "",
PlaceholderText = spec.placeholder or Field.format(spec, spec.default),
})
box.Parent = top
box.FocusLost:Connect(function()
opts.onCommit(box.Text)
end)
if pickable then
-- Cycle the ids authored in the referenced content category.
local pick = Theme.button({
Size = UDim2.fromOffset(22, ROW_H - 6),
Position = UDim2.new(1, -22, 0, 3),
Text = "⌄",
TextSize = 13,
})
pick.Parent = top
pick.MouseButton1Click:Connect(function()
local choices = opts.choices
local current = if state.hasOverride then Field.format(spec, state.value) else ""
local index = 0
for i, choice in choices do
if choice == current then
index = i
break
end
end
opts.onCommit(choices[(index % #choices) + 1])
end)
end
end
if hasHelp then
-- `row` owns a UIListLayout, which overwrites child Position — so the indent under the
-- label column has to come from padding, not Position.
local help = Theme.label({
Size = UDim2.fromScale(1, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Text = spec.help,
TextColor3 = Theme.COLOR.DIM,
TextWrapped = true,
TextSize = 11,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
})
Theme.pad(22, 0, 0, 2).Parent = help
help.Parent = row
end
return row
end
return FieldRow