Files
gen1recomp/tests/engine/launcher_page_scroll.lua
Marcus Pereira cc67be341f Scroll the launcher page so the footer stops covering the panel
On a window too short for the stacked single-column layout -- a phone, or a
narrow desktop window -- the ROM / SAVE FILES / Play / SAVE SLOT stack ran
past the bottom of the window while the footer stayed pinned there and drew
over it. Nothing clipped the panel and nothing scrolled it, so the overflow
was unreachable.

Everything under the tab bar (panel, updater banner, footer) is now one
scrolling column, used only when it is taller than the room below the tab
bar. The strip, logo and tab bar stay pinned, so navigation is always on
screen, and the footer is laid out downward from footerTop right after the
content instead of upward from the window bottom.

- RomImporter.pageScrollFor is the whole decision, pure and covered by
  tests/engine/launcher_page_scroll.lua. A window that grows back drags the
  offset down with it, so the page never stays parked past its own end.
- The panels return their natural height as they draw, so the measurement is
  the previous frame's: the same one-frame settle the slot and mod lists
  already rely on.
- One scroll axis at a time. While the page scrolls, the panels draw paged:
  the slot and mod lists take their natural height, keep no inner scroll
  region and report a max of 0, so wheel, right stick and drag all move the
  page. Two-column layouts do not overflow, paged stays false, and every one
  of these behaves exactly as before.
- inside() and _ptIn() reject a rect that scrolled out of the viewport, so a
  control that slid under the tab bar cannot be clicked through it. Tab chips
  are pinned and exempt. pageScroll resets on a tab change.

Android had no scroll gesture at all: the launcher is handed no move events
(main.lua forwards neither touchmoved nor mousemoved while it is up) and its
mouse emulation was never trusted, which is what "no reliable pointer
polling" referred to. That was survivable while every scroll region was an
inner list, and useless once the page itself scrolls, since a phone is
exactly where it overflows. love.touch is pollable, so _pointerHold reads the
first active touch there and hands _updateSlotDrag the same (held, y) pair
the mouse gives on desktop. Slot rows and mod toggles consequently arm on
press and commit on release on Android too, matching desktop, so a swipe that
starts on a card scrolls instead of selecting it. All of it is gated on
touchPollable: without love.touch every Android path is exactly what it was.

conf.lua also grows minwidth/minheight (480x360) for the desktop window,
under which the cards stop being readable. Mobile is fullscreen and ignores
it.

Verified on the Android emulator (1080x2400) against a build of the parent
commit: before, the footer painted over the SAVE SLOT card with no way to
reach it; after, the page pans by touch and the footer is reachable and
intact.
2026-07-29 12:53:24 -03:00

64 lines
3.2 KiB
Lua

-- Launcher page scroll (src/import/RomImporter.lua): the column under the tab
-- bar -- panel, updater banner, footer -- scrolls as one when the window is too
-- short to hold it. Before this, a stacked single-column layout on a narrow
-- window ran under a footer pinned to the window bottom, and the part below the
-- fold could not be reached at all. The arithmetic is pure, so pin it here
-- rather than in a screenshot.
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local T = require("tests.modkit")
local RomImporter = require("src.import.RomImporter")
local pageScrollFor = RomImporter.pageScrollFor
-- ------------------------------------------------------------- fits: inert
local paged, scroll, maxPage = pageScrollFor(400, 600, 0)
T.eq(paged, false, "a column shorter than the viewport does not scroll")
T.eq(maxPage, 0, "and has nowhere to scroll to")
T.eq(scroll, 0, "and sits at the top")
-- An exact fit is still not a scroll: one pixel of slack would show a thumb
-- for nothing and put the wheel on the page instead of the slot list.
paged, _, maxPage = pageScrollFor(600, 600, 0)
T.eq(paged, false, "a column exactly as tall as the viewport does not scroll")
T.eq(maxPage, 0, "an exact fit has no scroll extent")
-- --------------------------------------------------------- overflows: scrolls
paged, scroll, maxPage = pageScrollFor(900, 600, 0)
T.eq(paged, true, "a column taller than the viewport scrolls")
T.eq(maxPage, 300, "the extent is exactly the overflow")
T.eq(scroll, 0, "a fresh page starts at the top")
-- The bottom of the travel shows the footer: the whole overflow is reachable,
-- which is the point of the change (#footer under the fold).
_, scroll = pageScrollFor(900, 600, 300)
T.eq(scroll, 300, "the offset can reach the end of the column")
_, scroll = pageScrollFor(900, 600, 5000)
T.eq(scroll, 300, "an offset past the end clamps to it")
_, scroll = pageScrollFor(900, 600, -40)
T.eq(scroll, 0, "an offset above the top clamps to it")
-- ------------------------------------------------------- the window grows back
-- Resizing taller has to pull the page back down with it; leaving the offset
-- where it was would park the content above the viewport with no way back.
_, scroll, maxPage = pageScrollFor(900, 800, 300)
T.eq(maxPage, 100, "a taller window leaves less to scroll")
T.eq(scroll, 100, "and drags a deeper offset back to the new end")
paged, scroll = pageScrollFor(900, 900, 300)
T.eq(paged, false, "growing past the content stops the scrolling")
T.eq(scroll, 0, "and returns the page to the top")
-- ---------------------------------------------------------------- degenerate
-- draw() computes the viewport from the window height, so a window smaller than
-- the pinned header hands this a negative number; it must not become extra
-- travel.
_, _, maxPage = pageScrollFor(500, -120, 0)
T.eq(maxPage, 500, "a negative viewport counts as no room, not as more of it")
paged, scroll, maxPage = pageScrollFor(nil, nil, nil)
T.eq(paged, false, "a first frame with nothing measured yet does not scroll")
T.eq(scroll, 0, "and sits at the top")
T.eq(maxPage, 0, "with no extent")
T.finish("launcher page scroll")