mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 20:20:19 +02:00
CLOSES #1503
This commit is contained in:
+10
-1
@@ -26,7 +26,7 @@ as-is. Supported keys:
|
|||||||
| `overlayN_overlay` | bezel image |
|
| `overlayN_overlay` | bezel image |
|
||||||
| `overlayN_full_screen` | stretch the page to the window |
|
| `overlayN_full_screen` | stretch the page to the window |
|
||||||
| `overlayN_rect` | page placement, default `0,0,1,1` |
|
| `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_range_mod`, `overlayN_alpha_mod` | desc defaults |
|
||||||
| `overlayN_viewport` | `x,y,w,h`, the screen cutout |
|
| `overlayN_viewport` | `x,y,w,h`, the screen cutout |
|
||||||
| `overlayN_viewport_fill` | parsed; the engine always fits, see below |
|
| `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
|
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.
|
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
|
**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
|
images already in the skin folder; the **Import** button beside each one opens
|
||||||
the host file picker (`src/core/FilePicker.lua`: osascript, PowerShell,
|
the host file picker (`src/core/FilePicker.lua`: osascript, PowerShell,
|
||||||
|
|||||||
+90
-4
@@ -142,7 +142,11 @@ function TouchSkin.parse(text)
|
|||||||
controls = {},
|
controls = {},
|
||||||
}
|
}
|
||||||
if page.imagePath == "" then page.imagePath = nil end
|
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)
|
page.aspect = page.name:lower():find("portrait", 1, true)
|
||||||
and PORTRAIT_ASPECT or DEFAULT_ASPECT
|
and PORTRAIT_ASPECT or DEFAULT_ASPECT
|
||||||
end
|
end
|
||||||
@@ -170,6 +174,12 @@ function TouchSkin.parse(text)
|
|||||||
pages[#pages + 1] = page
|
pages[#pages + 1] = page
|
||||||
end
|
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 }
|
return { pages = pages }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -255,6 +265,9 @@ function TouchSkin.parseNative(text)
|
|||||||
rangeMod = num(raw.rangeMod, 1),
|
rangeMod = num(raw.rangeMod, 1),
|
||||||
alphaMod = num(raw.alphaMod, 1),
|
alphaMod = num(raw.alphaMod, 1),
|
||||||
aspect = num(raw.aspect, DEFAULT_ASPECT),
|
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 },
|
rect = { x = 0, y = 0, w = 1, h = 1 },
|
||||||
controls = {},
|
controls = {},
|
||||||
}
|
}
|
||||||
@@ -287,6 +300,7 @@ function TouchSkin.parseNative(text)
|
|||||||
nextTarget = c.nextTarget,
|
nextTarget = c.nextTarget,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
if not page.orient then page.orient = TouchSkin.pageOrient(page) end
|
||||||
pages[#pages + 1] = page
|
pages[#pages + 1] = page
|
||||||
end
|
end
|
||||||
return { pages = pages, name = data.name, author = data.author,
|
return { pages = pages, name = data.name, author = data.author,
|
||||||
@@ -309,6 +323,9 @@ function TouchSkin.toNative(skin)
|
|||||||
rangeMod = page.rangeMod,
|
rangeMod = page.rangeMod,
|
||||||
alphaMod = page.alphaMod,
|
alphaMod = page.alphaMod,
|
||||||
aspect = page.aspect,
|
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 = {},
|
controls = {},
|
||||||
}
|
}
|
||||||
if page.rect and (page.rect.x ~= 0 or page.rect.y ~= 0
|
if page.rect and (page.rect.x ~= 0 or page.rect.y ~= 0
|
||||||
@@ -704,6 +721,10 @@ end
|
|||||||
|
|
||||||
TouchSkin.active = nil
|
TouchSkin.active = nil
|
||||||
TouchSkin.pageIndex = 1
|
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
|
TouchSkin.surfaceRect = nil
|
||||||
|
|
||||||
@@ -732,9 +753,68 @@ function TouchSkin.select(id)
|
|||||||
return TouchSkin.setActive(skin)
|
return TouchSkin.setActive(skin)
|
||||||
end
|
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()
|
function TouchSkin.page()
|
||||||
local skin = TouchSkin.active
|
local skin = TouchSkin.active
|
||||||
if not skin then return nil end
|
if not skin then return nil end
|
||||||
|
local w, h = displaySize()
|
||||||
|
TouchSkin.syncOrientation(w, h)
|
||||||
return skin.pages[TouchSkin.pageIndex] or skin.pages[1]
|
return skin.pages[TouchSkin.pageIndex] or skin.pages[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -766,7 +846,12 @@ function TouchSkin.pageBox(page, w, h, ox, oy)
|
|||||||
ox, oy = ox or 0, oy or 0
|
ox, oy = ox or 0, oy or 0
|
||||||
if not page then return ox, oy, w, h end
|
if not page then return ox, oy, w, h end
|
||||||
local bx, by, bw, bh = ox, oy, w, h
|
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
|
local displayAspect = w / h
|
||||||
if displayAspect > page.aspect then
|
if displayAspect > page.aspect then
|
||||||
bw = h * page.aspect
|
bw = h * page.aspect
|
||||||
@@ -833,8 +918,9 @@ function TouchSkin.viewport(w, h, ox, oy)
|
|||||||
local page = TouchSkin.page()
|
local page = TouchSkin.page()
|
||||||
if not page or not page.viewport or not TouchSkin.drawable() then return nil end
|
if not page or not page.viewport or not TouchSkin.drawable() then return nil end
|
||||||
local v = page.viewport
|
local v = page.viewport
|
||||||
local x, y = (ox or 0) + v.x * w, (oy or 0) + v.y * h
|
local bx, by, bw, bh = TouchSkin.pageBox(page, w, h, ox, oy)
|
||||||
local vw, vh = v.w * w, v.h * h
|
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
|
if vw <= 0 or vh <= 0 then return nil end
|
||||||
return x, y, vw, vh, page.viewportFill == true, page.viewportExpand == true
|
return x, y, vw, vh, page.viewportFill == true, page.viewportExpand == true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4324,25 +4324,12 @@ function RomImporter:_findRows()
|
|||||||
category = self.findCategory,
|
category = self.findCategory,
|
||||||
})
|
})
|
||||||
if self.modScope then
|
if self.modScope then
|
||||||
|
local ModTargets = require("src.mods.ModTargets")
|
||||||
local gen = GameVersion.generation(self.modScope)
|
local gen = GameVersion.generation(self.modScope)
|
||||||
local kept = {}
|
local kept = {}
|
||||||
for _, entry in ipairs(rows) do
|
for _, entry in ipairs(rows) do
|
||||||
local has1, has2 = false, false
|
local versions = ModTargets.normalize(entry.games)
|
||||||
local function note(s)
|
if #versions == 0 or ModTargets.covers(versions, gen) then
|
||||||
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
|
|
||||||
kept[#kept + 1] = entry
|
kept[#kept + 1] = entry
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ local function parseEntry(raw)
|
|||||||
summary = str(raw.summary) or "",
|
summary = str(raw.summary) or "",
|
||||||
categories = strArray(raw.categories),
|
categories = strArray(raw.categories),
|
||||||
tags = strArray(raw.tags),
|
tags = strArray(raw.tags),
|
||||||
|
games = strArray(raw.games),
|
||||||
license = str(raw.license),
|
license = str(raw.license),
|
||||||
repo = str(raw.repo),
|
repo = str(raw.repo),
|
||||||
github = str(raw.github),
|
github = str(raw.github),
|
||||||
|
|||||||
+127
-9
@@ -20,6 +20,17 @@ Studio.CANVASES = {
|
|||||||
lockViewport = { x = 48 / 256, y = 40 / 224, w = 160 / 256, h = 144 / 224 } },
|
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 HANDLE = 7
|
||||||
local HANDLES = {
|
local HANDLES = {
|
||||||
{ "nw", 0, 0 }, { "n", 0.5, 0 }, { "ne", 1, 0 },
|
{ "nw", 0, 0 }, { "n", 0.5, 0 }, { "ne", 1, 0 },
|
||||||
@@ -64,9 +75,91 @@ local function syncActive()
|
|||||||
TouchSkin.pageIndex = Studio.pageIndex
|
TouchSkin.pageIndex = Studio.pageIndex
|
||||||
end
|
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
|
local n = #Studio.CANVASES
|
||||||
Studio.canvasIndex = ((index - 1) % n) + 1
|
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 canvas = Studio.canvas()
|
||||||
local page = Studio.page()
|
local page = Studio.page()
|
||||||
if page and canvas.lockViewport then
|
if page and canvas.lockViewport then
|
||||||
@@ -77,7 +170,11 @@ function Studio.setCanvas(index)
|
|||||||
page.viewportFill = false
|
page.viewportFill = false
|
||||||
markDirty()
|
markDirty()
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
function Studio.load(opts)
|
function Studio.load(opts)
|
||||||
@@ -102,6 +199,10 @@ function Studio.load(opts)
|
|||||||
TouchControls.active = true
|
TouchControls.active = true
|
||||||
TouchControls.enabled = true
|
TouchControls.enabled = true
|
||||||
TouchControls:setPreview(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
|
local start = opts.skinId
|
||||||
if not start then
|
if not start then
|
||||||
@@ -130,6 +231,7 @@ function Studio.open(id)
|
|||||||
Studio.dirty = false
|
Studio.dirty = false
|
||||||
Studio.images = TouchSkin.listImages(Studio.skin.root)
|
Studio.images = TouchSkin.listImages(Studio.skin.root)
|
||||||
syncActive()
|
syncActive()
|
||||||
|
Studio.applyImportedOrient()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -137,6 +239,7 @@ function Studio.unload()
|
|||||||
Studio.pendingPlay = false
|
Studio.pendingPlay = false
|
||||||
TouchSkin.setSurface(nil)
|
TouchSkin.setSurface(nil)
|
||||||
TouchSkin.setActive(nil)
|
TouchSkin.setActive(nil)
|
||||||
|
TouchSkin.autoOrient = true
|
||||||
TouchControls:setPreview(false)
|
TouchControls:setPreview(false)
|
||||||
TouchControls:reset()
|
TouchControls:reset()
|
||||||
Studio.skin = nil
|
Studio.skin = nil
|
||||||
@@ -358,6 +461,7 @@ function Studio.addPage()
|
|||||||
Studio.pageIndex = #skin.pages
|
Studio.pageIndex = #skin.pages
|
||||||
Studio.selected = nil
|
Studio.selected = nil
|
||||||
syncActive()
|
syncActive()
|
||||||
|
Studio.syncCanvasToPage()
|
||||||
markDirty()
|
markDirty()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -432,7 +536,8 @@ end
|
|||||||
local function viewportRect(page, r)
|
local function viewportRect(page, r)
|
||||||
local v = page.viewport
|
local v = page.viewport
|
||||||
if not v then return nil end
|
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
|
end
|
||||||
|
|
||||||
local function handleRects(bx, by, bw, bh)
|
local function handleRects(bx, by, bw, bh)
|
||||||
@@ -540,17 +645,19 @@ function Studio.updateDrag(mx, my, r)
|
|||||||
end
|
end
|
||||||
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
|
if d.kind:find("control") then
|
||||||
local ctl = Studio.selectedControl()
|
local ctl = Studio.selectedControl()
|
||||||
if not ctl then return end
|
if not ctl then return end
|
||||||
ctl.x = clamp01(((bx + bw * 0.5) - r.x) / r.w)
|
ctl.x = clamp01(((bx + bw * 0.5) - px) / pw)
|
||||||
ctl.y = clamp01(((by + bh * 0.5) - r.y) / r.h)
|
ctl.y = clamp01(((by + bh * 0.5) - py) / ph)
|
||||||
ctl.rangeX = math.max(0.002, (bw * 0.5) / r.w)
|
ctl.rangeX = math.max(0.002, (bw * 0.5) / pw)
|
||||||
ctl.rangeY = math.max(0.002, (bh * 0.5) / r.h)
|
ctl.rangeY = math.max(0.002, (bh * 0.5) / ph)
|
||||||
else
|
else
|
||||||
page.viewport = {
|
page.viewport = {
|
||||||
x = clamp01((bx - r.x) / r.w), y = clamp01((by - r.y) / r.h),
|
x = clamp01((bx - px) / pw), y = clamp01((by - py) / ph),
|
||||||
w = math.max(0.02, bw / r.w), h = math.max(0.02, bh / r.h),
|
w = math.max(0.02, bw / pw), h = math.max(0.02, bh / ph),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
markDirty()
|
markDirty()
|
||||||
@@ -665,11 +772,22 @@ local function inspectorBody(x, y, w)
|
|||||||
Studio.pageIndex = (Studio.pageIndex % #Studio.skin.pages) + 1
|
Studio.pageIndex = (Studio.pageIndex % #Studio.skin.pages) + 1
|
||||||
Studio.selected = nil
|
Studio.selected = nil
|
||||||
syncActive()
|
syncActive()
|
||||||
|
Studio.syncCanvasToPage()
|
||||||
end
|
end
|
||||||
if Kit.button(x + half + gap, cy, half, rowH, "Add page", { id = "pageadd" }) then
|
if Kit.button(x + half + gap, cy, half, rowH, "Add page", { id = "pageadd" }) then
|
||||||
Studio.addPage()
|
Studio.addPage()
|
||||||
end
|
end
|
||||||
cy = cy + rowH + gap
|
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
|
if page then
|
||||||
local bezel = page.imagePath or "(none)"
|
local bezel = page.imagePath or "(none)"
|
||||||
|
|||||||
@@ -195,6 +195,55 @@ Studio.addControl()
|
|||||||
eq(#Studio.skin.pages[2].controls, 1, "controls land on the active page")
|
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")
|
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
|
-- --------------------------------------------------------------- clone
|
||||||
|
|
||||||
local source = TouchSkin.load("assets/skins/gb_anim", "gb_anim")
|
local source = TouchSkin.load("assets/skins/gb_anim", "gb_anim")
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ local page = skin.pages[1]
|
|||||||
eq(page.name, "shell", "page name")
|
eq(page.name, "shell", "page name")
|
||||||
eq(page.imagePath, "img/back.png", "page background path")
|
eq(page.imagePath, "img/back.png", "page background path")
|
||||||
check(page.fullScreen, "full_screen parsed")
|
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.alphaMod, 0.001, "overlay alpha_mod parsed")
|
||||||
eq(#page.controls, 7, "seven descs 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(sx, 0, "viewport x") eq(sy, 0, "viewport y")
|
||||||
eq(sw, 400, "viewport w") eq(sh, 400, "viewport h")
|
eq(sw, 400, "viewport w") eq(sh, 400, "viewport h")
|
||||||
check(fill, "viewport fill flag returned")
|
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(TouchControls:touchpressed("f1", at(0.80, 0.75)), "press on A is captured")
|
||||||
check(Input:isDown("a"), "skin A presses GB a")
|
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
|
for _, btn in ipairs({ "a", "b", "start", "select", "up", "down", "left", "right" }) do
|
||||||
check(named[btn], "gb_anim binds GB " .. btn)
|
check(named[btn], "gb_anim binds GB " .. btn)
|
||||||
end
|
end
|
||||||
|
check(not TouchSkin.hasOrientPair(bundled),
|
||||||
|
"gb_anim is not a portrait/landscape auto-rotate overlay")
|
||||||
end
|
end
|
||||||
|
|
||||||
local tv = TouchSkin.load("assets/skins/tv_crt", "tv_crt")
|
local tv = TouchSkin.load("assets/skins/tv_crt", "tv_crt")
|
||||||
@@ -322,4 +325,104 @@ end
|
|||||||
TouchSkin.setActive(nil)
|
TouchSkin.setActive(nil)
|
||||||
TouchControls:setHotkeyHandler(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")
|
T.finish("touch_skin")
|
||||||
|
|||||||
Reference in New Issue
Block a user