Files
gen1recomp/tools/save-editor/Chooser.lua
T
2026-10-02 06:21:06 -04:00

226 lines
6.0 KiB
Lua

-- Launcher-style navigation: one dropdown opens a modal list over the page.
-- The overlay owns clicks, scrolling and dismissal until an option is chosen.
local Theme = require("Theme")
local SafeArea = require("src.core.SafeArea")
local Motion = require("Motion")
local Kit = require("Kit")
local PAL = Theme.PAL
local Chooser = {}
local function id(option)
return option.id or option[1]
end
local function label(option)
local text = option.label or option[2]
return (option.errors or 0) > 0 and (text .. " (" .. option.errors .. ")") or text
end
function Chooser.close(S)
S.navPopup = nil
Kit.blur()
Kit._drag = nil
Kit.mouseClicked, Kit.wheelY = false, 0
end
function Chooser.open(S, key, title, options, after)
if Motion.active() then
return false
end
local index = 1
for i, option in ipairs(options) do
if id(option) == S[key] then
index = i
end
end
Kit.blur()
Kit._drag = nil
S.navPopup = {
key = key,
title = title,
options = options,
index = index,
scroll = 0,
reveal = true,
opened = true,
after = after,
}
Kit.blockClicks = true
return true
end
function Chooser.navigation(S, kit, key, title, options, x, y, w, h, after)
local current = options[1]
for _, option in ipairs(options) do
if id(option) == S[key] then
current = option
end
end
local text = label(current)
local icon = current.icon or kit.navigationIcon(text)
-- Compact toolbars keep a readable current label beside the dropdown icon.
if w < 160 * kit.scale then
icon = nil
end
if
kit.button(x, y, w, h, text, {
id = "navigate-" .. key,
invalid = (current.errors or 0) > 0,
face = "invert",
font = "small",
icon = icon,
align = "left",
trailingIcon = S.navPopup and S.navPopup.key == key and "chevron-up" or "chevron-down",
})
then
return Chooser.open(S, key, title, options, after)
end
return false
end
function Chooser.actions(S, kit, key, title, options, x, y, w, h)
if
kit.button(x, y, w, h, title, {
id = "navigate-" .. key,
face = "invert",
font = "small",
icon = "sliders-horizontal",
trailingIcon = "chevron-down",
})
then
if Chooser.open(S, key, title, options) then
S.navPopup.mode = "actions"
return true
end
end
return false
end
local function active(S, popup, option)
if popup.mode == "actions" then
return option.active == true
end
return S[popup.key] == id(option)
end
local function choose(S, index)
local popup = S.navPopup
if not popup then
return
end
local option = popup.options[index]
if not option then
return
end
if popup.mode == "actions" then
Chooser.close(S)
option.fn(S)
return
end
local value, previous = id(option), S[popup.key]
local oldIndex = 1
for i, entry in ipairs(popup.options) do
if id(entry) == previous then
oldIndex = i
end
end
Chooser.close(S)
if Motion.change(S, popup.key, value, index >= oldIndex and 1 or -1) and popup.after then
popup.after(value, index)
end
end
function Chooser.keypressed(S, key)
local popup = S.navPopup
if not popup then
return false
end
if key == "escape" then
Chooser.close(S)
elseif key == "return" or key == "kpenter" then
choose(S, popup.index)
elseif key == "up" or key == "down" or key == "home" or key == "end" then
if key == "home" then
popup.index = 1
elseif key == "end" then
popup.index = #popup.options
else
popup.index = (popup.index - 1 + (key == "up" and -1 or 1)) % #popup.options + 1
end
popup.reveal = true
popup.keyboard = true
end
return true
end
function Chooser.draw(S, kit, width, height)
local popup = S.navPopup
if not popup then
return
end
kit.resetClip()
kit.blockClicks = popup.opened == true
popup.opened = nil
local s, row, gap = kit.scale, math.ceil(kit.controlH()), math.floor(8 * kit.scale)
local ox, oy, sw, sh = SafeArea.rect()
local gutter, pad = math.max(8, math.floor(12 * s)), math.floor(14 * s)
local w = math.floor(math.min(360 * s, sw - 2 * gutter))
local listH = #popup.options * (row + gap) - gap
local h = math.min(sh - 2 * gutter, 2 * pad + row + gap + listH)
local x, y = math.floor(ox + (sw - w) / 2), math.floor(oy + (sh - h) / 2)
popup.rect = { x = x, y = y, w = w, h = h }
Theme.col(PAL.bg, 0.72)
love.graphics.rectangle("fill", 0, 0, width, height)
if kit.press(0, 0, width, height) and not kit.hit(x, y, w, h) then
Chooser.close(S)
return
end
kit.card(x, y, w, h)
local cx, cy, inner = x + pad, y + pad, w - 2 * pad
kit.text(
"button",
kit.ellipsize("button", popup.title, inner - row - gap),
cx,
cy + (row - kit.textHeight("button")) / 2,
PAL.heading
)
if kit.iconButton(cx + inner - row, cy, row, row, "x", "Close chooser") then
Chooser.close(S)
return
end
cy = cy + row + gap
local bodyH = math.max(0, h - 2 * pad - row - gap)
local extent = math.max(0, listH - bodyH)
if popup.reveal then
local top = (popup.index - 1) * (row + gap)
popup.scroll = Theme.clamp(math.max(top + row - bodyH, math.min(popup.scroll, top)), 0, extent)
popup.reveal = nil
end
popup.scroll = kit.scrollPixels(cx, cy, inner, bodyH, popup.scroll, listH)
local contentW = inner - (extent > 0 and 8 * s or 0)
kit.pushClip(cx, cy, inner, bodyH)
for i, option in ipairs(popup.options) do
local text = label(option)
if
kit.button(cx, cy + (i - 1) * (row + gap) - popup.scroll, contentW, row, text, {
face = "selection",
invalid = (option.errors or 0) > 0,
active = active(S, popup, option),
font = "small",
align = "left",
icon = option.icon or kit.navigationIcon(text),
trailingIcon = active(S, popup, option) and "check" or nil,
ring = popup.keyboard and popup.index == i or nil,
})
then
kit.popClip()
choose(S, i)
return
end
end
kit.popClip()
kit.scrollbar(cx, cy, inner, bodyH, popup.scroll, listH, bodyH)
end
return Chooser