mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-03 08:07:07 -04:00
229 lines
7.6 KiB
Lua
229 lines
7.6 KiB
Lua
-- Type-to-search item picker: the items panel's answer to the species
|
|
-- picker (panels/SpeciesPicker.lua), and built to the same shape on purpose.
|
|
--
|
|
-- Adding an item used to mean an inline catalog card wedged into the Items
|
|
-- tab: a search field and a scrolling list competing for height with the bag
|
|
-- and PC lists beside it, which on a phone left about three rows visible.
|
|
-- Adding a Pokemon was already a full-screen modal with the whole window to
|
|
-- work with, and there was no reason for the two to differ.
|
|
--
|
|
-- Modal is literal. Kit hit-tests without a z-order, so App.draw raises
|
|
-- Kit.blockClicks over the chrome and the panel while this is open and lowers
|
|
-- it only for this overlay; nothing underneath can take the same tap.
|
|
|
|
local Theme = require("Theme")
|
|
local Ops = require("Ops")
|
|
local PickerChrome = require("PickerChrome")
|
|
local PAL = Theme.PAL
|
|
|
|
local Picker = {}
|
|
|
|
local FIELD_ID = "item-picker"
|
|
local FEEDBACK_SECONDS = 2.5
|
|
|
|
local function feedback(S, Kit, changed, dest)
|
|
local p = S.itemPicker
|
|
if p then
|
|
p.feedback = {
|
|
text = changed and (dest == "pc" and "Added to PC" or "Added to Bag") or "Item not added",
|
|
good = changed,
|
|
at = love.timer and love.timer.getTime and love.timer.getTime() or Kit.time,
|
|
}
|
|
end
|
|
return changed
|
|
end
|
|
|
|
local function drawFeedback(p, Kit, x, y, w, h)
|
|
local toast = p.feedback
|
|
if not toast then return false end
|
|
local elapsed = math.max(0, Kit.time - toast.at)
|
|
if elapsed >= FEEDBACK_SECONDS then
|
|
p.feedback = nil
|
|
return false
|
|
end
|
|
local alpha = math.min(1, (FEEDBACK_SECONDS - elapsed) / 0.4)
|
|
local s, pad = Kit.scale, 8 * Kit.scale
|
|
local icon = 18 * s
|
|
local tw = math.min(w, Kit.textWidth("small", toast.text) + icon + 3 * pad)
|
|
local color = toast.good and PAL.green or PAL.red
|
|
Theme.fillRounded(x, y, tw, h, PAL.cardBody, 0.98 * alpha)
|
|
Theme.stroke(x, y, tw, h, Theme.radius(), color, 0.8 * alpha, 1)
|
|
Kit.icon(toast.good and "check" or "triangle-alert", x + pad, y + (h - icon) / 2, icon, color, alpha)
|
|
Kit.text("small", toast.text, x + icon + 2 * pad, y + (h - Kit.textHeight("small")) / 2, PAL.text, alpha)
|
|
return true
|
|
end
|
|
|
|
function Picker.results(S)
|
|
local p = S.itemPicker
|
|
local hits = Ops.itemSearch(S, p and p.query or "")
|
|
if p and p.dest == "held" then
|
|
local filtered = {}
|
|
for _, id in ipairs(hits) do
|
|
if Ops.itemHoldable(S, id) then
|
|
filtered[#filtered + 1] = id
|
|
end
|
|
end
|
|
return filtered
|
|
end
|
|
return hits
|
|
end
|
|
|
|
-- Enter commits the top match into whichever destination the picker was
|
|
-- opened for, which is the whole point of a search field.
|
|
function Picker.commitFirst(S, Kit)
|
|
local hits = Picker.results(S)
|
|
if not hits[1] then
|
|
return Ops.say(S, "No item matches that")
|
|
end
|
|
return Picker.commit(S, Kit, hits[1])
|
|
end
|
|
|
|
-- One funnel for both destinations. The picker stays OPEN after a commit:
|
|
-- stocking a save means adding several items in a row, and reopening the
|
|
-- modal per item is the kind of friction the inline card at least did not
|
|
-- have. Escape / Close / tap-outside is the way out.
|
|
function Picker.commit(S, Kit, id)
|
|
local p = S.itemPicker
|
|
local dest = (p and p.dest) or "bag"
|
|
if dest == "held" then
|
|
local changed = Ops.setHeldItem(S, S.editingMon, id)
|
|
if changed then
|
|
Ops.closeItemPicker(S, Kit)
|
|
end
|
|
return changed
|
|
end
|
|
if dest == "pc" then
|
|
return feedback(S, Kit, Ops.addToPc(S, id), dest)
|
|
end
|
|
return feedback(S, Kit, Ops.addToBag(S, id), dest)
|
|
end
|
|
|
|
function Picker.draw(S, Kit, width, height)
|
|
local p = S.itemPicker
|
|
if not p then
|
|
return
|
|
end
|
|
local s = Kit.scale
|
|
|
|
-- The click that opened the picker is still the frame's click: the panel
|
|
-- dispatches earlier in App.draw than this overlay does, so without
|
|
-- swallowing it the scrim below would read it as a tap outside and shut the
|
|
-- picker in the same frame it went up.
|
|
if p.opened then
|
|
p.opened = nil
|
|
Kit.blockClicks = true
|
|
end
|
|
|
|
-- the scrim doubles as the "tap outside to cancel" target; it covers the
|
|
-- full window so unsafe bands (notch / home indicator) stay dimmed too
|
|
Theme.col(PAL.bg, 0.82)
|
|
love.graphics.rectangle("fill", 0, 0, width, height)
|
|
|
|
-- Card fills / centres in SafeArea so phones and RGxxx landscapes keep a
|
|
-- usable list (#917 / #715).
|
|
local x, y, w, h, pad = PickerChrome.card(Kit, width, height)
|
|
if Kit.press(0, 0, width, height) and not Kit.hit(x, y, w, h) then
|
|
Ops.closeItemPicker(S, Kit)
|
|
return
|
|
end
|
|
|
|
Kit.card(x, y, w, h)
|
|
local cx, cy = x + pad, y + pad
|
|
local inner = w - 2 * pad
|
|
|
|
local closeW = PickerChrome.closeSize(Kit)
|
|
local captionH = Kit.textHeight("caption")
|
|
local headH = math.max(captionH, closeW)
|
|
if not drawFeedback(p, Kit, cx, cy, inner - closeW - 10 * s, headH) then
|
|
Kit.caption(cx, cy + (headH - captionH) / 2, "ADD AN ITEM")
|
|
end
|
|
if
|
|
Kit.iconButton(
|
|
x + w - pad - closeW,
|
|
cy + (headH - closeW) / 2,
|
|
closeW,
|
|
closeW,
|
|
"x",
|
|
"Close picker"
|
|
)
|
|
then
|
|
Ops.closeItemPicker(S, Kit)
|
|
return
|
|
end
|
|
cy = cy + headH + 10 * s
|
|
|
|
-- Destination toggle. Which list an item lands in is the only real choice
|
|
-- here, so it is a pair of chips at the top rather than two buttons at the
|
|
-- bottom that each mean "commit, and also pick a destination".
|
|
local half = (inner - 8 * s) / 2
|
|
local destH = math.max(PickerChrome.tapMin(Kit), math.floor(30 * s))
|
|
if p.dest == "held" then
|
|
Kit.text("small", "Choose a held item", cx, cy, PAL.caption)
|
|
else
|
|
if Kit.chip(cx, cy, half, destH, "Bag", p.dest ~= "pc", PAL.green, PAL.steel) then
|
|
p.dest = "bag"
|
|
end
|
|
if Kit.chip(cx + half + 8 * s, cy, half, destH, "PC", p.dest == "pc", PAL.green, PAL.steel) then
|
|
p.dest = "pc"
|
|
end
|
|
end
|
|
cy = cy + destH + 10 * s
|
|
|
|
local fieldH = PickerChrome.fieldH(Kit)
|
|
p.query = Kit.textfield(FIELD_ID, cx, cy, inner, fieldH, p.query, "type an item name or id")
|
|
cy = cy + fieldH + 10 * s
|
|
|
|
local hits = Picker.results(S)
|
|
local listH, rowH, rowGap, pagerH = PickerChrome.listMetrics(Kit, y, h, pad, cy)
|
|
local perPage = math.max(1, math.floor((listH + rowGap) / (rowH + rowGap)))
|
|
-- wheel / touch drag scroll the modal list too; the shield is already
|
|
-- lowered for this layer, so Kit.scroll works here and only here
|
|
local drawn, shift = Kit.list(p, "offset", cx, cy, inner, listH, #hits, rowH + rowGap)
|
|
|
|
if #hits == 0 then
|
|
Kit.emptyBox(cx, cy, inner, listH, "Nothing matches that.")
|
|
else
|
|
Kit.pushClip(cx, cy, inner, listH)
|
|
for i = 1, drawn do
|
|
local id = hits[p.offset + i]
|
|
if not id then
|
|
break
|
|
end
|
|
local ry = cy + (i - 1) * (rowH + rowGap) - shift
|
|
if Kit.row(cx, ry, inner, rowH, false, PAL.green, 9 * s) then
|
|
Picker.commit(S, Kit, id)
|
|
end
|
|
-- how many the save already holds, so a second add is an informed one
|
|
local have = (S.save.inventory and S.save.inventory[id]) or (Ops.pcItems(S) or {})[id]
|
|
local tail = have and ("x%d"):format(have) or ""
|
|
local tailW = Kit.textWidth("tiny", tail)
|
|
Kit.text(
|
|
"monoRow",
|
|
Kit.ellipsize(
|
|
"monoRow",
|
|
(S.data.items[id] and S.data.items[id].name) or id,
|
|
inner - tailW - 30 * s
|
|
),
|
|
cx + 10 * s,
|
|
ry + (rowH - Kit.textHeight("monoRow")) / 2,
|
|
PAL.text
|
|
)
|
|
if tail ~= "" then
|
|
Kit.textRight(
|
|
"tiny",
|
|
tail,
|
|
cx + inner - 10 * s,
|
|
ry + (rowH - Kit.textHeight("tiny")) / 2,
|
|
PAL.caption
|
|
)
|
|
end
|
|
end
|
|
Kit.popClip()
|
|
Kit.listScrollbar(p, "offset", cx, cy, inner, listH)
|
|
end
|
|
|
|
p.offset = Kit.pager(cx, y + h - pad - pagerH, inner, p.offset, #hits, perPage)
|
|
end
|
|
|
|
return Picker
|