diff --git a/docs/skin-studio.md b/docs/skin-studio.md index 27218336..068ae316 100644 --- a/docs/skin-studio.md +++ b/docs/skin-studio.md @@ -26,7 +26,7 @@ as-is. Supported keys: | `overlayN_overlay` | bezel image | | `overlayN_full_screen` | stretch the page to the window | | `overlayN_rect` | page placement, default `0,0,1,1` | -| `overlayN_aspect_ratio` | fallback aspect when not full screen | +| `overlayN_aspect_ratio` | design aspect; the overlay letterboxes to it even when full screen | | `overlayN_range_mod`, `overlayN_alpha_mod` | desc defaults | | `overlayN_viewport` | `x,y,w,h`, the screen cutout | | `overlayN_viewport_fill` | parsed; the engine always fits, see below | @@ -162,6 +162,15 @@ coordinate its art was drawn at. Bind, hitbox shape, hit reach and idle and pressed images are per control; the bezel, the pages and the screen cutout are per page. The cutout is itself a draggable element with a 10:9 lock. +Each page can **Lock** to portrait or landscape. With **Match canvas** on +(the default), Next page picks a matching mock device and the canvas preset +picks a matching page. Turn Match canvas off to look at a portrait page on a +landscape device. + +A RetroArch overlay whose pages are already named portrait / landscape +(the auto-rotate convention) locks those pages and turns Match canvas on +when you open it. You do not have to click Lock first. + **Art.** The **Bezel**, **Idle art** and **Pressed art** rows cycle through the images already in the skin folder; the **Import** button beside each one opens the host file picker (`src/core/FilePicker.lua`: osascript, PowerShell, diff --git a/src/core/TouchSkin.lua b/src/core/TouchSkin.lua index db1db502..64eddfae 100644 --- a/src/core/TouchSkin.lua +++ b/src/core/TouchSkin.lua @@ -142,7 +142,11 @@ function TouchSkin.parse(text) controls = {}, } if page.imagePath == "" then page.imagePath = nil end - if not page.aspect or page.aspect <= 0 then + -- An explicit aspect_ratio is the overlay's design aspect. RetroArch + -- letterboxes to it even when full_screen is set, so range_x/range_y + -- that were authored as a circle stay a circle. #1503 + page.aspectFromCfg = page.aspect ~= nil and page.aspect > 0 + if not page.aspectFromCfg then page.aspect = page.name:lower():find("portrait", 1, true) and PORTRAIT_ASPECT or DEFAULT_ASPECT end @@ -170,6 +174,12 @@ function TouchSkin.parse(text) pages[#pages + 1] = page end + -- RetroArch auto-rotate: overlay names containing portrait / landscape + -- are the lock. Stamp it so the studio does not need a second click. #1503 + for _, page in ipairs(pages) do + if not page.orient then page.orient = TouchSkin.pageOrient(page) end + end + return { pages = pages } end @@ -255,6 +265,9 @@ function TouchSkin.parseNative(text) rangeMod = num(raw.rangeMod, 1), alphaMod = num(raw.alphaMod, 1), aspect = num(raw.aspect, DEFAULT_ASPECT), + aspectFromCfg = raw.fitAspect == true, + orient = (raw.orient == "portrait" or raw.orient == "landscape" + or raw.orient == "any") and raw.orient or nil, rect = { x = 0, y = 0, w = 1, h = 1 }, controls = {}, } @@ -287,6 +300,7 @@ function TouchSkin.parseNative(text) nextTarget = c.nextTarget, } end + if not page.orient then page.orient = TouchSkin.pageOrient(page) end pages[#pages + 1] = page end return { pages = pages, name = data.name, author = data.author, @@ -309,6 +323,9 @@ function TouchSkin.toNative(skin) rangeMod = page.rangeMod, alphaMod = page.alphaMod, aspect = page.aspect, + fitAspect = page.aspectFromCfg or nil, + orient = (page.orient == "portrait" or page.orient == "landscape" + or page.orient == "any") and page.orient or nil, controls = {}, } if page.rect and (page.rect.x ~= 0 or page.rect.y ~= 0 @@ -704,6 +721,10 @@ end TouchSkin.active = nil TouchSkin.pageIndex = 1 +-- RetroArch Auto-Rotate Overlay (1.7.9, default on mobile): a cfg whose +-- pages are named portrait / landscape is swapped to match the display. +-- The Skin Studio turns this off so PAGE and the canvas preset stay independent. +TouchSkin.autoOrient = true TouchSkin.surfaceRect = nil @@ -732,9 +753,68 @@ function TouchSkin.select(id) return TouchSkin.setActive(skin) end +local function displaySize() + local r = TouchSkin.surfaceRect + if r and r.w and r.h and r.w > 0 and r.h > 0 then return r.w, r.h end + if love and love.graphics and love.graphics.getDimensions then + return love.graphics.getDimensions() + end + return 0, 0 +end + +-- Explicit lock (studio) wins; otherwise the page name, the RetroArch +-- auto-rotate convention. "any" means unlocked even if the name says +-- portrait or landscape. +function TouchSkin.pageOrient(page) + if not page then return nil end + if page.orient == "any" then return nil end + if page.orient == "portrait" or page.orient == "landscape" then + return page.orient + end + local n = tostring(page.name or ""):lower() + if n:find("landscape", 1, true) then return "landscape" end + if n:find("portrait", 1, true) then return "portrait" end + return nil +end + +function TouchSkin.hasOrientPair(skin) + local saw = {} + for _, page in ipairs(skin and skin.pages or {}) do + local o = TouchSkin.pageOrient(page) + if o then saw[o] = true end + end + return saw.portrait == true and saw.landscape == true +end + +local function findOrientPage(skin, keyword) + for i, page in ipairs(skin.pages or {}) do + if TouchSkin.pageOrient(page) == keyword then return i end + end + return nil +end + +-- If the current page is the wrong orientation of a portrait/landscape pair, +-- jump to the matching one. Pages locked to neither (gb_anim's GameBoy / +-- GameBoyColor) are left alone. #1503 +function TouchSkin.syncOrientation(w, h) + if not TouchSkin.autoOrient then return end + local skin = TouchSkin.active + if not skin or not w or not h or w <= 0 or h <= 0 then return end + local want = w > h and "landscape" or "portrait" + local unwant = w > h and "portrait" or "landscape" + local page = skin.pages[TouchSkin.pageIndex] or skin.pages[1] + local current = TouchSkin.pageOrient(page) + if current == want then return end + if current ~= unwant then return end + local idx = findOrientPage(skin, want) + if idx then TouchSkin.pageIndex = idx end +end + function TouchSkin.page() local skin = TouchSkin.active if not skin then return nil end + local w, h = displaySize() + TouchSkin.syncOrientation(w, h) return skin.pages[TouchSkin.pageIndex] or skin.pages[1] end @@ -766,7 +846,12 @@ function TouchSkin.pageBox(page, w, h, ox, oy) ox, oy = ox or 0, oy or 0 if not page then return ox, oy, w, h end local bx, by, bw, bh = ox, oy, w, h - if not page.fullScreen and h > 0 then + -- full_screen means "relative to the window, not the game viewport". + -- When the cfg also names an aspect_ratio, that window is then fitted + -- to the overlay's design aspect so buttons do not stretch. #1503 + local fit = ((not page.fullScreen) or page.aspectFromCfg) + and page.aspect and page.aspect > 0 and h > 0 + if fit then local displayAspect = w / h if displayAspect > page.aspect then bw = h * page.aspect @@ -833,8 +918,9 @@ function TouchSkin.viewport(w, h, ox, oy) local page = TouchSkin.page() if not page or not page.viewport or not TouchSkin.drawable() then return nil end local v = page.viewport - local x, y = (ox or 0) + v.x * w, (oy or 0) + v.y * h - local vw, vh = v.w * w, v.h * h + local bx, by, bw, bh = TouchSkin.pageBox(page, w, h, ox, oy) + local x, y = bx + v.x * bw, by + v.y * bh + local vw, vh = v.w * bw, v.h * bh if vw <= 0 or vh <= 0 then return nil end return x, y, vw, vh, page.viewportFill == true, page.viewportExpand == true end diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index 81b15feb..19260433 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -4324,25 +4324,12 @@ function RomImporter:_findRows() category = self.findCategory, }) if self.modScope then + local ModTargets = require("src.mods.ModTargets") local gen = GameVersion.generation(self.modScope) local kept = {} for _, entry in ipairs(rows) do - local has1, has2 = false, false - local function note(s) - s = tostring(s or ""):lower() - if s == "gen1" or s == "gen 1" or s == "red" or s == "blue" - or s == "yellow" then - has1 = true - end - if s == "gen2" or s == "gen 2" or s == "gold" then - has2 = true - end - end - for _, cat in ipairs(entry.categories or {}) do note(cat) end - for _, tag in ipairs(entry.tags or {}) do note(tag) end - if (not has1 and not has2) - or (gen == 2 and has2) - or (gen ~= 2 and has1) then + local versions = ModTargets.normalize(entry.games) + if #versions == 0 or ModTargets.covers(versions, gen) then kept[#kept + 1] = entry end end diff --git a/src/mods/ModIndex.lua b/src/mods/ModIndex.lua index f170d920..2cd39f06 100644 --- a/src/mods/ModIndex.lua +++ b/src/mods/ModIndex.lua @@ -160,6 +160,7 @@ local function parseEntry(raw) summary = str(raw.summary) or "", categories = strArray(raw.categories), tags = strArray(raw.tags), + games = strArray(raw.games), license = str(raw.license), repo = str(raw.repo), github = str(raw.github), diff --git a/src/ui/SkinStudio.lua b/src/ui/SkinStudio.lua index 936ada14..4233906f 100644 --- a/src/ui/SkinStudio.lua +++ b/src/ui/SkinStudio.lua @@ -20,6 +20,17 @@ Studio.CANVASES = { lockViewport = { x = 48 / 256, y = 40 / 224, w = 160 / 256, h = 144 / 224 } }, } +-- Per-page lock, and whether the canvas preset follows it. Match is on +-- by default so a portrait/landscape overlay pair does not need two +-- separate clicks to preview the right way up. #1503 +Studio.matchOrient = true +Studio.ORIENT_CYCLE = { "any", "portrait", "landscape" } +Studio.ORIENT_LABEL = { + any = "Lock: Off", + portrait = "Lock: Portrait", + landscape = "Lock: Landscape", +} + local HANDLE = 7 local HANDLES = { { "nw", 0, 0 }, { "n", 0.5, 0 }, { "ne", 1, 0 }, @@ -64,9 +75,91 @@ local function syncActive() TouchSkin.pageIndex = Studio.pageIndex end -function Studio.setCanvas(index) +local function canvasOrientation(canvas) + canvas = canvas or Studio.canvas() + if not canvas then return nil end + return canvas.w > canvas.h and "landscape" or "portrait" +end + +local function pickCanvasIndex(want) + local cur = Studio.canvas() + if canvasOrientation(cur) == want then return Studio.canvasIndex end + if cur and cur.id then + local hint = cur.id:gsub("portrait", want):gsub("landscape", want) + for i, c in ipairs(Studio.CANVASES) do + if c.id == hint then return i end + end + end + for i, c in ipairs(Studio.CANVASES) do + if canvasOrientation(c) == want and not c.lockViewport then return i end + end + return nil +end + +function Studio.applyImportedOrient() + if not Studio.skin then return false end + if not TouchSkin.hasOrientPair(Studio.skin) then + Studio.syncCanvasToPage() + return false + end + -- A RetroArch overlay that already auto-rotates should do the same in + -- the studio: lock is on, canvas follows, and the visible page matches + -- the mock device. #1503 + Studio.matchOrient = true + Studio.syncPageToCanvas() + Studio.syncCanvasToPage() + return true +end + +function Studio.syncCanvasToPage() + if not Studio.matchOrient then return end + local want = TouchSkin.pageOrient(Studio.page()) + if not want then return end + local idx = pickCanvasIndex(want) + if idx and idx ~= Studio.canvasIndex then Studio.setCanvas(idx, true) end +end + +function Studio.syncPageToCanvas() + if not Studio.matchOrient or not Studio.skin then return end + local want = canvasOrientation() + if TouchSkin.pageOrient(Studio.page()) == want then return end + for i, page in ipairs(Studio.skin.pages or {}) do + if TouchSkin.pageOrient(page) == want then + Studio.pageIndex = i + Studio.selected = nil + syncActive() + return + end + end +end + +function Studio.cyclePageOrient(dir) + local page = Studio.page() + if not page then return end + local cur = TouchSkin.pageOrient(page) or "any" + local idx = 1 + for i, o in ipairs(Studio.ORIENT_CYCLE) do + if o == cur then idx = i break end + end + local n = #Studio.ORIENT_CYCLE + local nxt = Studio.ORIENT_CYCLE[((idx - 1 + (dir or 1)) % n) + 1] + page.orient = nxt + local name = tostring(page.name or "") + if (nxt == "portrait" or nxt == "landscape") + and (name == "" or name == "main" or name:match("^page%d+$")) then + page.name = nxt + end + markDirty() + Studio.syncCanvasToPage() + return nxt +end + +function Studio.setCanvas(index, fromSync) local n = #Studio.CANVASES Studio.canvasIndex = ((index - 1) % n) + 1 + -- Pick the matching page before writing canvas-owned fields onto it, + -- so a landscape preset does not stamp a portrait page. #1503 + if not fromSync then Studio.syncPageToCanvas() end local canvas = Studio.canvas() local page = Studio.page() if page and canvas.lockViewport then @@ -77,7 +170,11 @@ function Studio.setCanvas(index) page.viewportFill = false markDirty() end - if page then page.aspect = canvas.w / canvas.h end + -- A cfg-authored aspect_ratio is the overlay's design aspect; keep it so + -- the preview letterboxes like RetroArch instead of stretching (#1503). + if page and not page.aspectFromCfg then + page.aspect = canvas.w / canvas.h + end end function Studio.load(opts) @@ -102,6 +199,10 @@ function Studio.load(opts) TouchControls.active = true TouchControls.enabled = true TouchControls:setPreview(true) + -- Play snaps pages from the window aspect. The studio uses its own + -- Match canvas toggle against the mock device instead. #1503 + TouchSkin.autoOrient = false + Studio.matchOrient = true local start = opts.skinId if not start then @@ -130,6 +231,7 @@ function Studio.open(id) Studio.dirty = false Studio.images = TouchSkin.listImages(Studio.skin.root) syncActive() + Studio.applyImportedOrient() return true end @@ -137,6 +239,7 @@ function Studio.unload() Studio.pendingPlay = false TouchSkin.setSurface(nil) TouchSkin.setActive(nil) + TouchSkin.autoOrient = true TouchControls:setPreview(false) TouchControls:reset() Studio.skin = nil @@ -358,6 +461,7 @@ function Studio.addPage() Studio.pageIndex = #skin.pages Studio.selected = nil syncActive() + Studio.syncCanvasToPage() markDirty() end @@ -432,7 +536,8 @@ end local function viewportRect(page, r) local v = page.viewport if not v then return nil end - return r.x + v.x * r.w, r.y + v.y * r.h, v.w * r.w, v.h * r.h + local bx, by, bw, bh = TouchSkin.pageBox(page, r.w, r.h, r.x, r.y) + return bx + v.x * bw, by + v.y * bh, v.w * bw, v.h * bh end local function handleRects(bx, by, bw, bh) @@ -540,17 +645,19 @@ function Studio.updateDrag(mx, my, r) end end + local px, py, pw, ph = TouchSkin.pageBox(page, r.w, r.h, r.x, r.y) + if pw <= 0 or ph <= 0 then return end if d.kind:find("control") then local ctl = Studio.selectedControl() if not ctl then return end - ctl.x = clamp01(((bx + bw * 0.5) - r.x) / r.w) - ctl.y = clamp01(((by + bh * 0.5) - r.y) / r.h) - ctl.rangeX = math.max(0.002, (bw * 0.5) / r.w) - ctl.rangeY = math.max(0.002, (bh * 0.5) / r.h) + ctl.x = clamp01(((bx + bw * 0.5) - px) / pw) + ctl.y = clamp01(((by + bh * 0.5) - py) / ph) + ctl.rangeX = math.max(0.002, (bw * 0.5) / pw) + ctl.rangeY = math.max(0.002, (bh * 0.5) / ph) else page.viewport = { - x = clamp01((bx - r.x) / r.w), y = clamp01((by - r.y) / r.h), - w = math.max(0.02, bw / r.w), h = math.max(0.02, bh / r.h), + x = clamp01((bx - px) / pw), y = clamp01((by - py) / ph), + w = math.max(0.02, bw / pw), h = math.max(0.02, bh / ph), } end markDirty() @@ -665,11 +772,22 @@ local function inspectorBody(x, y, w) Studio.pageIndex = (Studio.pageIndex % #Studio.skin.pages) + 1 Studio.selected = nil syncActive() + Studio.syncCanvasToPage() end if Kit.button(x + half + gap, cy, half, rowH, "Add page", { id = "pageadd" }) then Studio.addPage() end cy = cy + rowH + gap + local lock = TouchSkin.pageOrient(page) or "any" + if Kit.button(x, cy, half, rowH, Studio.ORIENT_LABEL[lock] or "Lock: Off", + { id = "orient" }) then + Studio.cyclePageOrient(1) + end + local matchOn = Studio.matchOrient + Studio.matchOrient = Kit.checkbox(x + half + gap, cy, half, rowH, + Studio.matchOrient, "Match canvas", "matchorient") + if Studio.matchOrient and not matchOn then Studio.syncCanvasToPage() end + cy = cy + rowH + gap if page then local bezel = page.imagePath or "(none)" diff --git a/tests/engine/skin_studio_test.lua b/tests/engine/skin_studio_test.lua index 3fd66f18..ce97a6f1 100644 --- a/tests/engine/skin_studio_test.lua +++ b/tests/engine/skin_studio_test.lua @@ -195,6 +195,55 @@ Studio.addControl() eq(#Studio.skin.pages[2].controls, 1, "controls land on the active page") eq(#Studio.skin.pages[1].controls, 0, "and not on the other one") +-- page orientation lock follows the canvas when Match canvas is on (#1503) +session() +Studio.canvasIndex = 1 +Studio.matchOrient = true +eq(Studio.cyclePageOrient(1), "portrait", "cycle starts at portrait from unlocked") +eq(Studio.page().orient, "portrait", "and stores the lock on the page") +eq(Studio.page().name, "portrait", "a generic page is renamed so play can auto-rotate") +eq(Studio.canvas().id, "phone_portrait", "and the canvas stays portrait") + +Studio.addPage() +eq(Studio.cyclePageOrient(1), "portrait", "the new page starts unlocked, first lock is portrait") +Studio.cyclePageOrient(1) +eq(Studio.page().orient, "landscape", "second cycle is landscape") +eq(Studio.canvas().id, "phone_landscape", "Match canvas flips the mock device with the page") + +Studio.pageIndex = 1 +Studio.syncCanvasToPage() +eq(Studio.canvas().id, "phone_portrait", "switching back to the portrait page restores portrait canvas") + +Studio.setCanvas(2) +eq(Studio.pageIndex, 2, "picking a landscape canvas selects the landscape page") + +Studio.matchOrient = false +Studio.pageIndex = 1 +Studio.setCanvas(2) +eq(Studio.pageIndex, 1, "Match canvas off leaves the page when the device changes") +eq(Studio.canvas().id, "phone_landscape", "and still honours the canvas click") + +-- a RetroArch overlay that already auto-rotates locks itself on open (#1503) +session() +Studio.matchOrient = false +Studio.canvasIndex = 2 +Studio.skin = assert(TouchSkin.parse([[ +overlays = 2 +overlay0_name = "portrait" +overlay0_full_screen = true +overlay0_descs = 1 +overlay0_desc0 = "a,0.5,0.5,radial,0.05,0.05" +overlay1_name = "landscape" +overlay1_full_screen = true +overlay1_descs = 1 +overlay1_desc0 = "a,0.5,0.5,radial,0.05,0.05" +]])) +Studio.pageIndex = 1 +check(Studio.applyImportedOrient(), "import of a portrait/landscape pair is automatic") +check(Studio.matchOrient, "and turns Match canvas on") +eq(Studio.page().name, "landscape", "keeping the landscape canvas already on screen") +eq(Studio.page().orient, "landscape", "with the page already locked") + -- --------------------------------------------------------------- clone local source = TouchSkin.load("assets/skins/gb_anim", "gb_anim") diff --git a/tests/engine/touch_skin_test.lua b/tests/engine/touch_skin_test.lua index c0223638..7d43e4c0 100644 --- a/tests/engine/touch_skin_test.lua +++ b/tests/engine/touch_skin_test.lua @@ -51,6 +51,7 @@ local page = skin.pages[1] eq(page.name, "shell", "page name") eq(page.imagePath, "img/back.png", "page background path") check(page.fullScreen, "full_screen parsed") +check(not page.aspectFromCfg, "a cfg without aspect_ratio does not lock aspect") eq(page.alphaMod, 0.001, "overlay alpha_mod parsed") eq(#page.controls, 7, "seven descs parsed") @@ -109,7 +110,7 @@ local sx, sy, sw, sh, fill = TouchSkin.viewport(W, H) eq(sx, 0, "viewport x") eq(sy, 0, "viewport y") eq(sw, 400, "viewport w") eq(sh, 400, "viewport h") check(fill, "viewport fill flag returned") -eq(TouchSkin.viewport(W, H), 0, "viewport is window-relative, not page-relative") +eq(TouchSkin.viewport(W, H), 0, "without a locked aspect, viewport tracks the window") check(TouchControls:touchpressed("f1", at(0.80, 0.75)), "press on A is captured") check(Input:isDown("a"), "skin A presses GB a") @@ -195,6 +196,8 @@ if bundled then for _, btn in ipairs({ "a", "b", "start", "select", "up", "down", "left", "right" }) do check(named[btn], "gb_anim binds GB " .. btn) end + check(not TouchSkin.hasOrientPair(bundled), + "gb_anim is not a portrait/landscape auto-rotate overlay") end local tv = TouchSkin.load("assets/skins/tv_crt", "tv_crt") @@ -322,4 +325,104 @@ end TouchSkin.setActive(nil) TouchControls:setHotkeyHandler(nil) +-- ------------------------------------------ auto-rotate (#1503) + +-- RetroArch overlays name a portrait page and a landscape page and wire +-- overlay_next between them. Play has to pick the one that matches the +-- window, or landscape stretches the portrait ranges into wide ovals. +local ORIENT_CFG = [[ +overlays = 2 +overlay0_name = "portrait" +overlay0_full_screen = true +overlay0_normalized = true +overlay0_aspect_ratio = 0.45 +overlay0_descs = 1 +overlay0_desc0 = "a,0.84382,0.69563,radial,0.09722,0.04375" + +overlay1_name = "landscape" +overlay1_full_screen = true +overlay1_normalized = true +overlay1_aspect_ratio = 2.22222222222222 +overlay1_descs = 1 +overlay1_desc0 = "a,0.8307031248,0.8614400000,radial,0.0364168752,0.0813300000" +]] +local orient = assert(TouchSkin.parse(ORIENT_CFG)) +check(orient.pages[1].aspectFromCfg, "portrait page locks the cfg aspect_ratio") +check(orient.pages[2].aspectFromCfg, "landscape page locks the cfg aspect_ratio") +eq(orient.pages[1].orient, "portrait", "a portrait page name locks on import") +eq(orient.pages[2].orient, "landscape", "a landscape page name locks on import") +check(TouchSkin.hasOrientPair(orient), "the pair is an auto-rotate overlay") +TouchSkin.setActive(orient) +TouchSkin.autoOrient = true + +local PW, PH = 720, 1600 +TouchSkin.setSurface(0, 0, PW, PH) +eq(TouchSkin.page().name, "portrait", "a tall window picks the portrait page") +local _, _, pHalfW, pHalfH = + TouchSkin.controlGeometry(TouchSkin.page(), TouchSkin.page().controls[1], PW, PH) +eq(math.floor(pHalfW + 0.5), 70, "portrait A half-width follows range_x") +eq(math.floor(pHalfH + 0.5), 70, "portrait A half-height follows range_y") + +local LW, LH = 1600, 720 +TouchSkin.setSurface(0, 0, LW, LH) +eq(TouchSkin.page().name, "landscape", "a wide window picks the landscape page") +local _, _, lHalfW, lHalfH = + TouchSkin.controlGeometry(TouchSkin.page(), TouchSkin.page().controls[1], LW, LH) +eq(math.floor(lHalfW + 0.5), 58, "landscape A half-width follows the landscape range") +eq(math.floor(lHalfH + 0.5), 59, "landscape A half-height follows the landscape range") +check(math.abs(lHalfW - lHalfH) < 2, "landscape A stays round instead of stretching") + +-- 16:9 is not the overlay's 20:9. Letterbox to aspect_ratio so A stays round +-- instead of filling the window and turning into a tall oval (#1503). +TouchSkin.setSurface(0, 0, 1920, 1080) +eq(TouchSkin.page().name, "landscape", "16:9 still picks landscape") +local _, _, boxW, boxH = TouchSkin.pageBox(TouchSkin.page(), 1920, 1080) +eq(math.floor(boxW + 0.5), 1920, "letterbox keeps the 16:9 width") +eq(math.floor(boxH + 0.5), 864, "and the overlay's 20:9 height") +local _, _, sHalfW, sHalfH = + TouchSkin.controlGeometry(TouchSkin.page(), TouchSkin.page().controls[1], 1920, 1080) +check(math.abs(sHalfW - sHalfH) < 2, "A stays round on a 16:9 window") + +local nativeOrient = TouchSkin.parseNative(TouchSkin.serialize(orient)) +check(nativeOrient and nativeOrient.pages[2].aspectFromCfg, + "native export keeps the aspect lock") + +-- an explicit lock wins over the page name, so a studio-authored "main" +-- page can still auto-rotate in play +local LOCK_CFG = [[ +overlays = 2 +overlay0_name = "main" +overlay0_full_screen = true +overlay0_descs = 1 +overlay0_desc0 = "a,0.5,0.5,radial,0.05,0.05" +overlay1_name = "wide" +overlay1_full_screen = true +overlay1_descs = 1 +overlay1_desc0 = "a,0.5,0.5,radial,0.05,0.05" +]] +local locked = assert(TouchSkin.parse(LOCK_CFG)) +locked.pages[1].orient = "portrait" +locked.pages[2].orient = "landscape" +TouchSkin.setActive(locked) +TouchSkin.setSurface(0, 0, 1600, 720) +eq(TouchSkin.page().name, "wide", "orient lock auto-rotates a page not named landscape") +local roundTrip = TouchSkin.parseNative(TouchSkin.serialize(locked)) +eq(roundTrip.pages[1].orient, "portrait", "native export keeps a portrait lock") +eq(roundTrip.pages[2].orient, "landscape", "and a landscape lock") + +TouchSkin.setActive(orient) +TouchSkin.autoOrient = false +TouchSkin.pageIndex = 1 +eq(TouchSkin.page().name, "portrait", + "the studio can keep a portrait page on a landscape canvas") +TouchSkin.autoOrient = true +TouchSkin.setSurface(nil) + +-- pages that are not a portrait/landscape pair (gb_anim) stay put +TouchSkin.setActive(skin) +TouchSkin.setSurface(0, 0, LW, LH) +eq(TouchSkin.page().name, "shell", "a non-oriented skin does not auto-rotate") +TouchSkin.setSurface(nil) +TouchSkin.setActive(nil) + T.finish("touch_skin")