skins and skin studio

This commit is contained in:
bryanthaboi
2026-08-17 06:47:33 -04:00
parent a5b674f9da
commit 3cca70608f
45 changed files with 4194 additions and 64 deletions
+842
View File
@@ -0,0 +1,842 @@
local TouchSkin = {}
TouchSkin.BUNDLED_ROOT = "assets/skins"
TouchSkin.USER_ROOT = "skins"
TouchSkin.GB_BUTTONS = {
a = "a", b = "b", start = "start", select = "select",
up = "up", down = "down", left = "left", right = "right",
}
TouchSkin.HOTKEYS = {
overlay_next = "overlay_next",
overlay_previous = "overlay_prev",
menu_toggle = "menu",
reset = "soft_reset",
hold_fast_forward = "fast_forward_hold",
fast_forward = "fast_forward_hold",
toggle_fast_forward = "fast_forward_toggle",
screenshot = "screenshot",
pause_toggle = "pause",
exit_emulator = "quit",
}
local DEFAULT_ASPECT = 16 / 9
local PORTRAIT_ASPECT = 9 / 16
local function trim(s)
return (tostring(s or ""):match("^%s*(.-)%s*$"))
end
local function unquote(s)
s = trim(s)
local inner = s:match('^"(.*)"$')
return inner or s
end
local function toBool(v)
v = trim(v):lower()
return v == "true" or v == "1"
end
local function tokens(s)
local out = {}
for tok in tostring(s or ""):gmatch("[^,%s]+") do out[#out + 1] = tok end
return out
end
local function num(v, fallback)
local n = tonumber(trim(v))
if not n or n ~= n then return fallback end
return n
end
function TouchSkin.parseConfig(text)
local kv = {}
for line in tostring(text or ""):gmatch("[^\r\n]+") do
if not line:match("^%s*#") then
local key, value = line:match("^%s*([%w_]+)%s*=%s*(.-)%s*$")
if key then kv[key] = unquote(value) end
end
end
return kv
end
local function parseBinds(spec)
local buttons, hotkeys, keys, decorative = {}, {}, {}, true
for raw in tostring(spec or ""):gmatch("[^|]+") do
local name = trim(raw):lower()
local key = name:match("^key:(.+)$") or name:match("^retrok_(.+)$")
if name == "nul" or name == "" then
-- decoration
elseif key then
keys[#keys + 1] = key
decorative = false
elseif TouchSkin.GB_BUTTONS[name] then
buttons[#buttons + 1] = TouchSkin.GB_BUTTONS[name]
decorative = false
elseif TouchSkin.HOTKEYS[name] then
hotkeys[#hotkeys + 1] = TouchSkin.HOTKEYS[name]
decorative = false
end
end
return buttons, hotkeys, keys, decorative
end
local function parseDesc(kv, prefix, page)
local spec = kv[prefix]
if not spec then return nil end
local t = tokens(spec)
if #t < 6 then return nil end
local shape = trim(t[4]):lower()
if shape ~= "radial" and shape ~= "rect" then shape = "rect" end
local buttons, hotkeys, keys, decorative = parseBinds(t[1])
local reachX = num(kv[prefix .. "_reach_x"], 1)
local reachY = num(kv[prefix .. "_reach_y"], 1)
local ctl = {
spec = t[1],
buttons = buttons,
hotkeys = hotkeys,
keys = keys,
decorative = decorative,
x = num(t[2], 0.5),
y = num(t[3], 0.5),
shape = shape,
rangeX = math.abs(num(t[5], 0.05)),
rangeY = math.abs(num(t[6], 0.05)),
rangeMod = num(kv[prefix .. "_range_mod"], page.rangeMod),
alphaMod = num(kv[prefix .. "_alpha_mod"], page.alphaMod),
reachUp = num(kv[prefix .. "_reach_up"], reachY),
reachDown = num(kv[prefix .. "_reach_down"], reachY),
reachLeft = num(kv[prefix .. "_reach_left"], reachX),
reachRight = num(kv[prefix .. "_reach_right"], reachX),
imagePath = kv[prefix .. "_overlay"],
pressedImagePath = kv[prefix .. "_overlay_pressed"],
nextTarget = kv[prefix .. "_next_target"],
}
if ctl.imagePath == "" then ctl.imagePath = nil end
if ctl.pressedImagePath == "" then ctl.pressedImagePath = nil end
return ctl
end
function TouchSkin.parse(text)
local kv = TouchSkin.parseConfig(text)
local count = math.floor(num(kv.overlays, 0))
if count <= 0 then return nil, "no overlays" end
local pages = {}
for i = 0, count - 1 do
local p = "overlay" .. i
local page = {
index = i + 1,
name = kv[p .. "_name"] or ("overlay" .. i),
imagePath = kv[p .. "_overlay"],
fullScreen = toBool(kv[p .. "_full_screen"]),
normalized = toBool(kv[p .. "_normalized"]),
rangeMod = num(kv[p .. "_range_mod"], 1),
alphaMod = num(kv[p .. "_alpha_mod"], 1),
aspect = num(kv[p .. "_aspect_ratio"], nil),
controls = {},
}
if page.imagePath == "" then page.imagePath = nil end
if not page.aspect or page.aspect <= 0 then
page.aspect = page.name:lower():find("portrait", 1, true)
and PORTRAIT_ASPECT or DEFAULT_ASPECT
end
local rect = tokens(kv[p .. "_rect"])
page.rect = { x = 0, y = 0, w = 1, h = 1 }
if #rect >= 4 then
page.rect = { x = num(rect[1], 0), y = num(rect[2], 0),
w = num(rect[3], 1), h = num(rect[4], 1) }
end
local vp = tokens(kv[p .. "_viewport"])
if #vp >= 4 then
page.viewport = { x = num(vp[1], 0), y = num(vp[2], 0),
w = num(vp[3], 1), h = num(vp[4], 1) }
page.viewportFill = toBool(kv[p .. "_viewport_fill"])
page.viewportExpand = toBool(kv[p .. "_viewport_expand"])
end
local descs = math.floor(num(kv[p .. "_descs"], 0))
for d = 0, descs - 1 do
local ctl = parseDesc(kv, p .. "_desc" .. d, page)
if ctl then page.controls[#page.controls + 1] = ctl end
end
pages[#pages + 1] = page
end
return { pages = pages }
end
local function readFile(path)
if love and love.filesystem and love.filesystem.read then
local ok, data = pcall(love.filesystem.read, path)
if ok and data then return data end
end
local handle = io.open(path, "rb")
if not handle then return nil end
local data = handle:read("*a")
handle:close()
return data
end
local function listDir(path)
if love and love.filesystem and love.filesystem.getDirectoryItems then
local ok, items = pcall(love.filesystem.getDirectoryItems, path)
if ok and items then return items end
end
return {}
end
local function isDir(path)
if love and love.filesystem and love.filesystem.getInfo then
local ok, info = pcall(love.filesystem.getInfo, path)
if ok and info then return info.type == "directory" end
end
return false
end
local function joinPath(root, rel)
rel = tostring(rel or ""):gsub("^%./", ""):gsub("\\", "/")
if rel:sub(1, 1) == "/" then return rel:sub(2) end
return root .. "/" .. rel
end
TouchSkin.NATIVE_NAME = "skin.lua"
local function findConfig(root)
if readFile(root .. "/" .. TouchSkin.NATIVE_NAME) then
return root .. "/" .. TouchSkin.NATIVE_NAME, "native"
end
local named = { "overlay.cfg", "skin.cfg", "layout.cfg" }
for _, name in ipairs(named) do
if readFile(root .. "/" .. name) then return root .. "/" .. name, "retroarch" end
end
local items = listDir(root)
table.sort(items)
for _, name in ipairs(items) do
if name:match("%.cfg$") then return root .. "/" .. name, "retroarch" end
end
return nil
end
local function loadDataChunk(text, name)
if loadstring then
local chunk, err = loadstring(text, name)
if not chunk then return nil, err end
return setfenv(chunk, {})
end
return load(text, name, "t", {})
end
function TouchSkin.parseNative(text)
local chunk, err = loadDataChunk(tostring(text or ""), "skin")
if not chunk then return nil, "skin.lua does not parse: " .. tostring(err) end
local ok, data = pcall(chunk)
if not ok or type(data) ~= "table" then return nil, "skin.lua returned no table" end
if type(data.pages) ~= "table" or not data.pages[1] then
return nil, "skin.lua has no pages"
end
local pages = {}
for i, raw in ipairs(data.pages) do
if type(raw) ~= "table" then return nil, "page " .. i .. " is not a table" end
local page = {
index = i,
name = tostring(raw.name or ("page" .. i)),
imagePath = raw.image,
fullScreen = raw.fullScreen ~= false,
normalized = true,
rangeMod = num(raw.rangeMod, 1),
alphaMod = num(raw.alphaMod, 1),
aspect = num(raw.aspect, DEFAULT_ASPECT),
rect = { x = 0, y = 0, w = 1, h = 1 },
controls = {},
}
if type(raw.rect) == "table" then
page.rect = { x = num(raw.rect.x, 0), y = num(raw.rect.y, 0),
w = num(raw.rect.w, 1), h = num(raw.rect.h, 1) }
end
if type(raw.viewport) == "table" then
page.viewport = { x = num(raw.viewport.x, 0), y = num(raw.viewport.y, 0),
w = num(raw.viewport.w, 1), h = num(raw.viewport.h, 1) }
page.viewportFill = raw.viewport.fill == true
page.viewportExpand = raw.viewport.expand == true
end
for _, c in ipairs(raw.controls or {}) do
local buttons, hotkeys, keys, decorative = parseBinds(c.bind or "nul")
page.controls[#page.controls + 1] = {
spec = tostring(c.bind or "nul"),
buttons = buttons, hotkeys = hotkeys, keys = keys,
decorative = decorative,
x = num(c.x, 0.5), y = num(c.y, 0.5),
shape = c.shape == "radial" and "radial" or "rect",
rangeX = math.abs(num(c.w, 0.1)) * 0.5,
rangeY = math.abs(num(c.h, 0.1)) * 0.5,
rangeMod = num(c.rangeMod, page.rangeMod),
alphaMod = num(c.alphaMod, page.alphaMod),
reachUp = num(c.reachUp, 1), reachDown = num(c.reachDown, 1),
reachLeft = num(c.reachLeft, 1), reachRight = num(c.reachRight, 1),
imagePath = c.image,
pressedImagePath = c.imagePressed,
nextTarget = c.nextTarget,
}
end
pages[#pages + 1] = page
end
return { pages = pages, name = data.name, author = data.author,
notes = data.notes, format = "native" }
end
function TouchSkin.toNative(skin)
local out = {
name = skin.name or skin.id,
author = skin.author,
notes = skin.notes,
format = 1,
pages = {},
}
for _, page in ipairs(skin.pages or {}) do
local p = {
name = page.name,
image = page.imagePath,
fullScreen = page.fullScreen ~= false,
rangeMod = page.rangeMod,
alphaMod = page.alphaMod,
aspect = page.aspect,
controls = {},
}
if page.rect and (page.rect.x ~= 0 or page.rect.y ~= 0
or page.rect.w ~= 1 or page.rect.h ~= 1) then
p.rect = { x = page.rect.x, y = page.rect.y, w = page.rect.w, h = page.rect.h }
end
if page.viewport then
p.viewport = {
x = page.viewport.x, y = page.viewport.y,
w = page.viewport.w, h = page.viewport.h,
fill = page.viewportFill or nil,
expand = page.viewportExpand or nil,
}
end
for _, ctl in ipairs(page.controls or {}) do
p.controls[#p.controls + 1] = {
bind = ctl.spec,
x = ctl.x, y = ctl.y,
w = ctl.rangeX * 2, h = ctl.rangeY * 2,
shape = ctl.shape,
rangeMod = ctl.rangeMod ~= p.rangeMod and ctl.rangeMod or nil,
alphaMod = ctl.alphaMod ~= p.alphaMod and ctl.alphaMod or nil,
reachUp = ctl.reachUp ~= 1 and ctl.reachUp or nil,
reachDown = ctl.reachDown ~= 1 and ctl.reachDown or nil,
reachLeft = ctl.reachLeft ~= 1 and ctl.reachLeft or nil,
reachRight = ctl.reachRight ~= 1 and ctl.reachRight or nil,
image = ctl.imagePath,
imagePressed = ctl.pressedImagePath,
nextTarget = ctl.nextTarget,
}
end
out.pages[#out.pages + 1] = p
end
return out
end
function TouchSkin.serialize(skin)
return require("src.import.LuaWriter").encode(TouchSkin.toNative(skin))
end
local imageCache = setmetatable({}, { __mode = "v" })
local function loadImage(path)
if not (love and love.graphics and love.graphics.newImage) then return nil end
local cached = imageCache[path]
if cached then return cached end
local ok, img = pcall(love.graphics.newImage, path)
if not ok or not img then return nil end
if img.setFilter then img:setFilter("linear", "linear") end
imageCache[path] = img
return img
end
function TouchSkin.load(root, id)
local cfgPath, format = findConfig(root)
if not cfgPath then return nil, "no skin.lua or .cfg in " .. root end
local text = readFile(cfgPath)
if not text then return nil, "unreadable " .. cfgPath end
local skin, err
if format == "native" then
skin, err = TouchSkin.parseNative(text)
else
skin, err = TouchSkin.parse(text)
end
if not skin then return nil, err end
skin.id = id or root:match("([^/]+)$") or root
skin.root = root
skin.configPath = cfgPath
skin.format = format
skin.name = skin.name or skin.pages[1] and skin.pages[1].name or skin.id
for _, page in ipairs(skin.pages) do
if page.imagePath then
page.image = loadImage(joinPath(root, page.imagePath))
end
for _, ctl in ipairs(page.controls) do
if ctl.imagePath then ctl.image = loadImage(joinPath(root, ctl.imagePath)) end
if ctl.pressedImagePath then
ctl.pressedImage = loadImage(joinPath(root, ctl.pressedImagePath))
end
end
end
return skin
end
local function mountZip(archive, point)
if not (love and love.filesystem and love.filesystem.mount) then return false end
local ok, mounted = pcall(love.filesystem.mount, archive, point)
return ok and mounted == true
end
function TouchSkin.list()
local out, seen = {}, {}
local function scan(root, source)
for _, name in ipairs(listDir(root)) do
local id = name:gsub("%.zip$", "")
if not seen[id] then
local path = root .. "/" .. name
if name:match("%.zip$") then
local point = TouchSkin.USER_ROOT .. "/_mounted/" .. id
if mountZip(path, point) and findConfig(point) then
seen[id] = true
out[#out + 1] = { id = id, root = point, source = source, archive = path }
end
elseif isDir(path) and findConfig(path) then
seen[id] = true
out[#out + 1] = { id = id, root = path, source = source }
end
end
end
end
if love and love.filesystem and love.filesystem.createDirectory then
pcall(love.filesystem.createDirectory, TouchSkin.USER_ROOT)
end
scan(TouchSkin.USER_ROOT, "user")
scan(TouchSkin.BUNDLED_ROOT, "bundled")
table.sort(out, function(a, b) return a.id < b.id end)
return out
end
-- Drop a .zip into <save>/skins and report the id it will list under.
function TouchSkin.installArchive(name, data)
if not data or data == "" then return nil, "empty archive" end
if not (love and love.filesystem and love.filesystem.write) then
return nil, "no writable filesystem"
end
name = tostring(name or ""):match("([^/\\]+)$") or ""
name = name:gsub("[^%w%._%-]", "_")
if not name:lower():match("%.zip$") then return nil, "not a .zip" end
local id = name:gsub("%.[Zz][Ii][Pp]$", "")
if id == "" then return nil, "bad archive name" end
pcall(love.filesystem.createDirectory, TouchSkin.USER_ROOT)
local dest = TouchSkin.USER_ROOT .. "/" .. name
local ok, err = love.filesystem.write(dest, data)
if not ok then return nil, tostring(err) end
local entry = TouchSkin.find(id)
if not entry then
love.filesystem.remove(dest)
return nil, "no skin.lua or .cfg inside " .. name
end
return id
end
function TouchSkin.find(id)
if not id or id == "" then return nil end
for _, entry in ipairs(TouchSkin.list()) do
if entry.id == id then return entry end
end
return nil
end
function TouchSkin.assetPaths(skin)
local out, seen = {}, {}
local function add(rel)
if rel and rel ~= "" and not seen[rel] then
seen[rel] = true
out[#out + 1] = rel
end
end
for _, page in ipairs(skin.pages or {}) do
add(page.imagePath)
for _, ctl in ipairs(page.controls or {}) do
add(ctl.imagePath)
add(ctl.pressedImagePath)
end
end
return out
end
function TouchSkin.export(skin, destPath)
if not skin then return nil, "no skin" end
local SkinZip = require("src.core.SkinZip")
local entries = { { name = TouchSkin.NATIVE_NAME, data = TouchSkin.serialize(skin) } }
local missing = {}
for _, rel in ipairs(TouchSkin.assetPaths(skin)) do
local data = readFile(joinPath(skin.root, rel))
if data then
entries[#entries + 1] = { name = rel, data = data }
else
missing[#missing + 1] = rel
end
end
if skin.configPath and skin.format == "retroarch" then
local cfg = readFile(skin.configPath)
if cfg then
entries[#entries + 1] =
{ name = skin.configPath:match("([^/]+)$") or "overlay.cfg", data = cfg }
end
end
local blob = SkinZip.encode(entries)
destPath = destPath or (TouchSkin.USER_ROOT .. "/" .. skin.id .. "-export.zip")
local absolute = destPath:sub(1, 1) == "/" or destPath:match("^%a:[/\\]") ~= nil
if not absolute and love and love.filesystem and love.filesystem.write then
local ok, err = love.filesystem.write(destPath, blob)
if not ok then return nil, tostring(err) end
else
local handle = io.open(destPath, "wb")
if not handle then return nil, "cannot write " .. destPath end
handle:write(blob)
handle:close()
end
return destPath, missing
end
TouchSkin.BINDS = {
"nul",
"up", "down", "left", "right",
"a", "b", "start", "select",
"left|up", "right|up", "left|down", "right|down",
"hold_fast_forward", "toggle_fast_forward", "reset", "menu_toggle",
"overlay_next",
}
function TouchSkin.describeBind(spec)
local buttons, hotkeys, keys, decorative = parseBinds(spec)
if decorative then return "decoration" end
local parts = {}
for _, b in ipairs(buttons) do parts[#parts + 1] = "GB " .. b:upper() end
for _, h in ipairs(hotkeys) do parts[#parts + 1] = h end
for _, k in ipairs(keys) do parts[#parts + 1] = "key " .. k end
return table.concat(parts, " + ")
end
function TouchSkin.newControl(spec, x, y, w, h, shape)
local buttons, hotkeys, keys, decorative = parseBinds(spec)
return {
spec = spec, buttons = buttons, hotkeys = hotkeys, keys = keys,
decorative = decorative,
x = x, y = y, rangeX = w * 0.5, rangeY = h * 0.5,
shape = shape == "radial" and "radial" or "rect",
rangeMod = 1, alphaMod = 1,
reachUp = 1, reachDown = 1, reachLeft = 1, reachRight = 1,
}
end
function TouchSkin.setBind(ctl, spec)
ctl.spec = spec
ctl.buttons, ctl.hotkeys, ctl.keys, ctl.decorative = parseBinds(spec)
return ctl
end
function TouchSkin.newSkin(id)
local page = {
index = 1, name = "main", fullScreen = true, normalized = true,
rangeMod = 1, alphaMod = 1, aspect = PORTRAIT_ASPECT,
rect = { x = 0, y = 0, w = 1, h = 1 },
viewport = { x = 0, y = 0, w = 1, h = 0.5 },
viewportFill = false,
controls = {},
}
return { id = id or "new_skin", name = id or "new_skin",
root = TouchSkin.USER_ROOT .. "/" .. (id or "new_skin"),
format = "native", pages = { page } }
end
local function copyTable(src)
if type(src) ~= "table" then return src end
local out = {}
for k, v in pairs(src) do out[k] = copyTable(v) end
return out
end
function TouchSkin.clone(skin)
if not skin then return nil end
local out = {
id = skin.id, name = skin.name, root = skin.root, format = skin.format,
author = skin.author, notes = skin.notes, configPath = skin.configPath,
source = skin.source, pages = {},
}
for i, page in ipairs(skin.pages or {}) do
local p = copyTable(page)
p.image = page.image
p.controls = {}
for j, ctl in ipairs(page.controls or {}) do
local c = copyTable(ctl)
c.image = ctl.image
c.pressedImage = ctl.pressedImage
p.controls[j] = c
end
out.pages[i] = p
end
return out
end
function TouchSkin.resolveImage(root, rel)
if not rel or rel == "" then return nil end
return loadImage(joinPath(root, rel))
end
function TouchSkin.detectViewport(root, imagePath)
if not (love and love.image and love.image.newImageData) then return nil end
if not imagePath or imagePath == "" then return nil end
local ok, data = pcall(love.image.newImageData, joinPath(root, imagePath))
if not ok or not data then return nil end
local w, h = data:getWidth(), data:getHeight()
if w < 2 or h < 2 then return nil end
local function clear(x, y)
if x < 0 or y < 0 or x >= w or y >= h then return false end
local okp, _, _, _, a = pcall(data.getPixel, data, x, y)
return okp and a ~= nil and a < 0.05
end
local cx, cy = math.floor(w / 2), math.floor(h / 2)
if not clear(cx, cy) then return nil end
local left, right = cx, cx
while left > 0 and clear(left - 1, cy) do left = left - 1 end
while right < w - 1 and clear(right + 1, cy) do right = right + 1 end
local top, bottom = cy, cy
while top > 0 and clear(cx, top - 1) do top = top - 1 end
while bottom < h - 1 and clear(cx, bottom + 1) do bottom = bottom + 1 end
local rw, rh = right - left + 1, bottom - top + 1
if rw < 8 or rh < 8 then return nil end
return { x = left / w, y = top / h, w = rw / w, h = rh / h }, rw, rh
end
-- Bring outside art into the skin's own folder. A skin still living in
-- assets/ or a mounted zip is saved out to <save>/skins/<id> first, because
-- that is the only root the engine can write to.
function TouchSkin.importImage(skin, name, data)
if not skin then return nil, "no skin" end
if not data or data == "" then return nil, "no image data" end
if not (love and love.filesystem and love.filesystem.write) then
return nil, "no writable filesystem"
end
name = tostring(name):gsub("[^%w%._%-]", "_")
if name == "" then return nil, "bad file name" end
local dest = TouchSkin.USER_ROOT .. "/" .. skin.id
if skin.root ~= dest then
local saved, err = TouchSkin.saveTo(skin, skin.id)
if not saved then return nil, tostring(err) end
end
pcall(love.filesystem.createDirectory, dest .. "/img")
local rel = "img/" .. name
local ok, err = love.filesystem.write(dest .. "/" .. rel, data)
if not ok then return nil, tostring(err) end
return rel
end
function TouchSkin.listImages(root)
local out = {}
local function scan(dir, prefix)
for _, name in ipairs(listDir(dir)) do
local path = dir .. "/" .. name
if name:lower():match("%.png$") or name:lower():match("%.jpg$") then
out[#out + 1] = prefix .. name
elseif isDir(path) and prefix == "" then
scan(path, name .. "/")
end
end
end
scan(root, "")
table.sort(out)
return out
end
function TouchSkin.saveTo(skin, id)
id = id or skin.id
if not id or id == "" then return nil, "no skin id" end
if not (love and love.filesystem and love.filesystem.write) then
return nil, "no writable filesystem"
end
local dest = TouchSkin.USER_ROOT .. "/" .. id
pcall(love.filesystem.createDirectory, dest)
local copied, failed = 0, {}
for _, rel in ipairs(TouchSkin.assetPaths(skin)) do
local target = dest .. "/" .. rel
local dir = target:match("^(.*)/[^/]+$")
if dir then pcall(love.filesystem.createDirectory, dir) end
if skin.root ~= dest then
local data = readFile(joinPath(skin.root, rel))
if data then
if love.filesystem.write(target, data) then copied = copied + 1 end
else
failed[#failed + 1] = rel
end
end
end
local ok, err = love.filesystem.write(dest .. "/" .. TouchSkin.NATIVE_NAME,
TouchSkin.serialize(skin))
if not ok then return nil, tostring(err) end
skin.id, skin.root, skin.format = id, dest, "native"
return dest, failed, copied
end
TouchSkin.active = nil
TouchSkin.pageIndex = 1
TouchSkin.surfaceRect = nil
function TouchSkin.setSurface(x, y, w, h)
if not w or w <= 0 or not h or h <= 0 then
TouchSkin.surfaceRect = nil
else
TouchSkin.surfaceRect = { x = x, y = y, w = w, h = h }
end
return TouchSkin.surfaceRect
end
function TouchSkin.setActive(skin)
TouchSkin.active = skin or nil
TouchSkin.pageIndex = 1
return TouchSkin.active
end
function TouchSkin.select(id)
if not id or id == "" then return TouchSkin.setActive(nil) end
local entry = TouchSkin.find(id)
if not entry then return nil, "no skin " .. tostring(id) end
local skin, err = TouchSkin.load(entry.root, entry.id)
if not skin then return nil, err end
skin.source = entry.source
return TouchSkin.setActive(skin)
end
function TouchSkin.page()
local skin = TouchSkin.active
if not skin then return nil end
return skin.pages[TouchSkin.pageIndex] or skin.pages[1]
end
function TouchSkin.setPage(target)
local skin = TouchSkin.active
if not skin then return nil end
if type(target) == "number" then
local n = #skin.pages
TouchSkin.pageIndex = ((math.floor(target) - 1) % n) + 1
return TouchSkin.page()
end
for i, page in ipairs(skin.pages) do
if page.name == target then
TouchSkin.pageIndex = i
return page
end
end
return TouchSkin.page()
end
function TouchSkin.nextPage(target)
local skin = TouchSkin.active
if not skin then return nil end
if target and target ~= "" then return TouchSkin.setPage(target) end
return TouchSkin.setPage(TouchSkin.pageIndex + 1)
end
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
local displayAspect = w / h
if displayAspect > page.aspect then
bw = h * page.aspect
bx = ox + (w - bw) * 0.5
else
bh = w / page.aspect
by = oy + (h - bh) * 0.5
end
end
local r = page.rect
return bx + r.x * bw, by + r.y * bh, r.w * bw, r.h * bh
end
function TouchSkin.controlGeometry(page, ctl, w, h, ox, oy)
local bx, by, bw, bh = TouchSkin.pageBox(page, w, h, ox, oy)
local cx, cy = bx + ctl.x * bw, by + ctl.y * bh
local halfW, halfH = ctl.rangeX * bw, ctl.rangeY * bh
return cx, cy, halfW, halfH
end
function TouchSkin.hits(page, ctl, w, h, px, py, ox, oy)
local cx, cy, halfW, halfH = TouchSkin.controlGeometry(page, ctl, w, h, ox, oy)
local left = halfW * ctl.reachLeft * ctl.rangeMod
local right = halfW * ctl.reachRight * ctl.rangeMod
local up = halfH * ctl.reachUp * ctl.rangeMod
local down = halfH * ctl.reachDown * ctl.rangeMod
local dx = px - cx
local dy = py - cy
local rx = dx < 0 and left or right
local ry = dy < 0 and up or down
if rx <= 0 or ry <= 0 then return false end
if ctl.shape == "radial" then
return (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry) <= 1
end
return math.abs(dx) <= rx and math.abs(dy) <= ry
end
TouchSkin.overlayLive = false
function TouchSkin.setOverlayLive(on)
TouchSkin.overlayLive = on and true or false
end
function TouchSkin.decorativeOnly()
local page = TouchSkin.page()
if not page then return false end
for _, ctl in ipairs(page.controls) do
if not ctl.decorative then return false end
end
return true
end
function TouchSkin.drawable()
if not TouchSkin.active then return false end
return TouchSkin.overlayLive or TouchSkin.decorativeOnly()
end
function TouchSkin.hasViewport()
local page = TouchSkin.page()
return page ~= nil and page.viewport ~= nil and TouchSkin.drawable()
end
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
if vw <= 0 or vh <= 0 then return nil end
return x, y, vw, vh, page.viewportFill == true, page.viewportExpand == true
end
return TouchSkin