fix(save-editor): mobile safe areas, move search picker, and release window titles

Inset save-editor chrome and pickers into SafeArea so Save stays tappable on punch-hole phones (#917), replace move cycling with a ranked search modal that reflows on phones/RGxxx, and omit the engine version from window titles on stamped release builds.
This commit is contained in:
1jamie
2026-08-25 12:43:05 -05:00
parent bbe0f0d9ae
commit b796c30278
13 changed files with 752 additions and 61 deletions
+40 -11
View File
@@ -9,7 +9,11 @@
-- * Edit on a launcher save row (main.lua, embedded = true), Close returns
-- to the launcher with the slot list refreshed
--
-- Vertical rhythm (scaled by Kit's height/768 factor, everything else flexes):
-- Vertical rhythm inside the platform safe area (scaled by Kit's height/768
-- factor, everything else flexes). Background still fills the full window so
-- the notch / home-indicator bands match the field colour; interactive chrome
-- starts at SafeArea.rect() so Save stays reachable on a punch-hole phone
-- (#917). Offsets below are relative to that safe origin:
-- 0 6px tri-colour version rail, identical to the launcher's
-- 6 64px title bar identity, file chip, Save / Reload / Open / Close
-- (104px when the bar reflows to two rows, #715)
@@ -18,6 +22,7 @@
-- -38 38px status bar the last Ops message + the keyboard map
local Data = require("src.core.Data")
local SafeArea = require("src.core.SafeArea")
local TileRenderer = require("src.render.TileRenderer")
local SaveIO = require("SaveIO")
local Catalog = require("Catalog")
@@ -37,6 +42,7 @@ local MapBrowser = require("MapBrowser")
local Dex = require("Dex")
-- chrome, not a tab panel, so deliberately kept out of PANELS below (#541)
local SpeciesPicker = require("SpeciesPicker")
local MovePicker = require("MovePicker")
local ItemPicker = require("ItemPicker")
local App = {}
@@ -772,7 +778,17 @@ function App.draw()
-- tolerates that rather than indexing a torn-down state.
if not S then return end
local width, height = love.graphics.getDimensions()
Kit.layout(width, height)
width = math.max(1, tonumber(width) or 1)
height = math.max(1, tonumber(height) or 1)
-- Usable chrome rect. Background still fills the window so the notch /
-- home-indicator bands stay the field colour; every button (Save first)
-- lives inside the safe area, matching the launcher and Skin Studio (#917).
local ox, oy, sw, sh = SafeArea.rect()
ox = math.max(0, tonumber(ox) or 0)
oy = math.max(0, tonumber(oy) or 0)
sw = math.max(1, tonumber(sw) or width)
sh = math.max(1, tonumber(sh) or height)
Kit.layout(sw, sh)
local s = Kit.scale
local mx, my = love.mouse.getPosition()
@@ -791,6 +807,7 @@ function App.draw()
-- shield goes up before anything dispatches and comes down only for the
-- picker's own layer at the bottom of this function (#541).
Kit.blockClicks = (S.speciesPicker ~= nil) or (S.itemPicker ~= nil)
or (S.movePicker ~= nil)
Theme.field(width, height)
@@ -799,26 +816,29 @@ function App.draw()
-- the window is too narrow for both on one, instead of the buttons and the
-- identity painting through each other (#715). The taller bar simply
-- costs the content column height, which scrolls.
local titleTwoRow = titleNeedsTwoRows(width)
local titleTwoRow = titleNeedsTwoRows(sw)
local titleH = (titleTwoRow and 104 or 64) * s
local tabH = 66 * s
local statusH = 38 * s
Theme.versionRail(0, 0, width, railH)
drawTitleBar(0, railH, width, titleH, titleTwoRow)
drawTabRail(0, railH + titleH, width, tabH)
Theme.versionRail(ox, oy, sw, railH)
drawTitleBar(ox, oy + railH, sw, titleH, titleTwoRow)
drawTabRail(ox, oy + railH + titleH, sw, tabH)
local contentY = railH + titleH + tabH
local contentH = height - contentY - statusH
local contentY = oy + railH + titleH + tabH
local contentH = sh - railH - titleH - tabH - statusH
local panel = PANELS[S.tab]
if panel then
panel.draw(S, Kit, 22 * s, contentY + 20 * s,
width - 44 * s, contentH - 38 * s)
panel.draw(S, Kit, ox + 22 * s, contentY + 20 * s,
sw - 44 * s, contentH - 38 * s)
end
drawStatusBar(0, height - statusH, width, statusH)
drawStatusBar(ox, oy + sh - statusH, sw, statusH)
Kit.blockClicks = false
-- Scrim still covers the full window (including unsafe bands); the card
-- itself is centred in the safe rect so search fields clear the notch.
SpeciesPicker.draw(S, Kit, width, height)
MovePicker.draw(S, Kit, width, height)
ItemPicker.draw(S, Kit, width, height)
Kit.endFrame()
PadInput.draw()
@@ -841,6 +861,15 @@ function App.keypressed(key)
return
end
end
if S.movePicker then
if key == "return" or key == "kpenter" then
MovePicker.commitFirst(S, Kit)
return
elseif key == "escape" then
Ops.closeMovePicker(S, Kit)
return
end
end
if S.speciesPicker then
if key == "return" or key == "kpenter" then
SpeciesPicker.commitFirst(S, Kit)
+6 -1
View File
@@ -11,7 +11,8 @@
-- Hit testing is a plain rect with no z-order, so panels must draw
-- overlapping controls in dispatch order and every target is >= 26px tall
-- (rule 6 of the design spec) -- that sizing is the whole accessibility story
-- here.
-- here. Kit.tapMin() is the px floor after scale (a short handheld sits on
-- the 0.9 scale floor, so bare `26 * s` would slip under 26).
local Theme = require("Theme")
local PAL = Theme.PAL
@@ -25,6 +26,10 @@ Kit.time = 0
Kit.fonts = {}
Kit.scale = 1
function Kit.tapMin()
return math.max(26, math.floor(30 * (Kit.scale or 1)))
end
local G = love and love.graphics or nil
local edits = {} -- queued textinput / backspace since the last frame
local kbField = nil -- id of the field the OS soft keyboard is raised for
+133 -3
View File
@@ -329,6 +329,31 @@ function Ops.speciesSearch(S, query)
for _, id in ipairs(S.cat.species) do
if Ops.speciesMatches(S, id, query) then out[#out + 1] = id end
end
-- Rank hits so a typed prefix ("pika") puts PIKACHU above mid-string
-- noise; empty query keeps the catalog's A-Z order.
if query and tostring(query) ~= "" then
local q = tostring(query):lower()
local function rank(id)
local idLower = id:lower()
local def = S.data.pokemon[id]
local nameLower = def and def.name and tostring(def.name):lower() or ""
if idLower == q or nameLower == q then return 0 end
if idLower:sub(1, #q) == q
or (nameLower ~= "" and nameLower:sub(1, #q) == q) then
return 1
end
if idLower:find(q, 1, true)
or (nameLower ~= "" and nameLower:find(q, 1, true)) then
return 2
end
return 3 -- dex-number hit
end
table.sort(out, function(a, b)
local ra, rb = rank(a), rank(b)
if ra ~= rb then return ra < rb end
return a < b
end)
end
return out
end
@@ -429,6 +454,8 @@ function Ops.setDv(S, mon, key, value)
return Ops.mark(S, ("%s DV %d (HP DV now %d)"):format(key, mon.dvs[key], mon.dvs.hp))
end
-- Kept for tests and any keyboard path; the inspector opens the searchable
-- picker instead of walking the catalog one tap at a time.
function Ops.cycleMove(S, mon, slot)
if not mon or not (S.cat and S.cat.moves and #S.cat.moves > 0) then return false end
local moves = S.cat.moves
@@ -441,14 +468,117 @@ function Ops.cycleMove(S, mon, slot)
end
for step = 1, #moves do
local nextId = moves[((idx + step - 1) % #moves) + 1]
if S.data and S.data.moves and type(S.data.moves[nextId]) == "table" then
MonOps.setMove(S.data, mon, slot, nextId)
return Ops.mark(S, ("Move %d set to %s"):format(slot, nextId))
if Ops.moveUsable(S, nextId) then
return Ops.setMove(S, mon, slot, nextId)
end
end
return false
end
-- A move record is usable when it is a real table with a numeric PP -- the
-- same floor Catalog already uses to keep provenance scalars out of the list.
function Ops.moveUsable(S, id)
local def = id and S.data and S.data.moves and S.data.moves[id]
return type(def) == "table" and type(def.pp) == "number"
end
-- Search predicate behind the move picker's field: id, display name, and type
-- substring-match case-insensitively; power / accuracy match as whole numbers
-- (same plain-text / no-pattern rule as Ops.speciesMatches).
function Ops.moveMatches(S, id, query)
if not query or query == "" then return true end
local q = tostring(query):lower()
if id:lower():find(q, 1, true) then return true end
local def = S.data.moves[id]
if type(def) ~= "table" then return false end
local name = def.name
if name and tostring(name):lower():find(q, 1, true) then return true end
local typ = def.type
if typ and tostring(typ):lower():find(q, 1, true) then return true end
local power = tonumber(def.power)
if power ~= nil and q == tostring(power) then return true end
local accuracy = tonumber(def.accuracy)
return accuracy ~= nil and q == tostring(accuracy)
end
function Ops.moveSearch(S, query)
local out = {}
for _, id in ipairs(S.cat.moves or {}) do
if Ops.moveMatches(S, id, query) then out[#out + 1] = id end
end
-- Rank hits so a typed prefix ("sur") puts SURF above mid-string noise
-- like ACUPRESSURE / FISSURE; empty query keeps the catalog's A-Z order.
if query and tostring(query) ~= "" then
local q = tostring(query):lower()
local function rank(id)
local idLower = id:lower()
local def = S.data.moves[id]
local nameLower = (type(def) == "table" and def.name)
and tostring(def.name):lower() or ""
if idLower == q or nameLower == q then return 0 end
if idLower:sub(1, #q) == q
or (nameLower ~= "" and nameLower:sub(1, #q) == q) then
return 1
end
if idLower:find(q, 1, true)
or (nameLower ~= "" and nameLower:find(q, 1, true)) then
return 2
end
return 3 -- type / power / accuracy hit
end
table.sort(out, function(a, b)
local ra, rb = rank(a), rank(b)
if ra ~= rb then return ra < rb end
return a < b
end)
end
return out
end
-- One funnel for assigning a move (picker commit and cycleMove). Refuses
-- unknown / scalar ids before MonOps asserts, and speaks in the status bar.
function Ops.setMove(S, mon, slot, id)
if not mon then return false end
slot = math.floor(tonumber(slot) or 0)
if slot < 1 or slot > 4 then return false end
local current = mon.moves and mon.moves[slot] and mon.moves[slot].id
if id == current then
return Ops.say(S, ("Move %d is already %s"):format(slot, tostring(id)))
end
if not Ops.moveUsable(S, id) then
return Ops.say(S, ("%s is not a usable move, cannot assign it")
:format(tostring(id)))
end
local ok, err = pcall(MonOps.setMove, S.data, mon, slot, id)
if not ok then
return Ops.say(S, ("Could not set move %d: %s"):format(slot, tostring(err)))
end
return Ops.mark(S, ("Move %d set to %s"):format(slot, id))
end
-- Modal door for the move picker. `slot` is which of the four move rows the
-- inspector opened; the picker writes back through Ops.setMove on commit.
function Ops.openMovePicker(S, Kit, slot)
if not S.editingMon then
return Ops.say(S, "Pick a slot first, then choose a move")
end
slot = math.floor(tonumber(slot) or 0)
if slot < 1 or slot > 4 then
return Ops.say(S, "Move slots are 1 through 4")
end
if not (S.cat and S.cat.moves and #S.cat.moves > 0) then
return Ops.say(S, "No moves in the catalog")
end
S.movePicker = { query = "", offset = 0, opened = true, slot = slot }
if Kit then Kit.focus = "move-picker" end
return true
end
function Ops.closeMovePicker(S, Kit)
S.movePicker = nil
if Kit and Kit.blur then Kit.blur() end
end
function Ops.clearMove(S, mon, slot)
if not (mon and mon.moves and mon.moves[slot]) then
return Ops.say(S, ("Move slot %d is already empty"):format(slot))
+83
View File
@@ -0,0 +1,83 @@
-- Shared modal-picker frame for Species / Move / Item pickers.
--
-- Desktop keeps a centred card capped at 520x560 logical px. Phones and
-- RGxxx handhelds (RG34XXSP 720x480, RG35XX 640x480, Switch 1280x720, tall
-- portrait Androids) need the card to nearly fill SafeArea.rect() so the
-- search field, list, and pager stay usable instead of collapsing under a
-- fixed 32px margin into a short landscape window (#917 / #715).
local SafeArea = require("src.core.SafeArea")
local PickerChrome = {}
-- Minimum tap target (design rule 6). Scale can sit on the 0.9 floor on a
-- short handheld, so a bare `26 * s` would dip under 26px -- clamp in px.
function PickerChrome.tapMin(Kit)
local s = (Kit and Kit.scale) or 1
return math.max(26, math.floor(30 * s))
end
-- Usable card rect inside the platform safe area.
-- Returns x, y, w, h, pad.
function PickerChrome.card(Kit, windowW, windowH)
local s = (Kit and Kit.scale) or 1
windowW = math.max(1, tonumber(windowW) or 1)
windowH = math.max(1, tonumber(windowH) or 1)
local sox, soy, ssw, ssh = SafeArea.rect()
sox = math.max(0, tonumber(sox) or 0)
soy = math.max(0, tonumber(soy) or 0)
ssw = math.max(1, tonumber(ssw) or windowW)
ssh = math.max(1, tonumber(ssh) or windowH)
-- Shrink the gutter on short / narrow safe areas so the card keeps room
-- for caption + search + at least one list row + pager.
local gutter = math.floor(16 * s)
local minSide = math.min(ssw, ssh)
if minSide < 560 * s then
gutter = math.max(4, math.floor(minSide * 0.03))
end
local maxW = math.floor(520 * s)
local maxH = math.floor(560 * s)
local w = math.min(ssw - 2 * gutter, maxW)
local h = math.min(ssh - 2 * gutter, maxH)
-- Short landscapes (720x480 class): fill the safe height instead of
-- leaving letterbox bands inside an already-short rect.
if ssh <= maxH + 2 * gutter then
h = ssh - 2 * gutter
end
if ssw <= maxW + 2 * gutter then
w = ssw - 2 * gutter
end
w = math.max(1, w)
h = math.max(1, h)
local x = sox + (ssw - w) / 2
local y = soy + (ssh - h) / 2
local pad = math.max(8, math.floor(math.min(18 * s, w * 0.04, h * 0.04)))
return x, y, w, h, pad
end
-- List-body metrics once caption / field / optional extra chrome are placed.
-- `contentTop` is the y just below that chrome; returns listH, rowH, rowGap,
-- pagerH sized so taps stay >= tapMin and listH never goes negative.
function PickerChrome.listMetrics(Kit, cardY, cardH, pad, contentTop)
local s = (Kit and Kit.scale) or 1
local tap = PickerChrome.tapMin(Kit)
local pagerH = math.max(tap, math.floor(30 * s))
local rowH = math.max(tap, math.floor(40 * s))
local rowGap = math.max(4, math.floor(6 * s))
local listBottom = cardY + cardH - pad - pagerH - math.max(6, math.floor(10 * s))
local listH = math.max(0, listBottom - contentTop)
return listH, rowH, rowGap, pagerH
end
function PickerChrome.fieldH(Kit)
return math.max(PickerChrome.tapMin(Kit), math.floor(34 * ((Kit and Kit.scale) or 1)))
end
function PickerChrome.closeSize(Kit)
return math.max(PickerChrome.tapMin(Kit), math.floor(30 * ((Kit and Kit.scale) or 1)))
end
return PickerChrome
+5
View File
@@ -53,6 +53,11 @@ function State.new()
-- without a z-order (#541).
speciesPicker = nil,
-- move picker overlay: nil when closed, otherwise
-- { query, offset, slot = 1..4 }. Same modal contract as speciesPicker;
-- the inspector opens it instead of cycling the catalog one tap at a time.
movePicker = nil,
-- item picker overlay: nil when closed, otherwise
-- { query, offset, dest = "bag"|"pc" }. Same modal contract as
-- speciesPicker above -- adding an item is now a full-screen picker
+15 -16
View File
@@ -13,6 +13,7 @@
local Theme = require("Theme")
local Ops = require("Ops")
local PickerChrome = require("PickerChrome")
local PAL = Theme.PAL
local Picker = {}
@@ -57,38 +58,39 @@ function Picker.draw(S, Kit, width, height)
Kit.blockClicks = true
end
-- the scrim doubles as the "tap outside to cancel" target
-- 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)
local w = math.min(width - 32 * s, 520 * s)
local h = math.min(height - 32 * s, 560 * s)
local x = (width - w) / 2
local y = (height - h) / 2
-- 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 pad = 18 * s
local cx, cy = x + pad, y + pad
local inner = w - 2 * pad
Kit.caption(cx, cy, "ADD AN ITEM")
local closeW = 30 * s
if Kit.button(x + w - pad - closeW, cy - 4 * s, closeW, 26 * s, "x",
local closeW = PickerChrome.closeSize(Kit)
local captionH = Kit.textHeight("caption")
local headH = math.max(captionH, closeW)
Kit.caption(cx, cy + (headH - captionH) / 2, "ADD AN ITEM")
if Kit.button(x + w - pad - closeW, cy + (headH - closeW) / 2, closeW, closeW, "x",
{ font = "small" }) then
Ops.closeItemPicker(S, Kit)
return
end
cy = cy + Kit.textHeight("caption") + 10 * s
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 = 30 * s
local destH = math.max(PickerChrome.tapMin(Kit), math.floor(30 * s))
if Kit.chip(cx, cy, half, destH, "-> BAG", p.dest ~= "pc", PAL.green, PAL.steel) then
p.dest = "bag"
end
@@ -98,16 +100,13 @@ function Picker.draw(S, Kit, width, height)
end
cy = cy + destH + 10 * s
local fieldH = 34 * s
local fieldH = PickerChrome.fieldH(Kit)
p.query = Kit.textfield(FIELD_ID, cx, cy, inner, fieldH, p.query,
"type an item id")
cy = cy + fieldH + 10 * s
local hits = Picker.results(S)
local rowH = 36 * s
local rowGap = 6 * s
local pagerH = 30 * s
local listH = (y + h - pad - pagerH - 10 * s) - cy
local listH, rowH, rowGap, pagerH = PickerChrome.listMetrics(Kit, y, h, pad, cy)
local perPage = math.max(1, math.floor((listH + rowGap) / (rowH + rowGap)))
p.offset = Theme.clamp(p.offset or 0, 0, math.max(0, #hits - perPage))
-- wheel / touch drag scroll the modal list too; the shield is already
+7 -7
View File
@@ -213,7 +213,7 @@ local function drawMoveRows(S, Kit, mon, rightX, rowY, colW, rowH, rowGap)
local ry = rowY + (slot - 1) * (rowH + rowGap)
Theme.row(rightX, ry, colW, rowH, 10 * s, 0.6)
local mv = mon.moves and mon.moves[slot]
local clear = 24 * s
local clear = Kit.tapMin()
local clearX = rightX + colW - 10 * s - clear
local ppText = mv and ("PP %d"):format(mv.pp or 0) or ""
local ppW = Kit.textWidth("tiny", ppText)
@@ -226,9 +226,9 @@ local function drawMoveRows(S, Kit, mon, rightX, rowY, colW, rowH, rowGap)
mv and PAL.text or PAL.faint)
Kit.textRight("tiny", ppText, clearX - 10 * s,
ry + (rowH - Kit.textHeight("tiny")) / 2, PAL.caption)
-- the row body cycles, the x empties: two targets, no modal picker
-- the row body opens the searchable picker; the x empties the slot
if Kit.press(rightX, ry, clearX - rightX - 4 * s, rowH) then
Ops.cycleMove(S, mon, slot)
Ops.openMovePicker(S, Kit, slot)
end
if Kit.button(clearX, ry + (rowH - clear) / 2, clear, clear, "x",
{ kind = "danger", font = "tiny", radius = 6 * s }) then
@@ -264,10 +264,10 @@ function MonEditor.draw(S, Kit, x, y, w, h)
-- text keeps room.
local narrow = inner < 470 * s
local sprite = (narrow and 64 or 96) * s
local rowH = 30 * s
local rowH = math.max(Kit.tapMin(), 30 * s)
local rowGap = 8 * s
local cellH = 52 * s
local actH = 34 * s
local actH = math.max(Kit.tapMin(), 34 * s)
local hw = inner - sprite - 18 * s
local levelInHeader = hw >= levelRowWidth(Kit, mon)
@@ -449,7 +449,7 @@ function MonEditor.draw(S, Kit, x, y, w, h)
local movesY = rowY + colRowsH + 14 * s
Kit.caption(cx, movesY, "MOVES")
Kit.textRight("tiny", "click a slot to cycle", cx + inner, movesY, PAL.caption)
Kit.textRight("tiny", "click a slot to search", cx + inner, movesY, PAL.caption)
local mRowY = movesY + capH + 10 * s
drawMoveRows(S, Kit, mon, cx, mRowY, inner, rowH, rowGap)
@@ -472,7 +472,7 @@ function MonEditor.draw(S, Kit, x, y, w, h)
Kit.textRight("tiny", ("HP DV auto-derived . %d"):format(mon.dvs.hp or 0),
cx + colW, colY, PAL.caption)
Kit.caption(rightX, colY, "MOVES")
Kit.textRight("tiny", "click a slot to cycle", rightX + colW, colY, PAL.caption)
Kit.textRight("tiny", "click a slot to search", rightX + colW, colY, PAL.caption)
local rowY = colY + capH + 10 * s
drawDvRows(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
+136
View File
@@ -0,0 +1,136 @@
-- Type-to-search move picker. The inspector used to change moves by cycling
-- the catalog one tap at a time -- 165 taps to walk Gen1 -- which is the same
-- class of friction the species arrows had before #541. This is the
-- replacement: one modal list, filtered as you type, committing through
-- Ops.setMove so an unusable / scalar id refuses instead of asserting.
--
-- 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 = "move-picker"
function Picker.results(S)
local p = S.movePicker
return Ops.moveSearch(S, p and p.query or "")
end
local function commit(S, id)
local p = S.movePicker
if not p then return false end
return Ops.setMove(S, S.editingMon, p.slot, id)
end
-- Enter commits the top match, 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 move matches that") end
local ok = commit(S, hits[1])
if ok then Ops.closeMovePicker(S, Kit) end
return ok
end
function Picker.draw(S, Kit, width, height)
local p = S.movePicker
if not p then return end
local s = Kit.scale
-- The click that opened the picker is still the frame's click: the
-- inspector 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. App re-raises the shield
-- at the top of the next frame, so leaving it up here is safe.
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.bgBot, 0.72)
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.closeMovePicker(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)
Kit.caption(cx, cy + (headH - captionH) / 2, ("CHOOSE MOVE %d"):format(p.slot or 1))
if Kit.button(x + w - pad - closeW, cy + (headH - closeW) / 2, closeW, closeW, "x",
{ font = "small", radius = 7 * s }) then
Ops.closeMovePicker(S, Kit)
return
end
cy = cy + headH + 10 * s
local fieldH = PickerChrome.fieldH(Kit)
p.query = Kit.textfield(FIELD_ID, cx, cy, inner, fieldH, p.query,
"type a name, an id, or a type")
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)))
p.offset = Theme.clamp(p.offset or 0, 0, math.max(0, #hits - perPage))
-- wheel / touch drag scroll the modal list too; the shield is already
-- lowered for this layer, so Kit.scroll works here and only here (#715)
p.offset = Kit.scroll(cx, cy, inner, listH, p.offset, #hits, perPage)
local mon = S.editingMon
local currentId = mon and mon.moves and mon.moves[p.slot]
and mon.moves[p.slot].id
if #hits == 0 then
Kit.emptyBox(cx, cy, inner, listH, "Nothing matches that.")
else
Kit.pushClip(cx, cy, inner, listH)
for i = 1, perPage do
local id = hits[p.offset + i]
if not id then break end
local ry = cy + (i - 1) * (rowH + rowGap)
local def = S.data.moves[id]
local usable = Ops.moveUsable(S, id)
local current = currentId == id
if Kit.row(cx, ry, inner, rowH, current, PAL.green, 9 * s) then
if commit(S, id) then
Ops.closeMovePicker(S, Kit)
Kit.popClip()
return
end
end
local typ = usable and tostring(def and def.type or "?") or "no data"
local pp = usable and ("PP %d"):format(tonumber(def and def.pp) or 0) or ""
local tail = pp ~= "" and (typ .. " " .. pp) or typ
local tailW = Kit.textWidth("tiny", tail)
Kit.text("monoRow",
Kit.ellipsize("monoRow", id, inner - tailW - 28 * s),
cx + 12 * s, ry + (rowH - Kit.textHeight("monoRow")) / 2,
usable and PAL.text or PAL.faint)
Kit.textRight("tiny", tail, cx + inner - 10 * s,
ry + (rowH - Kit.textHeight("tiny")) / 2, PAL.caption)
end
Kit.popClip()
Kit.scrollbar(cx, cy, inner, listH, p.offset, #hits, perPage)
end
p.offset = Kit.pager(cx, y + h - pad - pagerH, inner, p.offset, #hits, perPage)
end
return Picker
+14 -15
View File
@@ -12,6 +12,7 @@
local Theme = require("Theme")
local Ops = require("Ops")
local MonEditor = require("MonEditor")
local PickerChrome = require("PickerChrome")
local PAL = Theme.PAL
local Picker = {}
@@ -59,44 +60,42 @@ function Picker.draw(S, Kit, width, height)
Kit.blockClicks = true
end
-- the scrim doubles as the "tap outside to cancel" target
-- 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.bgBot, 0.72)
love.graphics.rectangle("fill", 0, 0, width, height)
local w = math.min(width - 32 * s, 520 * s)
local h = math.min(height - 32 * s, 560 * s)
local x = (width - w) / 2
local y = (height - h) / 2
-- 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.closeSpeciesPicker(S, Kit)
return
end
Kit.card(x, y, w, h)
local pad = 18 * s
local cx, cy = x + pad, y + pad
local inner = w - 2 * pad
Kit.caption(cx, cy, p.mode == "box-add"
local closeW = PickerChrome.closeSize(Kit)
local captionH = Kit.textHeight("caption")
local headH = math.max(captionH, closeW)
Kit.caption(cx, cy + (headH - captionH) / 2, p.mode == "box-add"
and ("ADD TO BOX %d"):format(S.selectedBox or 1) or "CHOOSE A SPECIES")
local closeW = 30 * s
if Kit.button(x + w - pad - closeW, cy - 4 * s, closeW, 26 * s, "x",
if Kit.button(x + w - pad - closeW, cy + (headH - closeW) / 2, closeW, closeW, "x",
{ font = "small", radius = 7 * s }) then
Ops.closeSpeciesPicker(S, Kit)
return
end
cy = cy + Kit.textHeight("caption") + 10 * s
cy = cy + headH + 10 * s
local fieldH = 34 * s
local fieldH = PickerChrome.fieldH(Kit)
p.query = Kit.textfield(FIELD_ID, cx, cy, inner, fieldH, p.query,
"type a name, an id, or a dex number")
cy = cy + fieldH + 10 * s
local hits = Picker.results(S)
local rowH = 40 * s
local rowGap = 6 * s
local pagerH = 30 * s
local listH = (y + h - pad - pagerH - 10 * s) - cy
local listH, rowH, rowGap, pagerH = PickerChrome.listMetrics(Kit, y, h, pad, cy)
local perPage = math.max(1, math.floor((listH + rowGap) / (rowH + rowGap)))
p.offset = Theme.clamp(p.offset or 0, 0, math.max(0, #hits - perPage))
-- wheel / touch drag scroll the modal list too; the shield is already