mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-27 00:48:32 +02:00
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:
+40
-11
@@ -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)
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user