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 /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 /skins/ 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