mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 03:35:56 +02:00
skins and skin studio
This commit is contained in:
+246
-5
@@ -776,6 +776,26 @@ end
|
||||
|
||||
-- A hand-drawn X / check: the UI font has no guaranteed glyph for either,
|
||||
-- and the launcher ships no icon asset for them.
|
||||
-- Skins tab glyph: a bezel with a screen cutout and two face buttons, drawn
|
||||
-- rather than shipped as art so the tab needs no new asset.
|
||||
local function drawSkinGlyph(x, y, w, h, hot)
|
||||
local box = math.min(w, h)
|
||||
local bx = x + (w - box) / 2
|
||||
local by = y + (h - box) / 2
|
||||
local pad = math.floor(box * 0.22)
|
||||
local ow, oh = box - 2 * pad, box - 2 * pad
|
||||
local ink = hot and PAL.inverse or PAL.ink
|
||||
local a = 1
|
||||
Theme.strokeRounded(bx + pad, by + pad, ow, oh, ink, a,
|
||||
math.max(1, math.floor(Kit.scale)), math.floor(oh * 0.22))
|
||||
local sw, sh = ow * 0.58, oh * 0.40
|
||||
Theme.fillRounded(bx + pad + ow * 0.10, by + pad + oh * 0.14, sw, sh, ink, a,
|
||||
math.max(1, math.floor(sh * 0.2)))
|
||||
local r = math.max(1, oh * 0.09)
|
||||
Theme.fillRounded(bx + pad + ow * 0.60, by + pad + oh * 0.64, r * 2, r * 2, ink, a, r)
|
||||
Theme.fillRounded(bx + pad + ow * 0.80, by + pad + oh * 0.50, r * 2, r * 2, ink, a, r)
|
||||
end
|
||||
|
||||
local function drawCross(x, y, size, color)
|
||||
love.graphics.push("all")
|
||||
love.graphics.setColor(color)
|
||||
@@ -807,18 +827,46 @@ end
|
||||
-- frame. Their tab rows, opts tables and action closures are built once
|
||||
-- instead of 60 times a second -- only `active`, `image` and the queued
|
||||
-- action are written per frame.
|
||||
-- The four cartridges used to be four tabs of their own. They are one
|
||||
-- dropdown now: the tab row was seven controls wide and wrapped to two rows on
|
||||
-- anything narrow, and only ever one game is being looked at.
|
||||
local GAME_TABS = {
|
||||
{ id = "red", key = "tab-red", letter = "R", color = PAL.railRed,
|
||||
label = "Red" },
|
||||
{ id = "blue", key = "tab-blue", letter = "B", color = PAL.railBlue,
|
||||
label = "Blue" },
|
||||
{ id = "yellow", key = "tab-yellow", letter = "Y", color = PAL.railGold,
|
||||
label = "Yellow" },
|
||||
{ id = "gold", key = "tab-gold", letter = "G", color = PAL.railAmber,
|
||||
label = "Gold" },
|
||||
}
|
||||
|
||||
local HEADER_TABS = {
|
||||
{ id = "red", key = "tab-red", letter = "R", color = PAL.railRed },
|
||||
{ id = "blue", key = "tab-blue", letter = "B", color = PAL.railBlue },
|
||||
{ id = "yellow", key = "tab-yellow", letter = "Y", color = PAL.railGold },
|
||||
{ id = "gold", key = "tab-gold", letter = "G", color = PAL.railAmber },
|
||||
{ id = "mods", key = "tab-mods" },
|
||||
{ id = "find", key = "tab-find" },
|
||||
{ id = "skins", key = "tab-skins", glyph = true },
|
||||
}
|
||||
for _, t in ipairs(HEADER_TABS) do
|
||||
t.opts = { face = "tab", font = "tab", color = t.color, letter = t.letter }
|
||||
if t.glyph then t.opts.drawFn = drawSkinGlyph end
|
||||
end
|
||||
|
||||
-- Which cartridge the dropdown is showing: the open game tab, else the last
|
||||
-- one visited, else Red. Kept as a function so the mods/find/skins panels
|
||||
-- still answer "for which game" without a game tab being open.
|
||||
local function currentGame(imp)
|
||||
for _, g in ipairs(GAME_TABS) do
|
||||
if imp.tab == g.id then return g end
|
||||
end
|
||||
for _, g in ipairs(GAME_TABS) do
|
||||
if imp.modScope == g.id then return g end
|
||||
end
|
||||
return GAME_TABS[1]
|
||||
end
|
||||
|
||||
LauncherView.GAME_TABS = GAME_TABS
|
||||
LauncherView.currentGame = currentGame
|
||||
|
||||
local QUIT_INK_HOT = { 0, 0, 0, 1 }
|
||||
local QUIT_INK_REST = { 1, 1, 1, 0.85 }
|
||||
|
||||
@@ -837,11 +885,27 @@ local function headerChrome(imp)
|
||||
hot and QUIT_INK_HOT or QUIT_INK_REST)
|
||||
end },
|
||||
tab = {},
|
||||
game = { face = "tab", font = "tab",
|
||||
action = function()
|
||||
local g = currentGame(imp)
|
||||
if imp.tab == g.id then
|
||||
imp._gamePopup = true
|
||||
else
|
||||
imp:_switchTab(g.id)
|
||||
end
|
||||
end },
|
||||
}
|
||||
for _, t in ipairs(HEADER_TABS) do
|
||||
local id = t.id
|
||||
c.tab[id] = function() imp:_switchTab(id) end
|
||||
end
|
||||
for _, g in ipairs(GAME_TABS) do
|
||||
local id = g.id
|
||||
c.tab[id] = function()
|
||||
imp._gamePopup = nil
|
||||
imp:_switchTab(id)
|
||||
end
|
||||
end
|
||||
imp._headerChrome = c
|
||||
return c
|
||||
end
|
||||
@@ -927,7 +991,10 @@ local function buildHeader(imp, m)
|
||||
-- bright cart gold; Gold (Gen 2) uses the deeper amber so the two do not
|
||||
-- collide.
|
||||
local tabs = HEADER_TABS
|
||||
tabs[5].icon, tabs[6].icon = imp._modsIcon, imp._findIcon
|
||||
for _, t in ipairs(tabs) do
|
||||
if t.id == "mods" then t.icon = imp._modsIcon end
|
||||
if t.id == "find" then t.icon = imp._findIcon end
|
||||
end
|
||||
local tabH = m.chip
|
||||
local tx = m.x + m.pad
|
||||
local ty = y + math.floor(6 * m.s)
|
||||
@@ -935,6 +1002,40 @@ local function buildHeader(imp, m)
|
||||
local tabRight = m.x + m.w - m.pad
|
||||
local tabGap = math.floor(6 * m.s)
|
||||
local tabRowGap = math.floor(4 * m.s)
|
||||
|
||||
-- the cartridge dropdown, sized to its longest label so switching games
|
||||
-- never reflows the row
|
||||
local chrome0 = headerChrome(imp)
|
||||
local game = currentGame(imp)
|
||||
local labelW = 0
|
||||
for _, g in ipairs(GAME_TABS) do
|
||||
labelW = math.max(labelW, Kit.textWidth("tab", Strings(g.label)))
|
||||
end
|
||||
local dropW = math.min(tabRight - tabLeft,
|
||||
tabH + labelW + math.floor(34 * m.s))
|
||||
chrome0.game.color = game.color
|
||||
chrome0.game.letter = game.letter
|
||||
chrome0.game.active = imp.tab == game.id
|
||||
local gameHot = Kit.hover(tx, ty, dropW, tabH)
|
||||
local gameDown = gameHot and Kit.mouseDown
|
||||
-- face "tab" inverts on hover as well as when active, so the caret has to
|
||||
-- flip with it or it vanishes into the cartridge colour
|
||||
local gameInvert = chrome0.game.active or gameHot
|
||||
chrome0.game.ring = gameHot and not chrome0.game.active or nil
|
||||
btn(imp, tx, ty, dropW, tabH, "tab-game", Strings(game.label), chrome0.game)
|
||||
do
|
||||
local cw = math.floor(7 * m.s)
|
||||
local ccx = tx + dropW - math.floor(14 * m.s)
|
||||
local ccy = ty + tabH / 2 + (gameDown and math.floor(1 * m.s) or 0)
|
||||
if love.graphics.polygon then
|
||||
Theme.col(gameInvert and PAL.inverse or PAL.ink, gameDown and 1 or 0.9)
|
||||
love.graphics.polygon("fill",
|
||||
ccx - cw, ccy - cw * 0.5, ccx + cw, ccy - cw * 0.5, ccx, ccy + cw * 0.8)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
tx = tx + dropW + tabGap
|
||||
|
||||
for _, t in ipairs(tabs) do
|
||||
local w = tabH
|
||||
if tx > tabLeft and tx + w > tabRight then
|
||||
@@ -1909,6 +2010,117 @@ end
|
||||
|
||||
-- ---------------------------------------------------------- find mods panel
|
||||
|
||||
-- SKINS tab: pick the on-screen skin, import one, or open the desktop studio.
|
||||
local function buildSkinsPanel(imp, x, y, w, availH, m)
|
||||
local skins = imp:_ensureSkins()
|
||||
local active = imp:_activeSkin()
|
||||
local gap = m.gap
|
||||
local cy = y
|
||||
|
||||
if imp._skinNotice then
|
||||
cy = cy + Kit.textWrapped("small", imp._skinNotice.text, x, cy, w,
|
||||
imp._skinNotice.ok and PAL.green or PAL.red, 2) + math.floor(8 * m.s)
|
||||
end
|
||||
|
||||
-- Studio button. Desktop only: the host supplies the hook nowhere else.
|
||||
if imp.onOpenSkinStudio then
|
||||
local label = Strings("Open Skin Studio")
|
||||
local bw = math.min(w, Kit.textWidth("small", label) + math.floor(40 * m.s))
|
||||
btn(imp, x, cy, bw, m.btnH, "skins-studio", label, {
|
||||
kind = "accent", font = "small",
|
||||
action = function()
|
||||
-- the studio boots the game on Play, so hand it a real cartridge
|
||||
imp.onOpenSkinStudio(imp.modScope or "red")
|
||||
end })
|
||||
local hint = Strings("Design bezels and button layouts, then test them.")
|
||||
Kit.text("small", Kit.ellipsize("small", hint,
|
||||
w - bw - math.floor(12 * m.s)), x + bw + math.floor(12 * m.s),
|
||||
cy + math.floor((m.btnH - Kit.textHeight("small")) / 2), PAL.muted)
|
||||
cy = cy + m.btnH + gap
|
||||
end
|
||||
|
||||
Kit.caption(x, cy, Strings("INSTALLED"))
|
||||
cy = cy + Kit.textHeight("small") + math.floor(6 * m.s)
|
||||
|
||||
local rowH = math.max(Kit.tapMin(), math.floor(44 * m.s))
|
||||
imp._skinGear = imp._skinGear
|
||||
or love.graphics.newImage("assets/launcher/gear.png")
|
||||
|
||||
-- The row itself is "use this skin"; the gear beside it configures that
|
||||
-- entry -- the built-in pad opens the drag-a-button layout editor, a skin
|
||||
-- opens the studio, so neither lands on a screen that cannot edit it.
|
||||
local function skinRow(key, id, title, detail, selected, configure)
|
||||
local gearW = configure and rowH or 0
|
||||
local rowW = w - (gearW > 0 and (gearW + math.floor(6 * m.s)) or 0)
|
||||
local ink = rowHit(imp, x, cy, rowW, rowH, selected, key,
|
||||
function() imp:_useSkin(id) end)
|
||||
local tagW = selected
|
||||
and (Kit.textWidth("small", Strings("IN USE")) + math.floor(20 * m.s))
|
||||
or math.floor(12 * m.s)
|
||||
local textW = rowW - math.floor(24 * m.s) - tagW
|
||||
local tx = x + math.floor(12 * m.s)
|
||||
local ty = cy + math.floor(7 * m.s)
|
||||
Kit.text("mono", Kit.ellipsize("mono", title, textW), tx, ty,
|
||||
ink or PAL.heading)
|
||||
Kit.text("small", Kit.ellipsize("small", detail, textW),
|
||||
tx, ty + Kit.textHeight("mono"), ink or PAL.muted)
|
||||
if selected then
|
||||
Kit.textRight("small", Strings("IN USE"), x + rowW - math.floor(12 * m.s),
|
||||
cy + math.floor((rowH - Kit.textHeight("small")) / 2), ink or PAL.green)
|
||||
end
|
||||
if configure then
|
||||
btn(imp, x + w - gearW, cy, gearW, gearW, key .. "-cfg", "", {
|
||||
face = "invert", image = imp._skinGear, action = configure })
|
||||
end
|
||||
cy = cy + rowH + math.floor(4 * m.s)
|
||||
end
|
||||
|
||||
skinRow("skin-none", nil, Strings("Built-in pad"),
|
||||
Strings("The default on-screen buttons."), active == nil,
|
||||
imp.onEditTouchControls and function()
|
||||
imp.onEditTouchControls(imp.modScope or "red")
|
||||
end or nil)
|
||||
|
||||
for _, entry in ipairs(skins) do
|
||||
local bits = {}
|
||||
bits[#bits + 1] = entry.source == "user" and Strings("installed")
|
||||
or Strings("bundled")
|
||||
if entry.controls > 0 then
|
||||
bits[#bits + 1] = entry.controls .. " " .. Strings("buttons")
|
||||
else
|
||||
bits[#bits + 1] = Strings("bezel only")
|
||||
end
|
||||
if entry.pages > 1 then
|
||||
bits[#bits + 1] = entry.pages .. " " .. Strings("pages")
|
||||
end
|
||||
if entry.screen then bits[#bits + 1] = Strings("screen cutout") end
|
||||
local configure = imp.onOpenSkinStudio and function()
|
||||
imp.onOpenSkinStudio(imp.modScope or "red", entry.id)
|
||||
end or nil
|
||||
skinRow("skin-" .. entry.id, entry.id, entry.id,
|
||||
table.concat(bits, " \194\183 "), active == entry.id, configure)
|
||||
end
|
||||
|
||||
if #skins == 0 then
|
||||
Kit.emptyBox(x, cy, w, math.floor(72 * m.s),
|
||||
Strings("No skins installed yet."))
|
||||
cy = cy + math.floor(72 * m.s) + gap
|
||||
end
|
||||
|
||||
cy = cy + math.floor(6 * m.s)
|
||||
local TouchSkin = require("src.core.TouchSkin")
|
||||
Kit.caption(x, cy, Strings("IMPORT"))
|
||||
cy = cy + Kit.textHeight("small") + math.floor(6 * m.s)
|
||||
local boxH = math.floor(76 * m.s)
|
||||
Kit.card(x, cy, w, boxH, "muted")
|
||||
Kit.textWrapped("small", Strings(
|
||||
"Drop a skin .zip on this window to install it, or put a folder in the skins folder of your save directory. RetroArch overlay .cfg files work as-is."),
|
||||
x + math.floor(14 * m.s), cy + math.floor(12 * m.s),
|
||||
w - math.floor(28 * m.s), PAL.muted, 3)
|
||||
Kit.text("small", TouchSkin.USER_ROOT .. "/", x + math.floor(14 * m.s),
|
||||
cy + boxH - Kit.textHeight("small") - math.floor(10 * m.s), PAL.faint)
|
||||
end
|
||||
|
||||
local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
imp:_ensureFind()
|
||||
imp:_ensureMods()
|
||||
@@ -2780,6 +2992,32 @@ local function buildModScopeModal(imp, m)
|
||||
action = function() imp._modScopePopup = nil end })
|
||||
end
|
||||
|
||||
-- The cartridge dropdown's list. Replaces the four R/B/Y/G tabs, so it is
|
||||
-- also what a controller reaches after the tab row.
|
||||
local function buildGameModal(imp, m)
|
||||
local pad = math.floor(18 * m.s)
|
||||
local gap = math.floor(8 * m.s)
|
||||
local w = math.floor(360 * m.s)
|
||||
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
+ #GAME_TABS * (m.btnH + gap) + m.btnH + pad
|
||||
local px, py, pw = modalPanel(m, w, h)
|
||||
local cy = py + pad
|
||||
Kit.text("button", Strings("Choose game"), px + pad, cy, PAL.heading)
|
||||
cy = cy + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
local chrome = headerChrome(imp)
|
||||
for _, g in ipairs(GAME_TABS) do
|
||||
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "gamepop-" .. g.id,
|
||||
Strings(g.label), {
|
||||
face = "tab", font = "small", letter = g.letter, color = g.color,
|
||||
active = imp.tab == g.id,
|
||||
action = chrome.tab[g.id] })
|
||||
cy = cy + m.btnH + gap
|
||||
end
|
||||
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "gamepop-close",
|
||||
Strings("Close"), { font = "small",
|
||||
action = function() imp._gamePopup = nil end })
|
||||
end
|
||||
|
||||
-- Category filter for FIND MODS. Two columns, because an index can list
|
||||
-- enough categories to overflow a single stacked column on a short window.
|
||||
local function buildFilterModal(imp, m)
|
||||
@@ -3753,6 +3991,7 @@ local function buildModals(imp, m)
|
||||
if imp._profilesPopup then buildProfilesModal(imp, m) return true end
|
||||
if imp._modHeaderActionsPopup then buildModHeaderActionsModal(imp, m) return true end
|
||||
if imp._sortPopup then buildSortModal(imp, m) return true end
|
||||
if imp._gamePopup then buildGameModal(imp, m) return true end
|
||||
if imp._modScopePopup then buildModScopeModal(imp, m) return true end
|
||||
if imp._filterPopup then buildFilterModal(imp, m) return true end
|
||||
if imp._indexManage then buildIndexesModal(imp, m) return true end
|
||||
@@ -3906,6 +4145,8 @@ function LauncherView.draw(imp)
|
||||
buildModsPanel(imp, x, contentY, w, availH, m)
|
||||
elseif imp.tab == "find" then
|
||||
buildFindPanel(imp, x, contentY, w, availH, m)
|
||||
elseif imp.tab == "skins" then
|
||||
buildSkinsPanel(imp, x, contentY, w, availH, m)
|
||||
else
|
||||
buildGamePanel(imp, x, contentY, w, availH, m, imp.tab)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user