-- One-column launcher reach (#852) and the safe-area launcher anchor (#810). -- No pokered cite: the launcher is port-only chrome. -- -- #852: minPanelHeight in src/import/LauncherView.lua was a flat 460*s tuned -- for the two-column layout. A one-column window (portrait phone, squat 4:3 -- device) stacks title + actions card + slot card + the pinned -- Play/Reset-rebinds/Touch-Controls block, which needs more room; the flat -- threshold read "tall enough", so the short-window page scroll never -- engaged, buildSlotCard was cut by Kit.pushClip against the pinned block, -- and Kit's clip-bounded hit-testing (src/ui/kit/Kit.lua) left every slot -- row, the pager and "+ New save slot" drawn-but-inert. The fix makes the -- threshold column-aware, so those windows scroll instead of clipping. -- -- The seam is LauncherView.draw itself: it publishes the page-scroll extent -- on the importer (imp._pageScroll / imp._pageScrollMax, the values the -- touch-drag and wheel paths feed), so a headless draw shows whether the -- scroll engaged without reading any file-local constant. The 480x900 -- window below is the discriminator: its natural panel space satisfies the -- old flat threshold (inert, slot card clipped) but not the one-column one. -- -- #810 gets its unit-conversion pin in tests/engine/safe_area_units_test.lua; -- here the complementary end-to-end anchor: Layout.metrics must place the -- launcher at the corrected safe-area origin, not a DPI-inflated band down -- the screen. -- luajit tests/engine/launcher_one_column_reach_bug852.lua package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") local check, eq = T.check, T.eq love = love or require("tests.love_stub") -- The launcher touches two graphics calls the shared stub does not carry -- (focus-ring joins, the footer's BCG invert shader); both are draw-only, so -- inert fills are enough for the layout arithmetic under test. love.graphics.setLineJoin = love.graphics.setLineJoin or function() end love.graphics.newShader = love.graphics.newShader or function() return {} end local Layout = require("src.ui.kit.Layout") local RomImporter = require("src.import.RomImporter") local LauncherView = require("src.import.LauncherView") local function window(w, h) love.graphics.getDimensions = function() return w, h end love.graphics.getPixelDimensions = function() return w, h end end -- A fresh launcher on the Red tab; no cache exists headless, so every -- version sits in its "ROM required" state, which still lays out the full -- one-column pile (title, actions card, slot card, pinned block). local function freshLauncher() return RomImporter.new(function() end, { launcher = true }) end -- ------------------------------------------------ #852: the scroll engages -- 480x900 one column: enough room for the old flat 460*s threshold, not for -- the one-column stack. Before the fix draw() left _pageScrollMax at 0 here -- and the slot card sat clipped inert against the pinned buttons. window(480, 900) local m = Layout.metrics(1200) eq(m.twoCol, false, "480-wide window lays out one column") local imp = freshLauncher() LauncherView.draw(imp) check((imp._pageScrollMax or 0) > 0, "one-column window short of the stack engages the page scroll") eq(imp._pageScroll, 0, "a fresh page starts at the top") -- The wheel moves the page (the same offset the touch drag feeds), and the -- offset clamps to the extent, so the whole stack down to "+ New save slot" -- and the footer is reachable rather than clipped away. local extent = imp._pageScrollMax imp._wheelY = -1 LauncherView.draw(imp) eq(imp._pageScroll, math.min(math.floor(48 * m.s), extent), "one wheel notch scrolls the page down by its step") imp._pageScroll = 1e6 LauncherView.draw(imp) eq(imp._pageScroll, imp._pageScrollMax, "an offset past the end clamps to the extent, so the bottom is reachable") -- The reporter's portrait phone (360x780 units) is shorter still and must -- also scroll; before the fix its slot list was unreachable. window(360, 780) local phone = freshLauncher() LauncherView.draw(phone) check((phone._pageScrollMax or 0) > 0, "portrait-phone one-column window engages the page scroll") -- A one-column window tall enough for the whole stack stays inert: the -- column-aware minimum is a floor, not a permanent scroll. window(480, 1200) local tall = freshLauncher() LauncherView.draw(tall) eq(tall._pageScrollMax, 0, "a tall one-column window does not scroll for nothing") -- --------------------------------------- #810: launcher anchored in units -- Layout.metrics anchors the launcher at SafeArea.rect's origin. Feed it -- the iOS 16 portrait frame that reported the safe rect in framebuffer -- pixels (3x DPI): the launcher must start at the 44-unit notch inset, not -- 132 units down with the top of the window black (the #810 report). The -- rescale itself is pinned in tests/engine/safe_area_units_test.lua. love.graphics.getDimensions = function() return 375, 812 end love.graphics.getPixelDimensions = function() return 1125, 2436 end local oldSafe = love.window.getSafeArea love.window.getSafeArea = function() return 0, 132, 1125, 2232 end local ios = Layout.metrics(1200) eq(ios.top, 44, "launcher anchors at the unit-space notch inset") eq(ios.h, 744, "launcher gets the full unit-space safe height") love.window.getSafeArea = oldSafe T.finish("launcher one-column reach")